@rebasepro/server-postgres 0.9.1-canary.a57c262 → 0.9.1-canary.a8dbf1c

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 (42) hide show
  1. package/README.md +21 -0
  2. package/dist/PostgresBackendDriver.d.ts +18 -0
  3. package/dist/PostgresBootstrapper.d.ts +17 -1
  4. package/dist/auth/services.d.ts +68 -52
  5. package/dist/connection.d.ts +21 -0
  6. package/dist/index.es.js +914 -226
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
  9. package/dist/schema/auth-schema.d.ts +24 -24
  10. package/dist/schema/doctor.d.ts +1 -1
  11. package/dist/schema/introspect-db-logic.d.ts +0 -5
  12. package/dist/schema/introspect-db-naming.d.ts +10 -0
  13. package/dist/security/policy-drift.d.ts +30 -0
  14. package/dist/security/rls-enforcement.d.ts +2 -2
  15. package/dist/services/channel-history.d.ts +118 -0
  16. package/dist/services/realtimeService.d.ts +69 -2
  17. package/dist/services/row-pipeline.d.ts +5 -2
  18. package/package.json +8 -31
  19. package/src/PostgresBackendDriver.ts +74 -10
  20. package/src/PostgresBootstrapper.ts +69 -3
  21. package/src/auth/ensure-tables.ts +170 -28
  22. package/src/auth/services.ts +181 -150
  23. package/src/cli.ts +60 -0
  24. package/src/connection.ts +61 -1
  25. package/src/databasePoolManager.ts +2 -0
  26. package/src/schema/auth-bootstrap-sql.ts +7 -1
  27. package/src/schema/auth-schema.ts +13 -13
  28. package/src/schema/doctor.ts +45 -20
  29. package/src/schema/introspect-db-inference.ts +1 -1
  30. package/src/schema/introspect-db-logic.ts +1 -10
  31. package/src/schema/introspect-db-naming.ts +15 -0
  32. package/src/schema/introspect-db.ts +11 -1
  33. package/src/schema/introspect-runtime.ts +1 -1
  34. package/src/security/policy-drift.test.ts +106 -1
  35. package/src/security/policy-drift.ts +56 -0
  36. package/src/security/rls-enforcement.ts +11 -5
  37. package/src/services/PersistService.ts +9 -2
  38. package/src/services/channel-history.ts +343 -0
  39. package/src/services/realtimeService.ts +198 -10
  40. package/src/services/row-pipeline.ts +70 -7
  41. package/src/utils/drizzle-conditions.ts +13 -0
  42. package/src/websocket.ts +30 -11
