@rebasepro/server-postgres 0.9.1-canary.16c42e9 → 0.9.1-canary.26fe4b2

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 (35) hide show
  1. package/README.md +21 -0
  2. package/dist/PostgresBackendDriver.d.ts +18 -0
  3. package/dist/PostgresBootstrapper.d.ts +7 -1
  4. package/dist/auth/services.d.ts +53 -53
  5. package/dist/index.es.js +730 -194
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
  8. package/dist/schema/auth-schema.d.ts +24 -24
  9. package/dist/schema/doctor.d.ts +1 -1
  10. package/dist/schema/introspect-db-logic.d.ts +0 -5
  11. package/dist/schema/introspect-db-naming.d.ts +10 -0
  12. package/dist/security/policy-drift.d.ts +30 -0
  13. package/dist/security/rls-enforcement.d.ts +2 -2
  14. package/dist/services/channel-history.d.ts +118 -0
  15. package/dist/services/realtimeService.d.ts +69 -2
  16. package/package.json +8 -31
  17. package/src/PostgresBackendDriver.ts +56 -5
  18. package/src/PostgresBootstrapper.ts +18 -1
  19. package/src/auth/ensure-tables.ts +97 -17
  20. package/src/auth/services.ts +134 -133
  21. package/src/cli.ts +60 -0
  22. package/src/schema/auth-bootstrap-sql.ts +7 -1
  23. package/src/schema/auth-schema.ts +13 -13
  24. package/src/schema/doctor.ts +45 -20
  25. package/src/schema/introspect-db-inference.ts +1 -1
  26. package/src/schema/introspect-db-logic.ts +1 -10
  27. package/src/schema/introspect-db-naming.ts +15 -0
  28. package/src/schema/introspect-runtime.ts +1 -1
  29. package/src/security/policy-drift.test.ts +106 -1
  30. package/src/security/policy-drift.ts +56 -0
  31. package/src/security/rls-enforcement.ts +11 -5
  32. package/src/services/channel-history.ts +343 -0
  33. package/src/services/realtimeService.ts +198 -10
  34. package/src/services/row-pipeline.ts +27 -3
  35. package/src/websocket.ts +30 -11
package/dist/index.es.js CHANGED
@@ -353,7 +353,7 @@ function getDeclaredSubcollections(collection) {
353
353
  /**
354
354
  * The id a request without a logged-in user reports as `auth.uid()`.
355
355
  *
356
- * A user-context request always sets `app.user_id`: blank would read back as
356
+ * A user-context request always sets `app.uid`: blank would read back as
357
357
  * `NULL`, and `NULL` is how the trusted server context is recognised, so an
358
358
  * anonymous visitor would be promoted to server privileges. The driver
359
359
  * therefore substitutes this sentinel at the single chokepoint where the GUC
@@ -2134,7 +2134,7 @@ function getSubcollections(collection) {
2134
2134
  * It handles:
2135
2135
  * - `field = 'literal'`
2136
2136
  * - `field != 'literal'`
2137
- * - `field = current_setting('app.user_id')`
2137
+ * - `field = current_setting('app.uid')` (or the legacy `app.user_id`)
2138
2138
  * - `A AND B`, `A OR B` — only where the keyword is at the top level
2139
2139
  * - `true`
2140
2140
  * - `IN (...)` (as optimistic true)
@@ -2335,7 +2335,7 @@ function findAnonymousGrants(expr) {
2335
2335
  return found;
2336
2336
  }
2337
2337
  function parseOperand(str) {
2338
- if (/current_setting\s*\(\s*'app\.user_id'\s*\)/i.test(str) || /auth\.uid\(\)/i.test(str)) return policy.authUid();
2338
+ if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /auth\.uid\(\)/i.test(str)) return policy.authUid();
2339
2339
  const stringMatch = str.match(/^'(.+)'$/);
2340
2340
  if (stringMatch) return policy.literal(stringMatch[1]);
2341
2341
  if (/^\w+$/.test(str)) return policy.field(str);
@@ -6524,8 +6524,27 @@ function coerceDeclaredNumbers(row, collection) {
6524
6524
  return out;
6525
6525
  }
6526
6526
  /** Render one target row in the requested style. */
6527
+ /**
6528
+ * Drop every column the collection marked `excludeFromApi`.
6529
+ *
6530
+ * Password hashes and verification tokens have to be readable server-side but
6531
+ * must never reach a client — and "never" has to mean every exit from this
6532
+ * pipeline, including relation targets, or a secret leaks through whichever
6533
+ * path was overlooked. Keyed by both the property name and its column name,
6534
+ * since a row can arrive keyed either way depending on the caller.
6535
+ */
6536
+ function stripExcluded(row, collection) {
6537
+ const properties = collection.properties;
6538
+ if (!properties) return row;
6539
+ for (const [key, property] of Object.entries(properties)) {
6540
+ if (!property?.excludeFromApi) continue;
6541
+ delete row[key];
6542
+ if (property.columnName) delete row[property.columnName];
6543
+ }
6544
+ return row;
6545
+ }
6527
6546
  function renderTarget(targetRow, targetCollection, style, registry) {
6528
- if (style === "inline") return coerceDeclaredNumbers({ ...targetRow }, targetCollection);
6547
+ if (style === "inline") return stripExcluded(coerceDeclaredNumbers({ ...targetRow }, targetCollection), targetCollection);
6529
6548
  const address = relationTargetAddress(targetRow, targetCollection, registry);
6530
6549
  const path = targetCollection.slug;
6531
6550
  return createRelationRefWithData(address, path, {
@@ -6567,7 +6586,7 @@ function toCmsRow(row, collection, registry) {
6567
6586
  normalized[key] = relData.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, targetCollection, "ref", registry));
6568
6587
  } else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) normalized[key] = renderTarget(relData, relation.target(), "ref", registry);
6569
6588
  }
6570
- return normalized;
6589
+ return stripExcluded(normalized, collection);
6571
6590
  }
6572
6591
  /**
6573
6592
  * The row REST serves: every column under its own name, with the value Postgres
@@ -6592,7 +6611,7 @@ function toRestRow(row, collection, registry) {
6592
6611
  else if (relation && typeof value === "object" && value !== null) flat[key] = renderTarget(value, relation.target(), "inline", registry);
6593
6612
  else flat[key] = coerceDeclaredNumber(value, collection.properties?.[key]);
6594
6613
  }
6595
- return flat;
6614
+ return stripExcluded(flat, collection);
6596
6615
  }
6597
6616
  //#endregion
6598
6617
  //#region src/services/FetchService.ts
@@ -8163,7 +8182,7 @@ async function ensureAppRole(run, schemas) {
8163
8182
  * SECURITY: this function is only ever called on the **user** path (the server
8164
8183
  * context uses the base/owner driver and never calls it). The default policies
8165
8184
  * treat `auth.uid() IS NULL` as the trusted server context, and `auth.uid()`
8166
- * is `NULLIF(current_setting('app.user_id'), '')` — so an EMPTY user id would
8185
+ * is `NULLIF(current_setting('app.uid'), '')` — so an EMPTY user id would
8167
8186
  * be read as NULL and silently escalate a user request to server privileges.
8168
8187
  * Coerce empty/blank ids to `ANONYMOUS_USER_ID` here, at the single chokepoint,
8169
8188
  * rather than trusting every caller (e.g. realtime subscription auth) to do it.
@@ -8171,14 +8190,15 @@ async function ensureAppRole(run, schemas) {
8171
8190
  * semantics: it is why `auth.uid() IS NOT NULL` is true for anonymous requests.
8172
8191
  */