package/README.md CHANGED
@@ -76,6 +76,27 @@ process.on("SIGTERM", async () => {
76
76
  | `statementTimeout` | 30,000 |
77
77
  | `keepAlive` | true |
78
78
 
79
+ ## Testing
80
+
81
+ This package runs **two test runners**, split by directory. This is deliberate — check which half you are in before running anything.
82
+
83
+ | Tests | Runner | Config | Command |
84
+ |-------|--------|--------|---------|
85
+ | `test/*.ts` (unit) | jest | [`jest.config.cjs`](./jest.config.cjs) | `pnpm test` |
86
+ | `test/e2e/**` (integration) | vitest | [`vitest.e2e.config.ts`](./vitest.e2e.config.ts) | `pnpm test:e2e` |
87
+
88
+ The unit tests use jest's injected globals (`describe`/`it`/`expect`) and `jest.mock`. The e2e tests import explicitly from `vitest` and need Docker (testcontainers spins up a real Postgres).
89
+
90
+ **Running a single unit test:**
91
+
92
+ ```bash
93
+ npx jest test/auth-services.test.ts
94
+ ```
95
+
96
+ Pointing `vitest` at a unit test is the easy mistake here — it produces a bare `ReferenceError: jest is not defined` and a "no tests" result, which looks exactly like a dead or broken test file rather than the wrong runner. [`vitest.config.ts`](./vitest.config.ts) exists solely to intercept that and print the command you actually wanted.
97
+
98
+ `pnpm test` runs jest **without** `--passWithNoTests`: if the unit suite ever stops being collected, CI fails instead of going quietly green.
99
+
79
100
  ## Related Packages
80
101
 
81
102
  | Package | Role |
@@ -114,6 +114,24 @@ export declare class PostgresBackendDriver implements DataDriver {
114
114
  }): Promise<Record<string, unknown>[]>;
115
115
  fetchAvailableDatabases(): Promise<string[]>;
116
116
  fetchAvailableRoles(): Promise<string[]>;
117
+ /**
118
+ * Application-level roles actually in use in this project.
119
+ *
120
+ * Distinct from {@link fetchAvailableRoles}, which returns native
121
+ * PostgreSQL roles from `pg_roles` (`postgres`, `rebase_user`, …). Those
122
+ * are the roles the SQL editor can `SET ROLE` to. *These* are the strings
123
+ * held in the users table's `roles` column, injected per-transaction as
124
+ * `auth.roles()` and matched by `SecurityRule.roles`. Feeding the pg roles
125
+ * into a `SecurityRule.roles` field produces a condition no user can ever
126
+ * satisfy, so the two must not be conflated.
127
+ *
128
+ * Roles have no registry table — they were migrated out of
129
+ * `rebase.user_roles` onto an inline `roles TEXT[]` column — so the live
130
+ * set is derived from what is assigned. A role that is declared in a policy
131
+ * but held by nobody yet cannot be discovered here; callers that need it
132
+ * should union in the roles they already know about.
133
+ */
134
+ fetchApplicationRoles(): Promise<string[]>;
117
135
  fetchCurrentDatabase(): Promise<string | undefined>;
118
136
  /**
119
137
  * Fetch public tables that are not yet mapped to a collection.
@@ -4,11 +4,12 @@
4
4
  * Implements the `BackendBootstrapper` interface for PostgreSQL.
5
5
  */
6
6
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
7
- import { BackendBootstrapper } from "@rebasepro/types";
7
+ import { BackendBootstrapper, type RealtimeChannelsConfig } from "@rebasepro/types";
8
8
  import { PostgresBackendDriver } from "./PostgresBackendDriver";
9
9
  import { RealtimeService } from "./services/realtimeService";
10
10
  import { DatabasePoolManager } from "./databasePoolManager";
11
11
  import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
12
+ import { type CdcTableRef } from "./services/cdc/trigger-cdc";
12
13
  export interface PostgresDriverConfig {
13
14
  connectionString?: string;
14
15
  adminConnectionString?: string;
@@ -24,6 +25,12 @@ export interface PostgresDriverConfig {
24
25
  * (BaaS mode). Defaults to `public`.
25
26
  */
26
27
  introspectionSchema?: string;
28
+ /**
29
+ * Realtime options. Currently only channel retention, which is opt-in:
30
+ * without rules here no channel keeps any history and broadcast stays
31
+ * fire-and-forget. See {@link ChannelRetentionRule}.
32
+ */
33
+ realtime?: RealtimeChannelsConfig;
27
34
  }
28
35
  /**
29
36
  * Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`
@@ -36,6 +43,15 @@ export interface PostgresDriverInternals {
36
43
  realtimeService: RealtimeService;
37
44
  driver: PostgresBackendDriver;
38
45
  poolManager?: DatabasePoolManager;
46
+ /**
47
+ * Attach CDC triggers to tables that did not exist when the driver
48
+ * bootstrapped. Only set when database-level capture is actually active.
49
+ *
50
+ * Auth owns its own tables and creates them later in boot, so at driver
51
+ * bootstrap they are legitimately missing and get skipped; without this
52
+ * they would stay uninstrumented until the next restart.
53
+ */
54
+ provisionCdcForTables?: (tables: CdcTableRef[]) => Promise<void>;
39
55
  }
40
56
  /**
41
57
  * Default PostgreSQL bootstrapper.
@@ -19,14 +19,30 @@ export declare class UserService implements UserRepository {
19
19
  private userIdentitiesTable;
20
20
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
21
21
  private getQualifiedUsersTableName;
22
+ /**
23
+ * Run a privileged auth write with an explicitly cleared RLS context.
24
+ *
25
+ * The auth services run on the base/owner connection, which by design
26
+ * carries a NULL `app.uid` so the `auth.uid() IS NULL` server-escape
27
+ * in the default policies applies. That NULL is normally guaranteed by
28
+ * `set_config(..., is_local = true)` resetting at transaction end — but a
29
+ * GUC that survives on a pooled connection (or a connection role that
30
+ * doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role)
31
+ * turns the trusted write into an RLS-scoped one and denies it with
32
+ * SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the
33
+ * single chokepoint, makes the server context deterministic instead of
34
+ * trusting whatever state the pool hands us. `auth.uid()` reads '' as
35
+ * NULL via NULLIF, so '' is the server context.
36
+ */
37
+ private withServerContext;
22
38
  private mapRowToUser;
23
39
  private mapPayload;
24
40
  createUser(data: CreateUserData): Promise<UserData>;
25
41
  getUserById(id: string): Promise<UserData | null>;
26
42
  getUserByEmail(email: string): Promise<UserData | null>;
27
43
  getUserByIdentity(provider: string, providerId: string): Promise<UserData | null>;
28
- getUserIdentities(userId: string): Promise<UserIdentityData[]>;
29
- linkUserIdentity(userId: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
44
+ getUserIdentities(uid: string): Promise<UserIdentityData[]>;
45
+ linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
30
46
  updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
31
47
  deleteUser(id: string): Promise<void>;
32
48
  listUsers(): Promise<UserData[]>;
@@ -50,23 +66,23 @@ export declare class UserService implements UserRepository {
50
66
  /**
51
67
  * Get roles for a user from database (inline TEXT[] column)
52
68
  */
53
- getUserRoles(userId: string): Promise<Role[]>;
69
+ getUserRoles(uid: string): Promise<Role[]>;
54
70
  /**
55
71
  * Get role IDs for a user
56
72
  */
57
- getUserRoleIds(userId: string): Promise<string[]>;
73
+ getUserRoleIds(uid: string): Promise<string[]>;
58
74
  /**
59
75
  * Set roles for a user (replaces existing roles)
60
76
  */
61
- setUserRoles(userId: string, roleIds: string[]): Promise<void>;
77
+ setUserRoles(uid: string, roleIds: string[]): Promise<void>;
62
78
  /**
63
79
  * Assign a specific role to new user (appends if not present)
64
80
  */
65
- assignDefaultRole(userId: string, roleId: string): Promise<void>;
81
+ assignDefaultRole(uid: string, roleId: string): Promise<void>;
66
82
  /**
67
83
  * Get user with their roles
68
84
  */
69
- getUserWithRoles(userId: string): Promise<{
85
+ getUserWithRoles(uid: string): Promise<{
70
86
  user: UserData;
71
87
  roles: Role[];
72
88
  } | null>;
@@ -75,12 +91,12 @@ export declare class RefreshTokenService {
75
91
  private db;
76
92
  private refreshTokensTable;
77
93
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
78
- createToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
94
+ createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
79
95
  findByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
80
96
  deleteByHash(tokenHash: string): Promise<void>;
81
- deleteAllForUser(userId: string): Promise<void>;
82
- listForUser(userId: string): Promise<RefreshTokenInfo[]>;
83
- deleteById(id: string, userId: string): Promise<void>;
97
+ deleteAllForUser(uid: string): Promise<void>;
98
+ listForUser(uid: string): Promise<RefreshTokenInfo[]>;
99
+ deleteById(id: string, uid: string): Promise<void>;
84
100
  }
85
101
  /**
86
102
  * Password reset token service
@@ -93,12 +109,12 @@ export declare class PasswordResetTokenService {
93
109
  /**
94
110
  * Create a password reset token
95
111
  */
96
- createToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
112
+ createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
97
113
  /**
98
114
  * Find a valid (not expired, not used) token by hash
99
115
  */
100
116
  findValidByHash(tokenHash: string): Promise<{
101
- userId: string;
117
+ uid: string;
102
118
  expiresAt: Date;
103
119
  } | null>;
104
120
  /**
@@ -108,7 +124,7 @@ export declare class PasswordResetTokenService {
108
124
  /**
109
125
  * Delete all tokens for a user
110
126
  */
111
- deleteAllForUser(userId: string): Promise<void>;
127
+ deleteAllForUser(uid: string): Promise<void>;
112
128
  /**
113
129
  * Clean up expired tokens
114
130
  */
@@ -123,7 +139,7 @@ export declare class MagicLinkTokenService {
123
139
  private magicLinkTokensTable;
124
140
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
125
141
  private getQualifiedTableName;
126
- createToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
142
+ createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
127
143
  findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
128
144
  markAsUsed(tokenHash: string): Promise<void>;
129
145
  }
@@ -137,18 +153,18 @@ export declare class PostgresTokenRepository implements TokenRepository {
137
153
  private passwordResetTokenService;
138
154
  private magicLinkTokenService;
139
155
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
140
- createRefreshToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
156
+ createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
141
157
  findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
142
158
  deleteRefreshToken(tokenHash: string): Promise<void>;
143
- deleteAllRefreshTokensForUser(userId: string): Promise<void>;
144
- listRefreshTokensForUser(userId: string): Promise<RefreshTokenInfo[]>;
145
- deleteRefreshTokenById(id: string, userId: string): Promise<void>;
146
- createPasswordResetToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
159
+ deleteAllRefreshTokensForUser(uid: string): Promise<void>;
160
+ listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]>;
161
+ deleteRefreshTokenById(id: string, uid: string): Promise<void>;
162
+ createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
147
163
  findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null>;
148
164
  markPasswordResetTokenUsed(tokenHash: string): Promise<void>;
149
- deleteAllPasswordResetTokensForUser(userId: string): Promise<void>;
165
+ deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
150
166
  deleteExpiredTokens(): Promise<void>;
151
- createMagicLinkToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
167
+ createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
152
168
  findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
153
169
  markMagicLinkTokenUsed(tokenHash: string): Promise<void>;
154
170
  }
@@ -166,8 +182,8 @@ export declare class PostgresAuthRepository implements AuthRepository {
166
182
  getUserById(id: string): Promise<UserData | null>;
167
183
  getUserByEmail(email: string): Promise<UserData | null>;
168
184
  getUserByIdentity(provider: string, providerId: string): Promise<UserData | null>;
169
- getUserIdentities(userId: string): Promise<UserIdentityData[]>;
170
- linkUserIdentity(userId: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
185
+ getUserIdentities(uid: string): Promise<UserIdentityData[]>;
186
+ linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
171
187
  updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
172
188
  deleteUser(id: string): Promise<void>;
173
189
  listUsers(): Promise<UserData[]>;
@@ -176,11 +192,11 @@ export declare class PostgresAuthRepository implements AuthRepository {
176
192
  setEmailVerified(id: string, verified: boolean): Promise<void>;
177
193
  setVerificationToken(id: string, token: string | null): Promise<void>;
178
194
  getUserByVerificationToken(token: string): Promise<UserData | null>;
179
- getUserRoles(userId: string): Promise<RoleData[]>;
180
- getUserRoleIds(userId: string): Promise<string[]>;
181
- setUserRoles(userId: string, roleIds: string[]): Promise<void>;
182
- assignDefaultRole(userId: string, roleId: string): Promise<void>;
183
- getUserWithRoles(userId: string): Promise<{
195
+ getUserRoles(uid: string): Promise<RoleData[]>;
196
+ getUserRoleIds(uid: string): Promise<string[]>;
197
+ setUserRoles(uid: string, roleIds: string[]): Promise<void>;
198
+ assignDefaultRole(uid: string, roleId: string): Promise<void>;
199
+ getUserWithRoles(uid: string): Promise<{
184
200
  user: UserData;
185
201
  roles: RoleData[];
186
202
  } | null>;
@@ -189,37 +205,37 @@ export declare class PostgresAuthRepository implements AuthRepository {
189
205
  createRole(_data: CreateRoleData): Promise<RoleData>;
190
206
  updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null>;
191
207
  deleteRole(_id: string): Promise<void>;
192
- createRefreshToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
208
+ createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
193
209
  findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
194
210
  deleteRefreshToken(tokenHash: string): Promise<void>;
195
- deleteAllRefreshTokensForUser(userId: string): Promise<void>;
196
- listRefreshTokensForUser(userId: string): Promise<RefreshTokenInfo[]>;
197
- deleteRefreshTokenById(id: string, userId: string): Promise<void>;
198
- createPasswordResetToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
211
+ deleteAllRefreshTokensForUser(uid: string): Promise<void>;
212
+ listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]>;
213
+ deleteRefreshTokenById(id: string, uid: string): Promise<void>;
214
+ createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
199
215
  findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null>;
200
216
  markPasswordResetTokenUsed(tokenHash: string): Promise<void>;
201
- deleteAllPasswordResetTokensForUser(userId: string): Promise<void>;
217
+ deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
202
218
  deleteExpiredTokens(): Promise<void>;
203
- createMagicLinkToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
219
+ createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
204
220
  findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
205
221
  markMagicLinkTokenUsed(tokenHash: string): Promise<void>;
206
222
  private _mfaService;
207
223
  private getMfaService;
208
- createMfaFactor(userId: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
209
- getMfaFactors(userId: string): Promise<MfaFactor[]>;
224
+ createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
225
+ getMfaFactors(uid: string): Promise<MfaFactor[]>;
210
226
  getMfaFactorById(factorId: string): Promise<(MfaFactor & {
211
227
  secretEncrypted: string;
212
228
  }) | null>;
213
229
  verifyMfaFactor(factorId: string): Promise<void>;
214
- deleteMfaFactor(factorId: string, userId: string): Promise<void>;
230
+ deleteMfaFactor(factorId: string, uid: string): Promise<void>;
215
231
  createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo>;
216
232
  getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null>;
217
233
  verifyMfaChallenge(challengeId: string): Promise<void>;
218
- createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void>;
219
- useRecoveryCode(userId: string, codeHash: string): Promise<boolean>;
220
- getUnusedRecoveryCodeCount(userId: string): Promise<number>;
221
- deleteAllRecoveryCodes(userId: string): Promise<void>;
222
- hasVerifiedMfaFactors(userId: string): Promise<boolean>;
234
+ createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
235
+ useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
236
+ getUnusedRecoveryCodeCount(uid: string): Promise<number>;
237
+ deleteAllRecoveryCodes(uid: string): Promise<void>;
238
+ hasVerifiedMfaFactors(uid: string): Promise<boolean>;
223
239
  }
224
240
  /**
225
241
  * PostgreSQL implementation of MfaRepository.
@@ -230,21 +246,21 @@ export declare class MfaService implements MfaRepository {
230
246
  private schemaName;
231
247
  constructor(db: NodePgDatabase, schemaName?: string);
232
248
  private qualify;
233
- createMfaFactor(userId: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
234
- getMfaFactors(userId: string): Promise<MfaFactor[]>;
249
+ createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
250
+ getMfaFactors(uid: string): Promise<MfaFactor[]>;
235
251
  getMfaFactorById(factorId: string): Promise<(MfaFactor & {
236
252
  secretEncrypted: string;
237
253
  }) | null>;
238
254
  verifyMfaFactor(factorId: string): Promise<void>;
239
- deleteMfaFactor(factorId: string, userId: string): Promise<void>;
255
+ deleteMfaFactor(factorId: string, uid: string): Promise<void>;
240
256
  createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo>;
241
257
  getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null>;
242
258
  verifyMfaChallenge(challengeId: string): Promise<void>;
243
- createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void>;
244
- useRecoveryCode(userId: string, codeHash: string): Promise<boolean>;
245
- getUnusedRecoveryCodeCount(userId: string): Promise<number>;
246
- deleteAllRecoveryCodes(userId: string): Promise<void>;
247
- hasVerifiedMfaFactors(userId: string): Promise<boolean>;
259
+ createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
260
+ useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
261
+ getUnusedRecoveryCodeCount(uid: string): Promise<number>;
262
+ deleteAllRecoveryCodes(uid: string): Promise<void>;
263
+ hasVerifiedMfaFactors(uid: string): Promise<boolean>;
248
264
  }
249
265
  /** PostgreSQL user repository implementation */
250
266
  export type PostgresUserRepository = UserService;
@@ -19,6 +19,27 @@ export interface PostgresPoolConfig {
19
19
  /** Enable TCP keep-alive (default: true) */
20
20
  keepAlive?: boolean;
21
21
  }
22
+ /**
23
+ * Destroy pool clients that are released while still inside a transaction.
24
+ *
25
+ * pg-pool returns a client to the idle list whenever `release()` is called
26
+ * without an error — even if the connection is still mid-transaction (status
27
+ * `T`/`E`). That happens in practice: drizzle's pool transaction releases in
28
+ * a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails
29
+ * (e.g. it was queued behind a statement that hit the client-side
30
+ * query_timeout), the client goes back dirty. The next checkout then runs
31
+ * its statements inside the zombie transaction — with the previous request's
32
+ * `app.*` RLS GUCs still applied, which turns unrelated queries into
33
+ * RLS-scoped ones (observed in production as registration failing with
34
+ * SQLSTATE 42501 under a leaked anonymous context).
35
+ *
36
+ * pg-pool emits `release` before it consults its private `_expired` set, so
37
+ * marking the client expired here makes `_release()` destroy it instead of
38
+ * pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are
39
+ * private APIs — feature-detect and fall back to loud logging so an upstream
40
+ * change degrades to observability, never to silent corruption.
41
+ */
42
+ export declare function guardPoolAgainstDirtyRelease(pool: Pool, label: string): void;
22
43
  /**
23
44
  * Create a Drizzle-backed Postgres connection with a production-grade
24
45
  * connection pool.