8173
8192
  async function applyAuthContext(tx, auth, userRole) {
8174
- const userId = typeof auth.userId === "string" && auth.userId.trim() !== "" ? auth.userId : ANONYMOUS_USER_ID;
8193
+ const uid = typeof auth.uid === "string" && auth.uid.trim() !== "" ? auth.uid : ANONYMOUS_USER_ID;
8175
8194
  const normalizedRoles = auth.roles.map((r) => typeof r === "string" ? r : r?.id ?? String(r));
8176
8195
  await tx.execute(sql`
8177
8196
  SELECT
8178
- set_config('app.user_id', ${userId}, true),
8197
+ set_config('app.uid', ${uid}, true),
8198
+ set_config('app.user_id', ${uid}, true),
8179
8199
  set_config('app.user_roles', ${normalizedRoles.join(",")}, true),
8180
8200
  set_config('app.jwt', ${JSON.stringify({
8181
- sub: userId,
8201
+ sub: uid,
8182
8202
  roles: auth.roles
8183
8203
  })}, true)
8184
8204
  `);
@@ -8324,6 +8344,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
8324
8344
  executeSql: (...args) => this.executeSql(...args),
8325
8345
  fetchAvailableDatabases: () => this.fetchAvailableDatabases(),
8326
8346
  fetchAvailableRoles: () => this.fetchAvailableRoles(),
8347
+ fetchApplicationRoles: () => this.fetchApplicationRoles(),
8327
8348
  fetchCurrentDatabase: () => this.fetchCurrentDatabase(),
8328
8349
  fetchUnmappedTables: (...args) => this.fetchUnmappedTables(...args),
8329
8350
  fetchTableMetadata: (...args) => this.fetchTableMetadata(...args),
@@ -8977,6 +8998,45 @@ var PostgresBackendDriver = class PostgresBackendDriver {
8977
8998
  async fetchAvailableRoles() {
8978
8999
  return (await this.executeSql("SELECT rolname FROM pg_roles WHERE pg_has_role(current_user, rolname, 'member') ORDER BY rolname;")).map((r) => r.rolname);
8979
9000
  }
9001
+ /**
9002
+ * Application-level roles actually in use in this project.
9003
+ *
9004
+ * Distinct from {@link fetchAvailableRoles}, which returns native
9005
+ * PostgreSQL roles from `pg_roles` (`postgres`, `rebase_user`, …). Those
9006
+ * are the roles the SQL editor can `SET ROLE` to. *These* are the strings
9007
+ * held in the users table's `roles` column, injected per-transaction as
9008
+ * `auth.roles()` and matched by `SecurityRule.roles`. Feeding the pg roles
9009
+ * into a `SecurityRule.roles` field produces a condition no user can ever
9010
+ * satisfy, so the two must not be conflated.
9011
+ *
9012
+ * Roles have no registry table — they were migrated out of
9013
+ * `rebase.user_roles` onto an inline `roles TEXT[]` column — so the live
9014
+ * set is derived from what is assigned. A role that is declared in a policy
9015
+ * but held by nobody yet cannot be discovered here; callers that need it
9016
+ * should union in the roles they already know about.
9017
+ */
9018
+ async fetchApplicationRoles() {
9019
+ const located = await this.executeSql(`
9020
+ SELECT table_schema, table_name
9021
+ FROM information_schema.columns
9022
+ WHERE column_name = 'roles'
9023
+ AND data_type = 'ARRAY'
9024
+ AND table_name = 'users'
9025
+ AND table_schema NOT IN ('information_schema', 'pg_catalog')
9026
+ ORDER BY (table_schema = 'rebase') DESC, table_schema
9027
+ LIMIT 1;
9028
+ `);
9029
+ if (located.length === 0) return [];
9030
+ const schema = located[0].table_schema;
9031
+ const table = located[0].table_name;
9032
+ const qualified = `"${schema.replace(/"/g, "\"\"")}"."${table.replace(/"/g, "\"\"")}"`;
9033
+ return (await this.executeSql(`
9034
+ SELECT DISTINCT unnest(roles) AS role
9035
+ FROM ${qualified}
9036
+ WHERE roles IS NOT NULL
9037
+ ORDER BY role;
9038
+ `)).map((r) => r.role).filter((r) => typeof r === "string" && r.length > 0);
9039
+ }
8980
9040
  async fetchCurrentDatabase() {
8981
9041
  return this.poolManager?.defaultDatabaseName;
8982
9042
  }
@@ -9118,15 +9178,15 @@ var AuthenticatedPostgresBackendDriver = class {
9118
9178
  async withTransaction(operation, options) {
9119
9179
  const pendingNotifications = [];
9120
9180
  const result = await this.delegate.db.transaction(async (tx) => {
9121
- let userId = this.user?.uid;
9122
- if (!userId) {
9181
+ let uid = this.user?.uid;
9182
+ if (!uid) {
9123
9183
  logger.warn("[DataDriver] User ID (uid) is missing for authenticated delegate. Using 'anonymous'. User object", { detail: this.user });
9124
- userId = "anonymous";
9184
+ uid = "anonymous";
9125
9185
  }
9126
9186
  const userRoles = this.user?.roles ?? [];
9127
9187
  if (!this.user?.roles) logger.warn("[DataDriver] User roles are missing for authenticated delegate. Using empty array. User object", { detail: this.user });
9128
9188
  await applyAuthContext(tx, {
9129
- userId,
9189
+ uid,
9130
9190
  roles: userRoles
9131
9191
  }, this.delegate.rlsUserRole);
9132
9192
  const txEntityService = new DataService(tx, this.delegate.registry);
@@ -9153,7 +9213,7 @@ var AuthenticatedPostgresBackendDriver = class {
9153
9213
  */
9154
9214
  injectAuthContext(unsubscribe) {
9155
9215
  const authContext = {
9156
- userId: this.user?.uid || "anonymous",
9216
+ uid: this.user?.uid || "anonymous",
9157
9217
  roles: this.user?.roles ?? []
9158
9218
  };
9159
9219
  const entries = Array.from(this.delegate.realtimeService.subscriptions.entries());
@@ -9297,19 +9357,19 @@ function createAuthSchema(usersSchemaName = "rebase") {
9297
9357
  */
9298
9358
  const refreshTokens = tableCreator("refresh_tokens", {
9299
9359
  id: uuid("id").defaultRandom().primaryKey(),
9300
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
9360
+ uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
9301
9361
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
9302
9362
  expiresAt: timestamp("expires_at").notNull(),
9303
9363
  userAgent: varchar("user_agent", { length: 500 }),
9304
9364
  ipAddress: varchar("ip_address", { length: 45 }),
9305
9365
  createdAt: timestamp("created_at").defaultNow().notNull()
9306
- }, (table) => ({ uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress) }));
9366
+ }, (table) => ({ uniqueDeviceSession: unique("unique_device_session").on(table.uid, table.userAgent, table.ipAddress) }));
9307
9367
  /**
9308
9368
  * Password reset tokens for forgot password flow
9309
9369
  */
9310
9370
  const passwordResetTokens = tableCreator("password_reset_tokens", {
9311
9371
  id: uuid("id").defaultRandom().primaryKey(),
9312
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
9372
+ uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
9313
9373
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
9314
9374
  expiresAt: timestamp("expires_at").notNull(),
9315
9375
  usedAt: timestamp("used_at"),
@@ -9328,7 +9388,7 @@ function createAuthSchema(usersSchemaName = "rebase") {
9328
9388
  */
9329
9389
  const userIdentities = tableCreator("user_identities", {
9330
9390
  id: uuid("id").defaultRandom().primaryKey(),
9331
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
9391
+ uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
9332
9392
  provider: varchar("provider", { length: 50 }).notNull(),
9333
9393
  providerId: varchar("provider_id", { length: 255 }).notNull(),
9334
9394
  profileData: jsonb("profile_data"),
@@ -9340,7 +9400,7 @@ function createAuthSchema(usersSchemaName = "rebase") {
9340
9400
  */
9341
9401
  const mfaFactors = tableCreator("mfa_factors", {
9342
9402
  id: uuid("id").defaultRandom().primaryKey(),
9343
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
9403
+ uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
9344
9404
  factorType: varchar("factor_type", { length: 20 }).notNull(),
9345
9405
  secretEncrypted: varchar("secret_encrypted", { length: 500 }).notNull(),
9346
9406
  friendlyName: varchar("friendly_name", { length: 255 }),
@@ -9366,14 +9426,14 @@ function createAuthSchema(usersSchemaName = "rebase") {
9366
9426
  }),
9367
9427
  recoveryCodes: tableCreator("recovery_codes", {
9368
9428
  id: uuid("id").defaultRandom().primaryKey(),
9369
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
9429
+ uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
9370
9430
  codeHash: varchar("code_hash", { length: 255 }).notNull(),
9371
9431
  usedAt: timestamp("used_at"),
9372
9432
  createdAt: timestamp("created_at").defaultNow().notNull()
9373
9433
  }),
9374
9434
  magicLinkTokens: tableCreator("magic_link_tokens", {
9375
9435
  id: uuid("id").defaultRandom().primaryKey(),
9376
- userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
9436
+ uid: uuid("uid").notNull().references(() => users.id, { onDelete: "cascade" }),
9377
9437
  tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
9378
9438
  expiresAt: timestamp("expires_at").notNull(),
9379
9439
  usedAt: timestamp("used_at"),
@@ -9401,20 +9461,20 @@ var usersRelations = relations(users, ({ many }) => ({
9401
9461
  magicLinkTokens: many(magicLinkTokens)
9402
9462
  }));
9403
9463
  var refreshTokensRelations = relations(refreshTokens, ({ one }) => ({ user: one(users, {
9404
- fields: [refreshTokens.userId],
9464
+ fields: [refreshTokens.uid],
9405
9465
  references: [users.id]
9406
9466
  }) }));
9407
9467
  var passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({ user: one(users, {
9408
- fields: [passwordResetTokens.userId],
9468
+ fields: [passwordResetTokens.uid],
9409
9469
  references: [users.id]
9410
9470
  }) }));
9411
9471
  var userIdentitiesRelations = relations(userIdentities, ({ one }) => ({ user: one(users, {
9412
- fields: [userIdentities.userId],
9472
+ fields: [userIdentities.uid],
9413
9473
  references: [users.id]
9414
9474
  }) }));
9415
9475
  var mfaFactorsRelations = relations(mfaFactors, ({ one, many }) => ({
9416
9476
  user: one(users, {
9417
- fields: [mfaFactors.userId],
9477
+ fields: [mfaFactors.uid],
9418
9478
  references: [users.id]
9419
9479
  }),
9420
9480
  challenges: many(mfaChallenges)
@@ -9424,11 +9484,11 @@ var mfaChallengesRelations = relations(mfaChallenges, ({ one }) => ({ factor: on
9424
9484
  references: [mfaFactors.id]
9425
9485
  }) }));
9426
9486
  var recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({ user: one(users, {
9427
- fields: [recoveryCodes.userId],
9487
+ fields: [recoveryCodes.uid],
9428
9488
  references: [users.id]
9429
9489
  }) }));
9430
9490
  var magicLinkTokensRelations = relations(magicLinkTokens, ({ one }) => ({ user: one(users, {
9431
- fields: [magicLinkTokens.userId],
9491
+ fields: [magicLinkTokens.uid],
9432
9492
  references: [users.id]
9433
9493
  }) }));
9434
9494
  //#endregion
@@ -10261,6 +10321,267 @@ var CdcListener = class CdcListener {
10261
10321
  }
10262
10322
  };
10263
10323
  //#endregion
10324
+ //#region src/services/channel-history.ts
10325
+ /**
10326
+ * Ordered, replayable per-channel message history.
10327
+ *
10328
+ * Broadcast on its own is fire-and-forget to whoever is connected at the
10329
+ * instant it is sent: fine for presence and for "someone saved" notifications,
10330
+ * not enough for op-based collaborative editing, where a client that blinks
10331
+ * out for two seconds has to resync a whole document rather than catch up on
10332
+ * the four operations it missed. This adds the missing half — every retained
10333
+ * broadcast gets a per-channel sequence number, and a client can ask for
10334
+ * everything after the last one it saw.
10335
+ *
10336
+ * Three decisions worth stating, because each rules out a simpler-looking one:
10337
+ *
10338
+ * - **Retention is server-side and opt-in.** A channel is created by whoever
10339
+ * names it, so a client-supplied history depth would let any visitor commit
10340
+ * the backend to unbounded storage. And presence channels — the common case
10341
+ * — must not pay for this: with no rules configured nothing is written, no
10342
+ * table is created, and `broadcast` runs exactly the code it ran before.
10343
+ *
10344
+ * - **Sequence numbers come from the database, not from a counter in this
10345
+ * process.** They have to survive a restart and be shared across instances;
10346
+ * an in-memory counter would restart at 1 after a deploy and hand a
10347
+ * reconnecting client a replay from the wrong era, silently.
10348
+ *
10349
+ * - **The cursor row outlives the messages it numbered.** Pruning is what
10350
+ * makes retention affordable, but pruning the cursor along with the messages
10351
+ * would restart the sequence and make `sinceSeq` mean something different
10352
+ * before and after — the worst kind of bug, because replay would still
10353
+ * return rows and they would look plausible. Cursors are tiny and are kept
10354
+ * forever; see {@link prune}, which touches only `channel_messages`.
10355
+ */
10356
+ /** How many messages a replay returns when the caller does not say. */
10357
+ var DEFAULT_REPLAY_LIMIT = 200;
10358
+ /**
10359
+ * Hard ceiling on one replay, whatever the caller asks for.
10360
+ *
10361
+ * A reconnecting client names its own `limit`, so this is the only thing
10362
+ * standing between a stale `sinceSeq` and a single frame carrying a channel's
10363
+ * entire retained history. A client that is further behind than this is told so
10364
+ * via `latestSeq` and can decide to resync wholesale instead of paging.
10365
+ */
10366
+ var MAX_REPLAY_LIMIT = 1e3;
10367
+ /** Minimum gap between two prunes of the same channel. */
10368
+ var PRUNE_THROTTLE_MS = 3e4;
10369
+ /**
10370
+ * Parse a retention TTL into milliseconds.
10371
+ *
10372
+ * Accepts a raw millisecond count or a short duration string (`"30s"`, `"15m"`,
10373
+ * `"24h"`, `"7d"`). Returns undefined for anything unparseable, which the
10374
+ * caller treats as "no TTL" — a misspelt duration must not silently become an
10375
+ * aggressive one.
10376
+ */
10377
+ function parseTtlMs(ttl) {
10378
+ if (ttl === void 0 || ttl === null) return void 0;
10379
+ if (typeof ttl === "number") return Number.isFinite(ttl) && ttl > 0 ? ttl : void 0;
10380
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)\s*$/i.exec(ttl);
10381
+ if (!match) {
10382
+ logger.warn(`⚠️ [ChannelHistory] Ignoring unparseable retention ttl "${ttl}" — expected e.g. "30s", "15m", "24h", "7d".`);
10383
+ return;
10384
+ }
10385
+ const value = parseFloat(match[1]);
10386
+ const unit = match[2].toLowerCase();
10387
+ const ms = value * (unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5);
10388
+ return ms > 0 ? ms : void 0;
10389
+ }
10390
+ /**
10391
+ * Whether `channel` is covered by `rule`.
10392
+ *
10393
+ * Exact match, or a trailing `*` acting as a prefix. Not a general glob: this
10394
+ * decides what reaches disk, and a pattern language whose reach is not obvious
10395
+ * at a glance is the wrong tool for that job.
10396
+ */
10397
+ function channelMatchesRule(channel, rule) {
10398
+ const pattern = rule.match;
10399
+ if (pattern === "*") return true;
10400
+ if (pattern.endsWith("*")) return channel.startsWith(pattern.slice(0, -1));
10401
+ return channel === pattern;
10402
+ }
10403
+ /**
10404
+ * Persistence and replay for retained channels.
10405
+ *
10406
+ * Inert unless constructed with at least one rule: {@link enabled} is false,
10407
+ * {@link ensureTables} does nothing, and {@link retentionFor} answers undefined
10408
+ * for every channel, so the realtime service never reaches the SQL below.
10409
+ */
10410
+ var ChannelHistoryStore = class {
10411
+ db;
10412
+ rules;
10413
+ /** Resolved rule per channel name, so the match runs once per channel. */
10414
+ resolved = /* @__PURE__ */ new Map();
10415
+ /** Channel → timestamp of its last prune, for {@link PRUNE_THROTTLE_MS}. */
10416
+ lastPruned = /* @__PURE__ */ new Map();
10417
+ tablesReady = false;
10418
+ constructor(db, rules = []) {
10419
+ this.db = db;
10420
+ this.rules = rules.filter((rule) => {
10421
+ if (!rule?.match) {
10422
+ logger.warn("⚠️ [ChannelHistory] Ignoring a retention rule with no `match`.");
10423
+ return false;
10424
+ }
10425
+ if (!(rule.limit !== void 0 || rule.ttl !== void 0)) {
10426
+ logger.warn(`⚠️ [ChannelHistory] Retention rule "${rule.match}" sets neither \`limit\` nor \`ttl\` — ignoring it, as it would retain forever.`);
10427
+ return false;
10428
+ }
10429
+ return true;
10430
+ });
10431
+ }
10432
+ /** Whether any channel retains anything at all. */
10433
+ get enabled() {
10434
+ return this.rules.length > 0;
10435
+ }
10436
+ /**
10437
+ * The retention that applies to `channel`, or undefined when none does.
10438
+ *
10439
+ * First matching rule wins, so callers order them most-specific first.
10440
+ */
10441
+ retentionFor(channel) {
10442
+ if (!this.enabled || !channel) return void 0;
10443
+ const cached = this.resolved.get(channel);
10444
+ if (cached !== void 0) return cached ?? void 0;
10445
+ const rule = this.rules.find((r) => channelMatchesRule(channel, r));
10446
+ const resolved = rule ? {
10447
+ limit: rule.limit,
10448
+ ttlMs: parseTtlMs(rule.ttl)
10449
+ } : null;
10450
+ this.resolved.set(channel, resolved);
10451
+ return resolved ?? void 0;
10452
+ }
10453
+ /**
10454
+ * Create the history tables. Idempotent, and a no-op when no rule is set —
10455
+ * a deployment that never retains anything gets no schema for it.
10456
+ */
10457
+ async ensureTables() {
10458
+ if (!this.enabled || this.tablesReady) return;
10459
+ await this.db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
10460
+ await this.db.execute(sql`
10461
+ CREATE TABLE IF NOT EXISTS rebase.channel_messages (
10462
+ channel TEXT NOT NULL,
10463
+ seq BIGINT NOT NULL,
10464
+ event TEXT NOT NULL,
10465
+ payload JSONB,
10466
+ sender_id TEXT,
10467
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10468
+ PRIMARY KEY (channel, seq)
10469
+ )
10470
+ `);
10471
+ await this.db.execute(sql`
10472
+ CREATE INDEX IF NOT EXISTS idx_channel_messages_created
10473
+ ON rebase.channel_messages (created_at)
10474
+ `);
10475
+ await this.db.execute(sql`
10476
+ CREATE TABLE IF NOT EXISTS rebase.channel_cursors (
10477
+ channel TEXT PRIMARY KEY,
10478
+ last_seq BIGINT NOT NULL
10479
+ )
10480
+ `);
10481
+ this.tablesReady = true;
10482
+ logger.info(`✅ [ChannelHistory] Retained channels ready (${this.rules.length} rule(s)).`);
10483
+ }
10484
+ /**
10485
+ * Append a broadcast and return the sequence number it was given.
10486
+ *
10487
+ * The sequence is allocated by the same statement that stores the message,
10488
+ * so a crash between the two is not a possibility. `ON CONFLICT DO UPDATE`
10489
+ * takes a row lock on the channel's cursor, which is what makes concurrent
10490
+ * broadcasts to one channel line up in a single order — and what keeps
10491
+ * different channels from contending with each other at all.
10492
+ */
10493
+ async append(channel, event, payload, senderId) {
10494
+ const row = (await this.db.execute(sql`
10495
+ WITH next AS (
10496
+ INSERT INTO rebase.channel_cursors (channel, last_seq)
10497
+ VALUES (${channel}, 1)
10498
+ ON CONFLICT (channel)
10499
+ DO UPDATE SET last_seq = rebase.channel_cursors.last_seq + 1
10500
+ RETURNING last_seq
10501
+ )
10502
+ INSERT INTO rebase.channel_messages (channel, seq, event, payload, sender_id)
10503
+ SELECT ${channel}, next.last_seq, ${event}, ${JSON.stringify(payload ?? null)}::jsonb, ${senderId ?? null}
10504
+ FROM next
10505
+ RETURNING seq, created_at
10506
+ `)).rows[0];
10507
+ if (!row) throw new Error(`Failed to append to channel history for "${channel}"`);
10508
+ return {
10509
+ seq: Number(row.seq),
10510
+ at: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at)
10511
+ };
10512
+ }
10513
+ /**
10514
+ * Everything retained for `channel` after `sinceSeq`, oldest first.
10515
+ *
10516
+ * `latestSeq` is reported whether or not the messages were capped, so a
10517
+ * client that is further behind than one page can tell.
10518
+ */
10519
+ async replay(channel, sinceSeq = 0, limit = DEFAULT_REPLAY_LIMIT) {
10520
+ const capped = Math.max(1, Math.min(Math.floor(limit) || DEFAULT_REPLAY_LIMIT, MAX_REPLAY_LIMIT));
10521
+ const after = Number.isFinite(sinceSeq) && sinceSeq > 0 ? Math.floor(sinceSeq) : 0;
10522
+ const messages = (await this.db.execute(sql`
10523
+ SELECT seq, event, payload, sender_id, created_at
10524
+ FROM rebase.channel_messages
10525
+ WHERE channel = ${channel} AND seq > ${after}
10526
+ ORDER BY seq ASC
10527
+ LIMIT ${capped}
10528
+ `)).rows.map((row) => ({
10529
+ seq: Number(row.seq),
10530
+ event: row.event,
10531
+ payload: row.payload,
10532
+ senderId: row.sender_id ?? void 0,
10533
+ at: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at)
10534
+ }));
10535
+ const cursorRow = (await this.db.execute(sql`
10536
+ SELECT last_seq FROM rebase.channel_cursors WHERE channel = ${channel}
10537
+ `)).rows[0];
10538
+ return {
10539
+ messages,
10540
+ latestSeq: cursorRow ? Number(cursorRow.last_seq) : 0
10541
+ };
10542
+ }
10543
+ /**
10544
+ * Enforce a channel's retention bounds.
10545
+ *
10546
+ * Throttled per channel, so a burst of operations prunes once rather than
10547
+ * once per message — the cost then tracks elapsed time instead of write
10548
+ * volume, which is what makes retention affordable on a hot channel.
10549
+ */
10550
+ async prune(channel, retention) {
10551
+ const now = Date.now();
10552
+ if (now - (this.lastPruned.get(channel) ?? 0) < PRUNE_THROTTLE_MS) return 0;
10553
+ this.lastPruned.set(channel, now);
10554
+ let deleted = 0;
10555
+ if (retention.ttlMs !== void 0) {
10556
+ const result = await this.db.execute(sql`
10557
+ DELETE FROM rebase.channel_messages
10558
+ WHERE channel = ${channel}
10559
+ AND created_at < NOW() - MAKE_INTERVAL(secs => ${retention.ttlMs / 1e3})
10560
+ `);
10561
+ deleted += result.rowCount ?? 0;
10562
+ }
10563
+ if (retention.limit !== void 0 && retention.limit > 0) {
10564
+ const result = await this.db.execute(sql`
10565
+ DELETE FROM rebase.channel_messages
10566
+ WHERE channel = ${channel}
10567
+ AND seq <= (
10568
+ SELECT seq FROM rebase.channel_messages
10569
+ WHERE channel = ${channel}
10570
+ ORDER BY seq DESC
10571
+ OFFSET ${Math.floor(retention.limit)} LIMIT 1
10572
+ )
10573
+ `);
10574
+ deleted += result.rowCount ?? 0;
10575
+ }
10576
+ return deleted;
10577
+ }
10578
+ /** Forget throttle and match caches. Called on shutdown. */
10579
+ clear() {
10580
+ this.resolved.clear();
10581
+ this.lastPruned.clear();
10582
+ }
10583
+ };
10584
+ //#endregion
10264
10585
  //#region src/services/realtimeService.ts
10265
10586
  /** Channel name used for Postgres LISTEN/NOTIFY cross-instance realtime. */
10266
10587
  var PG_NOTIFY_CHANNEL = "rebase_entity_changes";
@@ -10276,6 +10597,26 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10276
10597
  clients = /* @__PURE__ */ new Map();
10277
10598
  channels = /* @__PURE__ */ new Map();
10278
10599
  presence = /* @__PURE__ */ new Map();
10600
+ /**
10601
+ * Ordered, replayable history for channels that opt into it.
10602
+ *
10603
+ * Undefined until {@link configureChannelHistory} is called, and inert even
10604
+ * then unless retention rules were supplied — so presence and ephemeral
10605
+ * notification channels never touch it. See `channel-history.ts`.
10606
+ */
10607
+ channelHistory;
10608
+ /**
10609
+ * One promise chain per retained channel, so that assigning a sequence
10610
+ * number and fanning the message out happen in the same order for every
10611
+ * message on that channel.
10612
+ *
10613
+ * Without it, two concurrent broadcasts can be numbered 4 and 5 by the
10614
+ * database and still reach subscribers as 5 then 4 — live order and replay
10615
+ * order would disagree, which is exactly the divergence sequence numbers
10616
+ * are supposed to rule out. Keyed by channel, so unrelated channels never
10617
+ * wait on each other.
10618
+ */
10619
+ channelSendQueues = /* @__PURE__ */ new Map();
10279
10620
  presenceInterval;
10280
10621
  static PRESENCE_TIMEOUT_MS = 3e4;
10281
10622
  dataService;
@@ -10452,6 +10793,9 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10452
10793
  case "broadcast":
10453
10794
  this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
10454
10795
  break;
10796
+ case "channel_history":
10797
+ await this.handleChannelHistoryRequest(clientId, payload?.channel, payload?.sinceSeq, payload?.limit);
10798
+ break;
10455
10799
  case "presence_track":
10456
10800
  this.joinChannel(clientId, payload?.channel);
10457
10801
  this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
@@ -10662,12 +11006,12 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10662
11006
  if (this.driver) {
10663
11007
  const collection = this.registry.getCollectionByPath(notifyPath);
10664
11008
  const activeAuth = authContext || {
10665
- userId: "anon",
11009
+ uid: "anon",
10666
11010
  roles: ["anon"]
10667
11011
  };
10668
11012
  return await this.db.transaction(async (tx) => {
10669
11013
  await applyAuthContext(tx, {
10670
- userId: activeAuth.userId,
11014
+ uid: activeAuth.uid,
10671
11015
  roles: activeAuth.roles
10672
11016
  }, this.rlsUserRole);
10673
11017
  const txEntityService = new DataService(tx, this.registry);
@@ -10699,7 +11043,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10699
11043
  if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
10700
11044
  const contextForCallback = {
10701
11045
  user: {
10702
- uid: activeAuth.userId,
11046
+ uid: activeAuth.uid,
10703
11047
  roles: activeAuth.roles
10704
11048
  },
10705
11049
  driver: this.driver,
@@ -10791,12 +11135,12 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10791
11135
  if (this.driver) {
10792
11136
  const collection = this.registry.getCollectionByPath(notifyPath);
10793
11137
  const activeAuth = authContext || {
10794
- userId: "anon",
11138
+ uid: "anon",
10795
11139
  roles: ["anon"]
10796
11140
  };
10797
11141
  return await this.db.transaction(async (tx) => {
10798
11142
  await applyAuthContext(tx, {
10799
- userId: activeAuth.userId,
11143
+ uid: activeAuth.uid,
10800
11144
  roles: activeAuth.roles
10801
11145
  }, this.rlsUserRole);
10802
11146
  let processedEntity = await new DataService(tx, this.registry).fetchOne(notifyPath, id, collection?.databaseId);
@@ -10812,7 +11156,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10812
11156
  if (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead) {
10813
11157
  const contextForCallback = {
10814
11158
  user: {
10815
- uid: activeAuth.userId,
11159
+ uid: activeAuth.uid,
10816
11160
  roles: activeAuth.roles
10817
11161
  },
10818
11162
  driver: this.driver,
@@ -10938,15 +11282,67 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10938
11282
  }
10939
11283
  this.removePresence(clientId, channel);
10940
11284
  }
10941
- /** Broadcast a message to all clients in a channel except sender */
11285
+ /**
11286
+ * Broadcast a message to all clients in a channel except the sender.
11287
+ *
11288
+ * On a channel with no retention rule this is what it always was: a
11289
+ * synchronous fan-out to whoever is connected, with no sequence number, no
11290
+ * SQL and no await — the body below runs to completion before returning.
11291
+ *
11292
+ * On a retained channel the message is durably numbered first and only then
11293
+ * delivered, through a per-channel queue so that delivery order matches
11294
+ * sequence order. That ordering is the whole point: a client that catches up
11295
+ * with `sinceSeq` has to arrive at the same state as one that never
11296
+ * disconnected.
11297
+ */
10942
11298
  broadcastToChannel(clientId, channel, event, payload) {
11299
+ const retention = this.channelHistory?.retentionFor(channel);
11300
+ if (!retention) {
11301
+ this.fanOutBroadcast(clientId, channel, event, payload);
11302
+ return;
11303
+ }
11304
+ const next = (this.channelSendQueues.get(channel) ?? Promise.resolve()).catch(() => {}).then(() => this.persistAndFanOut(clientId, channel, event, payload, retention));
11305
+ this.channelSendQueues.set(channel, next);
11306
+ next.finally(() => {
11307
+ if (this.channelSendQueues.get(channel) === next) this.channelSendQueues.delete(channel);
11308
+ });
11309
+ }
11310
+ /**
11311
+ * Number a broadcast, store it, then deliver it.
11312
+ *
11313
+ * A message that cannot be stored is **not** delivered. Delivering it would
11314
+ * put it in front of live subscribers while leaving it absent from every
11315
+ * future replay — the two views of the channel would disagree permanently,
11316
+ * and no later message could repair the gap. Failing loudly to the sender
11317
+ * instead lets it retry, which for an operation stream is the only outcome
11318
+ * that keeps clients convergent.
11319
+ */
11320
+ async persistAndFanOut(clientId, channel, event, payload, retention) {
11321
+ let seq;
11322
+ try {
11323
+ ({seq} = await this.channelHistory.append(channel, event, payload, clientId));
11324
+ } catch (error) {
11325
+ logger.error(`❌ [ChannelHistory] Could not persist broadcast on "${channel}" — message dropped`, { error });
11326
+ this.sendError(clientId, `Could not persist broadcast on retained channel "${channel}"`, void 0, "CHANNEL_HISTORY_WRITE_FAILED");
11327
+ return;
11328
+ }
11329
+ this.fanOutBroadcast(clientId, channel, event, payload, seq);
11330
+ try {
11331
+ await this.channelHistory.prune(channel, retention);
11332
+ } catch (error) {
11333
+ logger.warn(`⚠️ [ChannelHistory] Prune failed for "${channel}"`, { error });
11334
+ }
11335
+ }
11336
+ /** Deliver a broadcast frame to every member of a channel but the sender. */
11337
+ fanOutBroadcast(clientId, channel, event, payload, seq) {
10943
11338
  const members = this.channels.get(channel);
10944
11339
  if (!members) return;
10945
11340
  const message = JSON.stringify({
10946
11341
  type: "broadcast",
10947
11342
  channel,
10948
11343
  event,
10949
- payload
11344
+ payload,
11345
+ ...seq !== void 0 ? { seq } : {}
10950
11346
  });
10951
11347
  for (const memberId of members) {
10952
11348
  if (memberId === clientId) continue;
@@ -10954,6 +11350,55 @@ var RealtimeService = class RealtimeService extends EventEmitter {
10954
11350
  if (ws && ws.readyState === WebSocket.OPEN) ws.send(message);
10955
11351
  }
10956
11352
  }
11353
+ /**
11354
+ * Install retention rules and create the tables they need.
11355
+ *
11356
+ * Safe to call with no rules (and safe not to call at all): the store stays
11357
+ * inert, no schema is created, and broadcast keeps its original
11358
+ * fire-and-forget path.
11359
+ */
11360
+ async configureChannelHistory(rules) {
11361
+ this.channelHistory = new ChannelHistoryStore(this.db, rules ?? []);
11362
+ if (!this.channelHistory.enabled) return;
11363
+ await this.channelHistory.ensureTables();
11364
+ }
11365
+ /** Whether any channel is configured to retain messages. */
11366
+ isChannelHistoryEnabled() {
11367
+ return this.channelHistory?.enabled ?? false;
11368
+ }
11369
+ /**
11370
+ * Answer a client's catch-up request.
11371
+ *
11372
+ * A channel with no retention rule is answered with `retained: false`
11373
+ * rather than an empty list, so the client can tell "you missed nothing"
11374
+ * apart from "this channel never keeps anything" — the second means its
11375
+ * reconnect strategy has to be a full resync, and silence would leave it
11376
+ * guessing.
11377
+ */
11378
+ async handleChannelHistoryRequest(clientId, channel, sinceSeq, limit) {
11379
+ if (!channel) return;
11380
+ if (!this.channelHistory?.retentionFor(channel)) {
11381
+ this.sendChannelHistory(clientId, channel, [], false);
11382
+ return;
11383
+ }
11384
+ try {
11385
+ const { messages, latestSeq } = await this.channelHistory.replay(channel, sinceSeq, limit);
11386
+ this.sendChannelHistory(clientId, channel, messages, true, latestSeq);
11387
+ } catch (error) {
11388
+ logger.error(`❌ [ChannelHistory] Replay failed for "${channel}"`, { error });
11389
+ this.sendError(clientId, `Could not replay history for channel "${channel}"`, void 0, "CHANNEL_HISTORY_READ_FAILED");
11390
+ }
11391
+ }
11392
+ sendChannelHistory(clientId, channel, messages, retained, latestSeq) {
11393
+ const ws = this.clients.get(clientId);
11394
+ if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
11395
+ type: "channel_history",
11396
+ channel,
11397
+ messages,
11398
+ retained,
11399
+ ...latestSeq !== void 0 ? { latestSeq } : {}
11400
+ }));
11401
+ }
10957
11402
  /** Track presence in a channel */
10958
11403
  trackPresence(clientId, channel, state) {
10959
11404
  if (!this.presence.has(channel)) this.presence.set(channel, /* @__PURE__ */ new Map());
@@ -11034,6 +11479,9 @@ var RealtimeService = class RealtimeService extends EventEmitter {
11034
11479
  this.subscriptionCallbacks.clear();
11035
11480
  this.channels.clear();
11036
11481
  this.presence.clear();
11482
+ await Promise.allSettled([...this.channelSendQueues.values()]);
11483
+ this.channelSendQueues.clear();
11484
+ this.channelHistory?.clear();
11037
11485
  if (this.presenceInterval) {
11038
11486
  clearInterval(this.presenceInterval);
11039
11487
  this.presenceInterval = void 0;
@@ -11375,20 +11823,20 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
11375
11823
  if (authAdapter) try {
11376
11824
  const adapterUser = authAdapter.verifyToken ? await authAdapter.verifyToken(token) : await authAdapter.verifyRequest(new Request("http://localhost/_ws_auth", { headers: { Authorization: `Bearer ${token}` } }));
11377
11825
  if (adapterUser) verifiedUser = {
11378
- userId: adapterUser.uid,
11826
+ uid: adapterUser.uid,
11379
11827
  roles: adapterUser.roles,
11380
11828
  isAdmin: adapterUser.isAdmin
11381
11829
  };
11382
11830
  } catch {}
11383
11831
  else if (authConfig?.serviceKey && safeCompare(token, authConfig.serviceKey)) verifiedUser = {
11384
- userId: "service",
11832
+ uid: "service",
11385
11833
  roles: ["admin"],
11386
11834
  isAdmin: true
11387
11835
  };
11388
11836
  else {
11389
11837
  const jwtPayload = extractUserFromToken(token);
11390
11838
  if (jwtPayload) verifiedUser = {
11391
- userId: jwtPayload.userId,
11839
+ uid: jwtPayload.uid,
11392
11840
  roles: jwtPayload.roles ?? [],
11393
11841
  isAdmin: (jwtPayload.roles ?? []).some((r) => r === "admin")
11394
11842
  };
@@ -11404,11 +11852,11 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
11404
11852
  type: "AUTH_SUCCESS",
11405
11853
  requestId,
11406
11854
  payload: {
11407
- userId: verifiedUser.userId,
11855
+ uid: verifiedUser.uid,
11408
11856
  roles: verifiedUser.roles
11409
11857
  }
11410
11858
  }));
11411
- wsDebug(`🔐 [WebSocket Server] Client ${clientId} authenticated as ${verifiedUser.userId}`);
11859
+ wsDebug(`🔐 [WebSocket Server] Client ${clientId} authenticated as ${verifiedUser.uid}`);
11412
11860
  } else {
11413
11861
  wsDebug(`[WS] replying AUTH_ERROR for requestId ${requestId} (invalid token)`);
11414
11862
  sendError("AUTH_ERROR", "INVALID_TOKEN", "Invalid or expired token");
@@ -11446,7 +11894,7 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
11446
11894
  const session = clientSessions.get(clientId);
11447
11895
  if (typeof driver.withAuth === "function") try {
11448
11896
  const userForAuth = session?.user ? {
11449
- uid: session.user.userId,
11897
+ uid: session.user.uid,
11450
11898
  displayName: null,
11451
11899
  email: null,
11452
11900
  photoURL: null,
@@ -11580,7 +12028,7 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
11580
12028
  sql: typeof sql === "string" ? sql.substring(0, 500) : sql,
11581
12029
  options,
11582
12030
  resultRows: Array.isArray(result) ? result.length : "unknown",
11583
- userId: auditSession?.user?.userId ?? "unknown",
12031
+ uid: auditSession?.user?.uid ?? "unknown",
11584
12032
  roles: auditSession?.user?.roles ?? [],
11585
12033
  isAdmin: auditSession?.user?.isAdmin ?? false
11586
12034
  }));
@@ -11625,6 +12073,21 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
11625
12073
  ws.send(JSON.stringify(response));
11626
12074
  }
11627
12075
  break;
12076
+ case "FETCH_APPLICATION_ROLES":
12077
+ {
12078
+ wsDebug("👤 [WebSocket Server] Processing FETCH_APPLICATION_ROLES request");
12079
+ const admin = (await getScopedDelegate()).admin;
12080
+ let roles = [];
12081
+ if (isSQLAdmin(admin) && admin.fetchApplicationRoles) roles = await admin.fetchApplicationRoles();
12082
+ wsDebug(`👤 [WebSocket Server] Fetched ${roles.length} application roles.`);
12083
+ const response = {
12084
+ type: "FETCH_APPLICATION_ROLES_SUCCESS",
12085
+ payload: { roles },
12086
+ requestId
12087
+ };
12088
+ ws.send(JSON.stringify(response));
12089
+ }
12090
+ break;
11628
12091
  case "FETCH_CURRENT_DATABASE":
11629
12092
  {
11630
12093
  wsDebug("📚 [WebSocket Server] Processing FETCH_CURRENT_DATABASE request");
@@ -11731,14 +12194,15 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
11731
12194
  case "broadcast":
11732
12195
  case "presence_track":
11733
12196
  case "presence_untrack":
11734
- case "presence_state": {
12197
+ case "presence_state":
12198
+ case "channel_history": {
11735
12199
  wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
11736
12200
  const session = clientSessions.get(clientId);
11737
12201
  const authContext = session?.user ? {
11738
- userId: session.user.userId,
12202
+ uid: session.user.uid,
11739
12203
  roles: session.user.roles ?? []
11740
12204
  } : {
11741
- userId: "anon",
12205
+ uid: "anon",
11742
12206
  roles: ["anon"]
11743
12207
  };
11744
12208
  await realtimeService.handleClientMessage(clientId, {
@@ -20694,9 +21158,65 @@ async function ensureAuthTablesExist(db, collection) {
20694
21158
  )
20695
21159
  `);
20696
21160
  await db.execute(sql`
21161
+ CREATE OR REPLACE FUNCTION ${sql.raw(`"${authSchema}"`)}.sync_uid_user_id() RETURNS trigger AS $$
21162
+ BEGIN
21163
+ IF NEW.uid IS NULL AND NEW.user_id IS NOT NULL THEN
21164
+ NEW.uid := NEW.user_id;
21165
+ ELSIF NEW.user_id IS NULL AND NEW.uid IS NOT NULL THEN
21166
+ NEW.user_id := NEW.uid;
21167
+ END IF;
21168
+ RETURN NEW;
21169
+ END $$ LANGUAGE plpgsql
21170
+ `);
21171
+ for (const authTable of [
21172
+ "user_identities",
21173
+ "refresh_tokens",
21174
+ "password_reset_tokens",
21175
+ "magic_link_tokens",
21176
+ "mfa_factors",
21177
+ "recovery_codes"
21178
+ ]) {
21179
+ const qualified = `"${authSchema}"."${authTable}"`;
21180
+ await db.execute(sql`
21181
+ DO $$
21182
+ DECLARE
21183
+ has_legacy boolean;
21184
+ has_uid boolean;
21185
+ BEGIN
21186
+ SELECT
21187
+ bool_or(column_name = 'user_id'),
21188
+ bool_or(column_name = 'uid')
21189
+ INTO has_legacy, has_uid
21190
+ FROM information_schema.columns
21191
+ WHERE table_schema = ${sql.raw(`'${authSchema}'`)}
21192
+ AND table_name = ${sql.raw(`'${authTable}'`)};
21193
+
21194
+ -- Table absent, or already uid-only (a fresh install, or
21195
+ -- phase 2 already run): nothing to do.
21196
+ IF has_legacy IS NOT TRUE THEN
21197
+ RETURN;
21198
+ END IF;
21199
+
21200
+ IF has_uid IS NOT TRUE THEN
21201
+ EXECUTE ${sql.raw(`'ALTER TABLE ${qualified} ADD COLUMN uid ${userIdType} REFERENCES ${usersTableName}(id) ON DELETE CASCADE'`)};
21202
+ EXECUTE ${sql.raw(`'UPDATE ${qualified} SET uid = user_id WHERE uid IS NULL'`)};
21203
+ EXECUTE ${sql.raw(`'CREATE INDEX IF NOT EXISTS idx_${authTable}_uid ON ${qualified}(uid)'`)};
21204
+ END IF;
21205
+
21206
+ -- New code inserts uid and never user_id, so the legacy
21207
+ -- column can no longer be NOT NULL. The trigger below
21208
+ -- backfills it, but the constraint is checked first.
21209
+ EXECUTE ${sql.raw(`'ALTER TABLE ${qualified} ALTER COLUMN user_id DROP NOT NULL'`)};
21210
+
21211
+ EXECUTE ${sql.raw(`'DROP TRIGGER IF EXISTS sync_uid_user_id ON ${qualified}'`)};
21212
+ EXECUTE ${sql.raw(`'CREATE TRIGGER sync_uid_user_id BEFORE INSERT OR UPDATE ON ${qualified} FOR EACH ROW EXECUTE FUNCTION "${authSchema}".sync_uid_user_id()'`)};
21213
+ END $$
21214
+ `);
21215
+ }
21216
+ await db.execute(sql`
20697
21217
  CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (
20698
21218
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
20699
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
21219
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
20700
21220
  provider TEXT NOT NULL,
20701
21221
  provider_id TEXT NOT NULL,
20702
21222
  profile_data JSONB,
@@ -20707,18 +21227,18 @@ async function ensureAuthTablesExist(db, collection) {
20707
21227
  `);
20708
21228
  await db.execute(sql`
20709
21229
  CREATE INDEX IF NOT EXISTS idx_user_identities_user
20710
- ON ${sql.raw(userIdentitiesTable)}(user_id)
21230
+ ON ${sql.raw(userIdentitiesTable)}(uid)
20711
21231
  `);
20712
21232
  await db.execute(sql`
20713
21233
  CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
20714
21234
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
20715
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
21235
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
20716
21236
  token_hash TEXT NOT NULL UNIQUE,
20717
21237
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
20718
21238
  user_agent TEXT,
20719
21239
  ip_address TEXT,
20720
21240
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
20721
- CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
21241
+ CONSTRAINT unique_device_session UNIQUE (uid, user_agent, ip_address)
20722
21242
  )
20723
21243
  `);
20724
21244
  await db.execute(sql`
@@ -20727,12 +21247,12 @@ async function ensureAuthTablesExist(db, collection) {
20727
21247
  `);
20728
21248
  await db.execute(sql`
20729
21249
  CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
20730
- ON ${sql.raw(refreshTokensTableName)}(user_id)
21250
+ ON ${sql.raw(refreshTokensTableName)}(uid)
20731
21251
  `);
20732
21252
  await db.execute(sql`
20733
21253
  CREATE TABLE IF NOT EXISTS ${sql.raw(passwordResetTokensTableName)} (
20734
21254
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
20735
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
21255
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
20736
21256
  token_hash TEXT NOT NULL UNIQUE,
20737
21257
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
20738
21258
  used_at TIMESTAMP WITH TIME ZONE,
@@ -20745,13 +21265,13 @@ async function ensureAuthTablesExist(db, collection) {
20745
21265
  `);
20746
21266
  await db.execute(sql`
20747
21267
  CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
20748
- ON ${sql.raw(passwordResetTokensTableName)}(user_id)
21268
+ ON ${sql.raw(passwordResetTokensTableName)}(uid)
20749
21269
  `);
20750
21270
  const magicLinkTokensTableName = `"${authSchema}"."magic_link_tokens"`;
20751
21271
  await db.execute(sql`
20752
21272
  CREATE TABLE IF NOT EXISTS ${sql.raw(magicLinkTokensTableName)} (
20753
21273
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
20754
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
21274
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
20755
21275
  token_hash TEXT NOT NULL UNIQUE,
20756
21276
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
20757
21277
  used_at TIMESTAMP WITH TIME ZONE,
@@ -20764,7 +21284,7 @@ async function ensureAuthTablesExist(db, collection) {
20764
21284
  `);
20765
21285
  await db.execute(sql`
20766
21286
  CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_user
20767
- ON ${sql.raw(magicLinkTokensTableName)}(user_id)
21287
+ ON ${sql.raw(magicLinkTokensTableName)}(uid)
20768
21288
  `);
20769
21289
  await db.execute(sql`
20770
21290
  CREATE TABLE IF NOT EXISTS ${sql.raw(appConfigTableName)} (
@@ -20778,7 +21298,10 @@ async function ensureAuthTablesExist(db, collection) {
20778
21298
  await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
20779
21299
  await tx.execute(sql`
20780
21300
  CREATE OR REPLACE FUNCTION auth.uid() RETURNS text AS $$
20781
- SELECT NULLIF(current_setting('app.user_id', true), '');
21301
+ SELECT COALESCE(
21302
+ NULLIF(current_setting('app.uid', true), ''),
21303
+ NULLIF(current_setting('app.user_id', true), '')
21304
+ );
20782
21305
  $$ LANGUAGE sql STABLE
20783
21306
  `);
20784
21307
  await tx.execute(sql`
@@ -20841,7 +21364,7 @@ async function ensureAuthTablesExist(db, collection) {
20841
21364
  await db.execute(sql`
20842
21365
  CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
20843
21366
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
20844
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
21367
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
20845
21368
  factor_type TEXT NOT NULL DEFAULT 'totp',
20846
21369
  secret_encrypted TEXT NOT NULL,
20847
21370
  friendly_name TEXT,
@@ -20852,7 +21375,7 @@ async function ensureAuthTablesExist(db, collection) {
20852
21375
  `);
20853
21376
  await db.execute(sql`
20854
21377
  CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
20855
- ON ${sql.raw(mfaFactorsTableName)}(user_id)
21378
+ ON ${sql.raw(mfaFactorsTableName)}(uid)
20856
21379
  `);
20857
21380
  await db.execute(sql`
20858
21381
  CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
@@ -20871,7 +21394,7 @@ async function ensureAuthTablesExist(db, collection) {
20871
21394
  await db.execute(sql`
20872
21395
  CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
20873
21396
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
20874
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
21397
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
20875
21398
  code_hash TEXT NOT NULL,
20876
21399
  used_at TIMESTAMP WITH TIME ZONE,
20877
21400
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
@@ -20879,7 +21402,7 @@ async function ensureAuthTablesExist(db, collection) {
20879
21402
  `);
20880
21403
  await db.execute(sql`
20881
21404
  CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
20882
- ON ${sql.raw(recoveryCodesTableName)}(user_id)
21405
+ ON ${sql.raw(recoveryCodesTableName)}(uid)
20883
21406
  `);
20884
21407
  try {
20885
21408
  const authTablePairs = [
@@ -20960,7 +21483,7 @@ var UserService = class {
20960
21483
  * Run a privileged auth write with an explicitly cleared RLS context.
20961
21484
  *
20962
21485
  * The auth services run on the base/owner connection, which by design
20963
- * carries a NULL `app.user_id` so the `auth.uid() IS NULL` server-escape
21486
+ * carries a NULL `app.uid` so the `auth.uid() IS NULL` server-escape
20964
21487
  * in the default policies applies. That NULL is normally guaranteed by
20965
21488
  * `set_config(..., is_local = true)` resetting at transaction end — but a
20966
21489
  * GUC that survives on a pooled connection (or a connection role that
@@ -20974,7 +21497,8 @@ var UserService = class {
20974
21497
  async withServerContext(fn) {
20975
21498
  return await this.db.transaction(async (tx) => {
20976
21499
  await tx.execute(sql`
20977
- SELECT set_config('app.user_id', '', true),
21500
+ SELECT set_config('app.uid', '', true),
21501
+ set_config('app.user_id', '', true),
20978
21502
  set_config('app.user_roles', '', true),
20979
21503
  set_config('app.jwt', '', true)
20980
21504
  `);
@@ -21096,19 +21620,19 @@ var UserService = class {
21096
21620
  async getUserByIdentity(provider, providerId) {
21097
21621
  const userIdCol = getColumn(this.usersTable, "id");
21098
21622
  if (!userIdCol) return null;
21099
- const result = await this.db.select({ user: this.usersTable }).from(this.usersTable).innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.userId)).where(sql`${this.userIdentitiesTable.provider} = ${provider} AND ${this.userIdentitiesTable.providerId} = ${providerId}`).limit(1);
21623
+ const result = await this.db.select({ user: this.usersTable }).from(this.usersTable).innerJoin(this.userIdentitiesTable, eq(userIdCol, this.userIdentitiesTable.uid)).where(sql`${this.userIdentitiesTable.provider} = ${provider} AND ${this.userIdentitiesTable.providerId} = ${providerId}`).limit(1);
21100
21624
  if (result.length === 0) return null;
21101
21625
  return this.mapRowToUser(result[0].user);
21102
21626
  }
21103
- async getUserIdentities(userId) {
21627
+ async getUserIdentities(uid) {
21104
21628
  const schema = getTableConfig(this.userIdentitiesTable).schema || "public";
21105
21629
  return (await this.db.execute(sql`
21106
- SELECT id, user_id, provider, provider_id, profile_data, created_at, updated_at
21630
+ SELECT id, uid, provider, provider_id, profile_data, created_at, updated_at
21107
21631
  FROM ${sql.raw(`"${schema}"."user_identities"`)}
21108
- WHERE user_id = ${userId}
21632
+ WHERE uid = ${uid}
21109
21633
  `)).rows.map((row) => ({
21110
21634
  id: row.id,
21111
- userId: row.user_id,
21635
+ uid: row.uid,
21112
21636
  provider: row.provider,
21113
21637
  providerId: row.provider_id,
21114
21638
  profileData: row.profile_data ?? null,
@@ -21116,9 +21640,9 @@ var UserService = class {
21116
21640
  updatedAt: row.updated_at
21117
21641
  }));
21118
21642
  }
21119
- async linkUserIdentity(userId, provider, providerId, profileData) {
21643
+ async linkUserIdentity(uid, provider, providerId, profileData) {
21120
21644
  await this.withServerContext(async (db) => db.insert(this.userIdentitiesTable).values({
21121
- userId,
21645
+ uid,
21122
21646
  provider,
21123
21647
  providerId,
21124
21648
  profileData: profileData || null
@@ -21237,10 +21761,10 @@ var UserService = class {
21237
21761
  /**
21238
21762
  * Get roles for a user from database (inline TEXT[] column)
21239
21763
  */
21240
- async getUserRoles(userId) {
21764
+ async getUserRoles(uid) {
21241
21765
  const usersTableName = this.getQualifiedUsersTableName();
21242
21766
  const result = await this.db.execute(sql`
21243
- SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
21767
+ SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}
21244
21768
  `);
21245
21769
  if (result.rows.length === 0) return [];
21246
21770
  return (result.rows[0].roles ?? []).map((id) => ({
@@ -21254,10 +21778,10 @@ var UserService = class {
21254
21778
  /**
21255
21779
  * Get role IDs for a user
21256
21780
  */
21257
- async getUserRoleIds(userId) {
21781
+ async getUserRoleIds(uid) {
21258
21782
  const usersTableName = this.getQualifiedUsersTableName();
21259
21783
  const result = await this.db.execute(sql`
21260
- SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${userId}
21784
+ SELECT roles FROM ${sql.raw(usersTableName)} WHERE id = ${uid}
21261
21785
  `);
21262
21786
  if (result.rows.length === 0) return [];
21263
21787
  return result.rows[0].roles ?? [];
@@ -21265,35 +21789,35 @@ var UserService = class {
21265
21789
  /**
21266
21790
  * Set roles for a user (replaces existing roles)
21267
21791
  */
21268
- async setUserRoles(userId, roleIds) {
21792
+ async setUserRoles(uid, roleIds) {
21269
21793
  const usersTableName = this.getQualifiedUsersTableName();
21270
21794
  const rolesArray = `{${roleIds.join(",")}}`;
21271
21795
  await this.withServerContext(async (db) => db.execute(sql`
21272
21796
  UPDATE ${sql.raw(usersTableName)}
21273
21797
  SET roles = ${rolesArray}::text[], updated_at = NOW()
21274
- WHERE id = ${userId}
21798
+ WHERE id = ${uid}
21275
21799
  `));
21276
21800
  }
21277
21801
  /**
21278
21802
  * Assign a specific role to new user (appends if not present)
21279
21803
  */
21280
- async assignDefaultRole(userId, roleId) {
21804
+ async assignDefaultRole(uid, roleId) {
21281
21805
  const usersTableName = this.getQualifiedUsersTableName();
21282
21806
  await this.withServerContext(async (db) => db.execute(sql`
21283
21807
  UPDATE ${sql.raw(usersTableName)}
21284
21808
  SET roles = array_append(roles, ${roleId}), updated_at = NOW()
21285
- WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
21809
+ WHERE id = ${uid} AND NOT (${roleId} = ANY(roles))
21286
21810
  `));
21287
21811
  }
21288
21812
  /**
21289
21813
  * Get user with their roles
21290
21814
  */
21291
- async getUserWithRoles(userId) {
21292
- const user = await this.getUserById(userId);
21815
+ async getUserWithRoles(uid) {
21816
+ const user = await this.getUserById(uid);
21293
21817
  if (!user) return null;
21294
21818
  return {
21295
21819
  user,
21296
- roles: await this.getUserRoles(userId)
21820
+ roles: await this.getUserRoles(uid)
21297
21821
  };
21298
21822
  }
21299
21823
  };
@@ -21305,18 +21829,18 @@ var RefreshTokenService = class {
21305
21829
  if (tableOrTables && (tableOrTables.refreshTokens || tableOrTables.users)) this.refreshTokensTable = tableOrTables.refreshTokens || refreshTokens;
21306
21830
  else this.refreshTokensTable = tableOrTables || refreshTokens;
21307
21831
  }
21308
- async createToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
21832
+ async createToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
21309
21833
  const safeUserAgent = userAgent || "";
21310
21834
  const safeIpAddress = ipAddress || "";
21311
21835
  await this.db.insert(this.refreshTokensTable).values({
21312
- userId,
21836
+ uid,
21313
21837
  tokenHash,
21314
21838
  expiresAt,
21315
21839
  userAgent: safeUserAgent,
21316
21840
  ipAddress: safeIpAddress
21317
21841
  }).onConflictDoUpdate({
21318
21842
  target: [
21319
- this.refreshTokensTable.userId,
21843
+ this.refreshTokensTable.uid,
21320
21844
  this.refreshTokensTable.userAgent,
21321
21845
  this.refreshTokensTable.ipAddress
21322
21846
  ],
@@ -21329,7 +21853,7 @@ var RefreshTokenService = class {
21329
21853
  async findByHash(tokenHash) {
21330
21854
  const [token] = await this.db.select({
21331
21855
  id: this.refreshTokensTable.id,
21332
- userId: this.refreshTokensTable.userId,
21856
+ uid: this.refreshTokensTable.uid,
21333
21857
  tokenHash: this.refreshTokensTable.tokenHash,
21334
21858
  expiresAt: this.refreshTokensTable.expiresAt,
21335
21859
  createdAt: this.refreshTokensTable.createdAt,
@@ -21341,22 +21865,22 @@ var RefreshTokenService = class {
21341
21865
  async deleteByHash(tokenHash) {
21342
21866
  await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.tokenHash, tokenHash));
21343
21867
  }
21344
- async deleteAllForUser(userId) {
21345
- await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.userId, userId));
21868
+ async deleteAllForUser(uid) {
21869
+ await this.db.delete(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid));
21346
21870
  }
21347
- async listForUser(userId) {
21871
+ async listForUser(uid) {
21348
21872
  return await this.db.select({
21349
21873
  id: this.refreshTokensTable.id,
21350
- userId: this.refreshTokensTable.userId,
21874
+ uid: this.refreshTokensTable.uid,
21351
21875
  tokenHash: this.refreshTokensTable.tokenHash,
21352
21876
  expiresAt: this.refreshTokensTable.expiresAt,
21353
21877
  createdAt: this.refreshTokensTable.createdAt,
21354
21878
  userAgent: this.refreshTokensTable.userAgent,
21355
21879
  ipAddress: this.refreshTokensTable.ipAddress
21356
- }).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.userId, userId)).orderBy(this.refreshTokensTable.createdAt);
21880
+ }).from(this.refreshTokensTable).where(eq(this.refreshTokensTable.uid, uid)).orderBy(this.refreshTokensTable.createdAt);
21357
21881
  }
21358
- async deleteById(id, userId) {
21359
- await this.db.delete(this.refreshTokensTable).where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.userId} = ${userId}`);
21882
+ async deleteById(id, uid) {
21883
+ await this.db.delete(this.refreshTokensTable).where(sql`${this.refreshTokensTable.id} = ${id} AND ${this.refreshTokensTable.uid} = ${uid}`);
21360
21884
  }
21361
21885
  };
21362
21886
  /**
@@ -21377,14 +21901,14 @@ var PasswordResetTokenService = class {
21377
21901
  /**
21378
21902
  * Create a password reset token
21379
21903
  */
21380
- async createToken(userId, tokenHash, expiresAt) {
21904
+ async createToken(uid, tokenHash, expiresAt) {
21381
21905
  const tableName = this.getQualifiedPasswordResetTokensTableName();
21382
21906
  await this.db.execute(sql`
21383
21907
  DELETE FROM ${sql.raw(tableName)}
21384
- WHERE user_id = ${userId} AND used_at IS NULL
21908
+ WHERE uid = ${uid} AND used_at IS NULL
21385
21909
  `);
21386
21910
  await this.db.insert(this.passwordResetTokensTable).values({
21387
- userId,
21911
+ uid,
21388
21912
  tokenHash,
21389
21913
  expiresAt
21390
21914
  });
@@ -21394,13 +21918,13 @@ var PasswordResetTokenService = class {
21394
21918
  */
21395
21919
  async findValidByHash(tokenHash) {
21396
21920
  const [token] = await this.db.select({
21397
- userId: this.passwordResetTokensTable.userId,
21921
+ uid: this.passwordResetTokensTable.uid,
21398
21922
  expiresAt: this.passwordResetTokensTable.expiresAt
21399
21923
  }).from(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.tokenHash, tokenHash));
21400
21924
  if (!token) return null;
21401
21925
  const tableName = this.getQualifiedPasswordResetTokensTableName();
21402
21926
  const result = await this.db.execute(sql`
21403
- SELECT user_id, expires_at
21927
+ SELECT uid, expires_at
21404
21928
  FROM ${sql.raw(tableName)}
21405
21929
  WHERE token_hash = ${tokenHash}
21406
21930
  AND used_at IS NULL
@@ -21409,7 +21933,7 @@ var PasswordResetTokenService = class {
21409
21933
  if (result.rows.length === 0) return null;
21410
21934
  const row = result.rows[0];
21411
21935
  return {
21412
- userId: row.user_id,
21936
+ uid: row.uid,
21413
21937
  expiresAt: new Date(row.expires_at)
21414
21938
  };
21415
21939
  }
@@ -21422,8 +21946,8 @@ var PasswordResetTokenService = class {
21422
21946
  /**
21423
21947
  * Delete all tokens for a user
21424
21948
  */
21425
- async deleteAllForUser(userId) {
21426
- await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.userId, userId));
21949
+ async deleteAllForUser(uid) {
21950
+ await this.db.delete(this.passwordResetTokensTable).where(eq(this.passwordResetTokensTable.uid, uid));
21427
21951
  }
21428
21952
  /**
21429
21953
  * Clean up expired tokens
@@ -21451,14 +21975,14 @@ var MagicLinkTokenService = class {
21451
21975
  const name = getTableName(this.magicLinkTokensTable);
21452
21976
  return `"${getTableConfig(this.magicLinkTokensTable).schema || "public"}"."${name}"`;
21453
21977
  }
21454
- async createToken(userId, tokenHash, expiresAt) {
21978
+ async createToken(uid, tokenHash, expiresAt) {
21455
21979
  const tableName = this.getQualifiedTableName();
21456
21980
  await this.db.execute(sql`
21457
21981
  DELETE FROM ${sql.raw(tableName)}
21458
- WHERE user_id = ${userId} AND used_at IS NULL
21982
+ WHERE uid = ${uid} AND used_at IS NULL
21459
21983
  `);
21460
21984
  await this.db.insert(this.magicLinkTokensTable).values({
21461
- userId,
21985
+ uid,
21462
21986
  tokenHash,
21463
21987
  expiresAt
21464
21988
  });
@@ -21466,7 +21990,7 @@ var MagicLinkTokenService = class {
21466
21990
  async findValidByHash(tokenHash) {
21467
21991
  const tableName = this.getQualifiedTableName();
21468
21992
  const result = await this.db.execute(sql`
21469
- SELECT user_id, expires_at
21993
+ SELECT uid, expires_at
21470
21994
  FROM ${sql.raw(tableName)}
21471
21995
  WHERE token_hash = ${tokenHash}
21472
21996
  AND used_at IS NULL
@@ -21475,7 +21999,7 @@ var MagicLinkTokenService = class {
21475
21999
  if (result.rows.length === 0) return null;
21476
22000
  const row = result.rows[0];
21477
22001
  return {
21478
- userId: row.user_id,
22002
+ uid: row.uid,
21479
22003
  expiresAt: new Date(row.expires_at)
21480
22004
  };
21481
22005
  }
@@ -21498,8 +22022,8 @@ var PostgresTokenRepository = class {
21498
22022
  this.passwordResetTokenService = new PasswordResetTokenService(db, tableOrTables);
21499
22023
  this.magicLinkTokenService = new MagicLinkTokenService(db, tableOrTables);
21500
22024
  }
21501
- async createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
21502
- await this.refreshTokenService.createToken(userId, tokenHash, expiresAt, userAgent, ipAddress);
22025
+ async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
22026
+ await this.refreshTokenService.createToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
21503
22027
  }
21504
22028
  async findRefreshTokenByHash(tokenHash) {
21505
22029
  return this.refreshTokenService.findByHash(tokenHash);
@@ -21507,17 +22031,17 @@ var PostgresTokenRepository = class {
21507
22031
  async deleteRefreshToken(tokenHash) {
21508
22032
  await this.refreshTokenService.deleteByHash(tokenHash);
21509
22033
  }
21510
- async deleteAllRefreshTokensForUser(userId) {
21511
- await this.refreshTokenService.deleteAllForUser(userId);
22034
+ async deleteAllRefreshTokensForUser(uid) {
22035
+ await this.refreshTokenService.deleteAllForUser(uid);
21512
22036
  }
21513
- async listRefreshTokensForUser(userId) {
21514
- return this.refreshTokenService.listForUser(userId);
22037
+ async listRefreshTokensForUser(uid) {
22038
+ return this.refreshTokenService.listForUser(uid);
21515
22039
  }
21516
- async deleteRefreshTokenById(id, userId) {
21517
- await this.refreshTokenService.deleteById(id, userId);
22040
+ async deleteRefreshTokenById(id, uid) {
22041
+ await this.refreshTokenService.deleteById(id, uid);
21518
22042
  }
21519
- async createPasswordResetToken(userId, tokenHash, expiresAt) {
21520
- await this.passwordResetTokenService.createToken(userId, tokenHash, expiresAt);
22043
+ async createPasswordResetToken(uid, tokenHash, expiresAt) {
22044
+ await this.passwordResetTokenService.createToken(uid, tokenHash, expiresAt);
21521
22045
  }
21522
22046
  async findValidPasswordResetToken(tokenHash) {
21523
22047
  return this.passwordResetTokenService.findValidByHash(tokenHash);
@@ -21525,14 +22049,14 @@ var PostgresTokenRepository = class {
21525
22049
  async markPasswordResetTokenUsed(tokenHash) {
21526
22050
  await this.passwordResetTokenService.markAsUsed(tokenHash);
21527
22051
  }
21528
- async deleteAllPasswordResetTokensForUser(userId) {
21529
- await this.passwordResetTokenService.deleteAllForUser(userId);
22052
+ async deleteAllPasswordResetTokensForUser(uid) {
22053
+ await this.passwordResetTokenService.deleteAllForUser(uid);
21530
22054
  }
21531
22055
  async deleteExpiredTokens() {
21532
22056
  await this.passwordResetTokenService.deleteExpired();
21533
22057
  }
21534
- async createMagicLinkToken(userId, tokenHash, expiresAt) {
21535
- await this.magicLinkTokenService.createToken(userId, tokenHash, expiresAt);
22058
+ async createMagicLinkToken(uid, tokenHash, expiresAt) {
22059
+ await this.magicLinkTokenService.createToken(uid, tokenHash, expiresAt);
21536
22060
  }
21537
22061
  async findValidMagicLinkToken(tokenHash) {
21538
22062
  return this.magicLinkTokenService.findValidByHash(tokenHash);
@@ -21567,11 +22091,11 @@ var PostgresAuthRepository = class {
21567
22091
  async getUserByIdentity(provider, providerId) {
21568
22092
  return this.userService.getUserByIdentity(provider, providerId);
21569
22093
  }
21570
- async getUserIdentities(userId) {
21571
- return this.userService.getUserIdentities(userId);
22094
+ async getUserIdentities(uid) {
22095
+ return this.userService.getUserIdentities(uid);
21572
22096
  }
21573
- async linkUserIdentity(userId, provider, providerId, profileData) {
21574
- return this.userService.linkUserIdentity(userId, provider, providerId, profileData);
22097
+ async linkUserIdentity(uid, provider, providerId, profileData) {
22098
+ return this.userService.linkUserIdentity(uid, provider, providerId, profileData);
21575
22099
  }
21576
22100
  async updateUser(id, data) {
21577
22101
  return this.userService.updateUser(id, data);
@@ -21597,20 +22121,20 @@ var PostgresAuthRepository = class {
21597
22121
  async getUserByVerificationToken(token) {
21598
22122
  return this.userService.getUserByVerificationToken(token);
21599
22123
  }
21600
- async getUserRoles(userId) {
21601
- return this.userService.getUserRoles(userId);
22124
+ async getUserRoles(uid) {
22125
+ return this.userService.getUserRoles(uid);
21602
22126
  }
21603
- async getUserRoleIds(userId) {
21604
- return this.userService.getUserRoleIds(userId);
22127
+ async getUserRoleIds(uid) {
22128
+ return this.userService.getUserRoleIds(uid);
21605
22129
  }
21606
- async setUserRoles(userId, roleIds) {
21607
- await this.userService.setUserRoles(userId, roleIds);
22130
+ async setUserRoles(uid, roleIds) {
22131
+ await this.userService.setUserRoles(uid, roleIds);
21608
22132
  }
21609
- async assignDefaultRole(userId, roleId) {
21610
- await this.userService.assignDefaultRole(userId, roleId);
22133
+ async assignDefaultRole(uid, roleId) {
22134
+ await this.userService.assignDefaultRole(uid, roleId);
21611
22135
  }
21612
- async getUserWithRoles(userId) {
21613
- return this.userService.getUserWithRoles(userId);
22136
+ async getUserWithRoles(uid) {
22137
+ return this.userService.getUserWithRoles(uid);
21614
22138
  }
21615
22139
  async getRoleById(id) {
21616
22140
  return {
@@ -21665,8 +22189,8 @@ var PostgresAuthRepository = class {
21665
22189
  };
21666
22190
  }
21667
22191
  async deleteRole(_id) {}
21668
- async createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress) {
21669
- await this.tokenRepository.createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress);
22192
+ async createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress) {
22193
+ await this.tokenRepository.createRefreshToken(uid, tokenHash, expiresAt, userAgent, ipAddress);
21670
22194
  }
21671
22195
  async findRefreshTokenByHash(tokenHash) {
21672
22196
  return this.tokenRepository.findRefreshTokenByHash(tokenHash);
@@ -21674,17 +22198,17 @@ var PostgresAuthRepository = class {
21674
22198
  async deleteRefreshToken(tokenHash) {
21675
22199
  await this.tokenRepository.deleteRefreshToken(tokenHash);
21676
22200
  }
21677
- async deleteAllRefreshTokensForUser(userId) {
21678
- await this.tokenRepository.deleteAllRefreshTokensForUser(userId);
22201
+ async deleteAllRefreshTokensForUser(uid) {
22202
+ await this.tokenRepository.deleteAllRefreshTokensForUser(uid);
21679
22203
  }
21680
- async listRefreshTokensForUser(userId) {
21681
- return this.tokenRepository.listRefreshTokensForUser(userId);
22204
+ async listRefreshTokensForUser(uid) {
22205
+ return this.tokenRepository.listRefreshTokensForUser(uid);
21682
22206
  }
21683
- async deleteRefreshTokenById(id, userId) {
21684
- await this.tokenRepository.deleteRefreshTokenById(id, userId);
22207
+ async deleteRefreshTokenById(id, uid) {
22208
+ await this.tokenRepository.deleteRefreshTokenById(id, uid);
21685
22209
  }
21686
- async createPasswordResetToken(userId, tokenHash, expiresAt) {
21687
- await this.tokenRepository.createPasswordResetToken(userId, tokenHash, expiresAt);
22210
+ async createPasswordResetToken(uid, tokenHash, expiresAt) {
22211
+ await this.tokenRepository.createPasswordResetToken(uid, tokenHash, expiresAt);
21688
22212
  }
21689
22213
  async findValidPasswordResetToken(tokenHash) {
21690
22214
  return this.tokenRepository.findValidPasswordResetToken(tokenHash);
@@ -21692,14 +22216,14 @@ var PostgresAuthRepository = class {
21692
22216
  async markPasswordResetTokenUsed(tokenHash) {
21693
22217
  await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);
21694
22218
  }
21695
- async deleteAllPasswordResetTokensForUser(userId) {
21696
- await this.tokenRepository.deleteAllPasswordResetTokensForUser(userId);
22219
+ async deleteAllPasswordResetTokensForUser(uid) {
22220
+ await this.tokenRepository.deleteAllPasswordResetTokensForUser(uid);
21697
22221
  }
21698
22222
  async deleteExpiredTokens() {
21699
22223
  await this.tokenRepository.deleteExpiredTokens();
21700
22224
  }
21701
- async createMagicLinkToken(userId, tokenHash, expiresAt) {
21702
- await this.tokenRepository.createMagicLinkToken(userId, tokenHash, expiresAt);
22225
+ async createMagicLinkToken(uid, tokenHash, expiresAt) {
22226
+ await this.tokenRepository.createMagicLinkToken(uid, tokenHash, expiresAt);
21703
22227
  }
21704
22228
  async findValidMagicLinkToken(tokenHash) {
21705
22229
  return this.tokenRepository.findValidMagicLinkToken(tokenHash);
@@ -21712,11 +22236,11 @@ var PostgresAuthRepository = class {
21712
22236
  if (!this._mfaService) this._mfaService = new MfaService(this.db);
21713
22237
  return this._mfaService;
21714
22238
  }
21715
- async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
21716
- return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
22239
+ async createMfaFactor(uid, factorType, secretEncrypted, friendlyName) {
22240
+ return this.getMfaService().createMfaFactor(uid, factorType, secretEncrypted, friendlyName);
21717
22241
  }
21718
- async getMfaFactors(userId) {
21719
- return this.getMfaService().getMfaFactors(userId);
22242
+ async getMfaFactors(uid) {
22243
+ return this.getMfaService().getMfaFactors(uid);
21720
22244
  }
21721
22245
  async getMfaFactorById(factorId) {
21722
22246
  return this.getMfaService().getMfaFactorById(factorId);
@@ -21724,8 +22248,8 @@ var PostgresAuthRepository = class {
21724
22248
  async verifyMfaFactor(factorId) {
21725
22249
  return this.getMfaService().verifyMfaFactor(factorId);
21726
22250
  }
21727
- async deleteMfaFactor(factorId, userId) {
21728
- return this.getMfaService().deleteMfaFactor(factorId, userId);
22251
+ async deleteMfaFactor(factorId, uid) {
22252
+ return this.getMfaService().deleteMfaFactor(factorId, uid);
21729
22253
  }
21730
22254
  async createMfaChallenge(factorId, ipAddress) {
21731
22255
  return this.getMfaService().createMfaChallenge(factorId, ipAddress);
@@ -21736,20 +22260,20 @@ var PostgresAuthRepository = class {
21736
22260
  async verifyMfaChallenge(challengeId) {
21737
22261
  return this.getMfaService().verifyMfaChallenge(challengeId);
21738
22262
  }
21739
- async createRecoveryCodes(userId, codeHashes) {
21740
- return this.getMfaService().createRecoveryCodes(userId, codeHashes);
22263
+ async createRecoveryCodes(uid, codeHashes) {
22264
+ return this.getMfaService().createRecoveryCodes(uid, codeHashes);
21741
22265
  }
21742
- async useRecoveryCode(userId, codeHash) {
21743
- return this.getMfaService().useRecoveryCode(userId, codeHash);
22266
+ async useRecoveryCode(uid, codeHash) {
22267
+ return this.getMfaService().useRecoveryCode(uid, codeHash);
21744
22268
  }
21745
- async getUnusedRecoveryCodeCount(userId) {
21746
- return this.getMfaService().getUnusedRecoveryCodeCount(userId);
22269
+ async getUnusedRecoveryCodeCount(uid) {
22270
+ return this.getMfaService().getUnusedRecoveryCodeCount(uid);
21747
22271
  }
21748
- async deleteAllRecoveryCodes(userId) {
21749
- return this.getMfaService().deleteAllRecoveryCodes(userId);
22272
+ async deleteAllRecoveryCodes(uid) {
22273
+ return this.getMfaService().deleteAllRecoveryCodes(uid);
21750
22274
  }
21751
- async hasVerifiedMfaFactors(userId) {
21752
- return this.getMfaService().hasVerifiedMfaFactors(userId);
22275
+ async hasVerifiedMfaFactors(uid) {
22276
+ return this.getMfaService().hasVerifiedMfaFactors(uid);
21753
22277
  }
21754
22278
  };
21755
22279
  /**
@@ -21766,16 +22290,16 @@ var MfaService = class {
21766
22290
  qualify(tableName) {
21767
22291
  return `"${this.schemaName}"."${tableName}"`;
21768
22292
  }
21769
- async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
22293
+ async createMfaFactor(uid, factorType, secretEncrypted, friendlyName) {
21770
22294
  const tableName = this.qualify("mfa_factors");
21771
22295
  const row = (await this.db.execute(sql`
21772
- INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
21773
- VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
21774
- RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
22296
+ INSERT INTO ${sql.raw(tableName)} (uid, factor_type, secret_encrypted, friendly_name)
22297
+ VALUES (${uid}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
22298
+ RETURNING id, uid, factor_type, friendly_name, verified, created_at, updated_at
21775
22299
  `)).rows[0];
21776
22300
  return {
21777
22301
  id: row.id,
21778
- userId: row.user_id,
22302
+ uid: row.uid,
21779
22303
  factorType: row.factor_type,
21780
22304
  friendlyName: row.friendly_name ?? void 0,
21781
22305
  verified: row.verified,
@@ -21783,16 +22307,16 @@ var MfaService = class {
21783
22307
  updatedAt: new Date(row.updated_at)
21784
22308
  };
21785
22309
  }
21786
- async getMfaFactors(userId) {
22310
+ async getMfaFactors(uid) {
21787
22311
  const tableName = this.qualify("mfa_factors");
21788
22312
  return (await this.db.execute(sql`
21789
- SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
22313
+ SELECT id, uid, factor_type, friendly_name, verified, created_at, updated_at
21790
22314
  FROM ${sql.raw(tableName)}
21791
- WHERE user_id = ${userId}
22315
+ WHERE uid = ${uid}
21792
22316
  ORDER BY created_at
21793
22317
  `)).rows.map((row) => ({
21794
22318
  id: row.id,
21795
- userId: row.user_id,
22319
+ uid: row.uid,
21796
22320
  factorType: row.factor_type,
21797
22321
  friendlyName: row.friendly_name ?? void 0,
21798
22322
  verified: row.verified,
@@ -21803,7 +22327,7 @@ var MfaService = class {
21803
22327
  async getMfaFactorById(factorId) {
21804
22328
  const tableName = this.qualify("mfa_factors");
21805
22329
  const result = await this.db.execute(sql`
21806
- SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
22330
+ SELECT id, uid, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
21807
22331
  FROM ${sql.raw(tableName)}
21808
22332
  WHERE id = ${factorId}
21809
22333
  `);
@@ -21811,7 +22335,7 @@ var MfaService = class {
21811
22335
  const row = result.rows[0];
21812
22336
  return {
21813
22337
  id: row.id,
21814
- userId: row.user_id,
22338
+ uid: row.uid,
21815
22339
  factorType: row.factor_type,
21816
22340
  secretEncrypted: row.secret_encrypted,
21817
22341
  friendlyName: row.friendly_name ?? void 0,
@@ -21828,11 +22352,11 @@ var MfaService = class {
21828
22352
  WHERE id = ${factorId}
21829
22353
  `);
21830
22354
  }
21831
- async deleteMfaFactor(factorId, userId) {
22355
+ async deleteMfaFactor(factorId, uid) {
21832
22356
  const tableName = this.qualify("mfa_factors");
21833
22357
  await this.db.execute(sql`
21834
22358
  DELETE FROM ${sql.raw(tableName)}
21835
- WHERE id = ${factorId} AND user_id = ${userId}
22359
+ WHERE id = ${factorId} AND uid = ${uid}
21836
22360
  `);
21837
22361
  }
21838
22362
  async createMfaChallenge(factorId, ipAddress) {
@@ -21876,43 +22400,43 @@ var MfaService = class {
21876
22400
  WHERE id = ${challengeId}
21877
22401
  `);
21878
22402
  }
21879
- async createRecoveryCodes(userId, codeHashes) {
22403
+ async createRecoveryCodes(uid, codeHashes) {
21880
22404
  const tableName = this.qualify("recovery_codes");
21881
22405
  await this.db.execute(sql`
21882
- DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
22406
+ DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}
21883
22407
  `);
21884
22408
  for (const hash of codeHashes) await this.db.execute(sql`
21885
- INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
21886
- VALUES (${userId}, ${hash})
22409
+ INSERT INTO ${sql.raw(tableName)} (uid, code_hash)
22410
+ VALUES (${uid}, ${hash})
21887
22411
  `);
21888
22412
  }
21889
- async useRecoveryCode(userId, codeHash) {
22413
+ async useRecoveryCode(uid, codeHash) {
21890
22414
  const tableName = this.qualify("recovery_codes");
21891
22415
  return (await this.db.execute(sql`
21892
22416
  UPDATE ${sql.raw(tableName)}
21893
22417
  SET used_at = NOW()
21894
- WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
22418
+ WHERE uid = ${uid} AND code_hash = ${codeHash} AND used_at IS NULL
21895
22419
  RETURNING id
21896
22420
  `)).rows.length > 0;
21897
22421
  }
21898
- async getUnusedRecoveryCodeCount(userId) {
22422
+ async getUnusedRecoveryCodeCount(uid) {
21899
22423
  const tableName = this.qualify("recovery_codes");
21900
22424
  return (await this.db.execute(sql`
21901
22425
  SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
21902
- WHERE user_id = ${userId} AND used_at IS NULL
22426
+ WHERE uid = ${uid} AND used_at IS NULL
21903
22427
  `)).rows[0].count;
21904
22428
  }
21905
- async deleteAllRecoveryCodes(userId) {
22429
+ async deleteAllRecoveryCodes(uid) {
21906
22430
  const tableName = this.qualify("recovery_codes");
21907
22431
  await this.db.execute(sql`
21908
- DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
22432
+ DELETE FROM ${sql.raw(tableName)} WHERE uid = ${uid}
21909
22433
  `);
21910
22434
  }
21911
- async hasVerifiedMfaFactors(userId) {
22435
+ async hasVerifiedMfaFactors(uid) {
21912
22436
  const tableName = this.qualify("mfa_factors");
21913
22437
  return (await this.db.execute(sql`
21914
22438
  SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
21915
- WHERE user_id = ${userId} AND verified = TRUE
22439
+ WHERE uid = ${uid} AND verified = TRUE
21916
22440
  `)).rows[0].count > 0;
21917
22441
  }
21918
22442
  };
@@ -22152,6 +22676,20 @@ function patchPgArrayNullSafety(tables) {
22152
22676
  if (patchedCount > 0) logger.debug(`[PgArray] Patched ${patchedCount} array column(s) for null-safety`);
22153
22677
  }
22154
22678
  //#endregion
22679
+ //#region src/schema/introspect-db-naming.ts
22680
+ /**
22681
+ * Naming helpers shared by the introspection modules. These live apart from
22682
+ * `introspect-db-logic.ts` because the inference pass needs them too, and
22683
+ * importing them from there would close a cycle back through this module.
22684
+ */
22685
+ /**
22686
+ * Convert a snake_case name to a human-readable Title Case label.
22687
+ * e.g. "created_at" -> "Created At", "customer_id" -> "Customer Id"
22688
+ */
22689
+ function humanize(snakeName) {
22690
+ return snakeName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
22691
+ }
22692
+ //#endregion
22155
22693
  //#region src/schema/introspect-db-logic.ts
22156
22694
  var IRREGULAR_SINGULARS = {
22157
22695
  people: "person",
@@ -22211,13 +22749,6 @@ function singularize(word) {
22211
22749
  if (lower.endsWith("s") && !lower.endsWith("ss") && !lower.endsWith("us") && !lower.endsWith("is")) return word.slice(0, -1);
22212
22750
  return word;
22213
22751
  }
22214
- /**
22215
- * Convert a snake_case name to a human-readable Title Case label.
22216
- * e.g. "created_at" -> "Created At", "customer_id" -> "Customer Id"
22217
- */
22218
- function humanize(snakeName) {
22219
- return snakeName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
22220
- }
22221
22752
  function getIconForTable(tableName) {
22222
22753
  const table = tableName.toLowerCase();
22223
22754
  if (table.includes("user") || table.includes("account") || table.includes("member") || table.includes("customer") || table.includes("client") || table.includes("patient")) return "Users";
@@ -22738,6 +23269,11 @@ function createPostgresBootstrapper(pgConfig) {
22738
23269
  } catch (err) {
22739
23270
  logger.warn("⚠️ Could not initialize branch metadata table", { error: err });
22740
23271
  }
23272
+ try {
23273
+ await realtimeService.configureChannelHistory(pgConfig.realtime?.channels);
23274
+ } catch (err) {
23275
+ logger.warn("⚠️ Could not initialize channel history tables — retained channels will not replay", { error: err });
23276
+ }
22741
23277
  const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
22742
23278
  const validModes = new Set([
22743
23279
  "auto",