@rebasepro/server-postgres 0.9.1-canary.ff338b5 → 0.10.1-canary.14e53ae

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 (70) hide show
  1. package/README.md +21 -0
  2. package/dist/PostgresBackendDriver.d.ts +18 -0
  3. package/dist/PostgresBootstrapper.d.ts +11 -1
  4. package/dist/auth/services.d.ts +93 -54
  5. package/dist/backup/backup-logic.d.ts +23 -0
  6. package/dist/backup/backup-service.d.ts +44 -2
  7. package/dist/backup/pg-tools.d.ts +41 -1
  8. package/dist/chunk-DSJWtz9O.js +40 -0
  9. package/dist/cli-helpers.d.ts +33 -1
  10. package/dist/ensure-collection-tables-CNlIONzj.js +304 -0
  11. package/dist/ensure-collection-tables-CNlIONzj.js.map +1 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.es.js +2090 -4741
  14. package/dist/index.es.js.map +1 -1
  15. package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
  16. package/dist/schema/auth-schema.d.ts +194 -24
  17. package/dist/schema/destructive-sql.d.ts +49 -0
  18. package/dist/schema/ensure-collection-tables.d.ts +79 -0
  19. package/dist/schema/generate-postgres-ddl-logic.d.ts +4 -1
  20. package/dist/schema/introspect-db-logic.d.ts +0 -5
  21. package/dist/schema/introspect-db-naming.d.ts +10 -0
  22. package/dist/security/policy-drift.d.ts +24 -0
  23. package/dist/security/rls-enforcement.d.ts +2 -2
  24. package/dist/services/cdc/CdcListener.d.ts +7 -14
  25. package/dist/services/channel-bus/ChannelBus.d.ts +29 -0
  26. package/dist/services/channel-bus/PostgresChannelBus.d.ts +111 -0
  27. package/dist/services/channel-bus/index.d.ts +55 -0
  28. package/dist/services/channel-history.d.ts +129 -0
  29. package/dist/services/channel-presence.d.ts +66 -0
  30. package/dist/services/pg-notify-listener.d.ts +47 -0
  31. package/dist/services/realtimeService.d.ts +183 -8
  32. package/dist/src-B0v4IKaI.js +329 -0
  33. package/dist/src-B0v4IKaI.js.map +1 -0
  34. package/dist/src-DmsRg8MR.js +4056 -0
  35. package/dist/src-DmsRg8MR.js.map +1 -0
  36. package/package.json +7 -31
  37. package/src/PostgresBackendDriver.ts +56 -5
  38. package/src/PostgresBootstrapper.ts +87 -1
  39. package/src/auth/ensure-tables.ts +187 -19
  40. package/src/auth/services.ts +309 -170
  41. package/src/backup/backup-cli.ts +60 -1
  42. package/src/backup/backup-cron.ts +24 -1
  43. package/src/backup/backup-logic.ts +62 -0
  44. package/src/backup/backup-service.ts +132 -13
  45. package/src/backup/pg-tools.ts +70 -2
  46. package/src/cli-helpers.ts +82 -27
  47. package/src/cli.ts +152 -6
  48. package/src/index.ts +4 -0
  49. package/src/schema/auth-bootstrap-sql.ts +7 -1
  50. package/src/schema/auth-schema.ts +53 -15
  51. package/src/schema/destructive-sql.ts +94 -0
  52. package/src/schema/ensure-collection-tables.test.ts +156 -0
  53. package/src/schema/ensure-collection-tables.ts +297 -0
  54. package/src/schema/generate-postgres-ddl-logic.ts +3 -3
  55. package/src/schema/introspect-db-inference.ts +1 -1
  56. package/src/schema/introspect-db-logic.ts +1 -10
  57. package/src/schema/introspect-db-naming.ts +15 -0
  58. package/src/schema/introspect-runtime.ts +1 -1
  59. package/src/security/policy-drift.test.ts +46 -0
  60. package/src/security/policy-drift.ts +70 -4
  61. package/src/security/rls-enforcement.ts +11 -5
  62. package/src/services/cdc/CdcListener.ts +27 -91
  63. package/src/services/channel-bus/ChannelBus.ts +44 -0
  64. package/src/services/channel-bus/PostgresChannelBus.ts +299 -0
  65. package/src/services/channel-bus/index.ts +123 -0
  66. package/src/services/channel-history.ts +378 -0
  67. package/src/services/channel-presence.ts +148 -0
  68. package/src/services/pg-notify-listener.ts +137 -0
  69. package/src/services/realtimeService.ts +581 -24
  70. 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,7 +4,7 @@
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";
@@ -25,6 +25,16 @@ export interface PostgresDriverConfig {
25
25
  * (BaaS mode). Defaults to `public`.
26
26
  */
27
27
  introspectionSchema?: string;
28
+ /**
29
+ * Realtime options, both opt-in:
30
+ *
31
+ * - `channels` — retention. Without rules no channel keeps any history and
32
+ * broadcast stays fire-and-forget. See {@link ChannelRetentionRule}.
33
+ * - `bus` — the cross-instance transport for channel broadcast and
34
+ * presence. Defaults to in-process only, which is correct for a single
35
+ * instance and wrong for two. See {@link ChannelBusConfig}.
36
+ */
37
+ realtime?: RealtimeChannelsConfig;
28
38
  }
29
39
  /**
30
40
  * Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`
@@ -1,6 +1,6 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import type { RebasePgTable } from "../types";
3
- import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData, RoleData, CreateRoleData, RefreshTokenInfo, PasswordResetTokenInfo, MagicLinkTokenInfo, UserIdentityData, ListUsersOptions, PaginatedUsersResult, MfaFactor, MfaChallengeInfo, RoleData as Role } from "@rebasepro/server";
3
+ import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData, RoleData, CreateRoleData, RefreshTokenInfo, RefreshTokenSession, PasswordResetTokenInfo, MagicLinkTokenInfo, UserIdentityData, ListUsersOptions, PaginatedUsersResult, MfaFactor, MfaChallengeInfo, RoleData as Role } from "@rebasepro/server";
4
4
  export type { Role };
5
5
  export interface AuthSchemaTables {
6
6
  users: RebasePgTable;
@@ -23,7 +23,7 @@ export declare class UserService implements UserRepository {
23
23
  * Run a privileged auth write with an explicitly cleared RLS context.
24
24
  *
25
25
  * The auth services run on the base/owner connection, which by design
26
- * carries a NULL `app.user_id` so the `auth.uid() IS NULL` server-escape
26
+ * carries a NULL `app.uid` so the `auth.uid() IS NULL` server-escape
27
27
  * in the default policies applies. That NULL is normally guaranteed by
28
28
  * `set_config(..., is_local = true)` resetting at transaction end — but a
29
29
  * GUC that survives on a pooled connection (or a connection role that
@@ -41,8 +41,8 @@ export declare class UserService implements UserRepository {
41
41
  getUserById(id: string): Promise<UserData | null>;
42
42
  getUserByEmail(email: string): Promise<UserData | null>;
43
43
  getUserByIdentity(provider: string, providerId: string): Promise<UserData | null>;
44
- getUserIdentities(userId: string): Promise<UserIdentityData[]>;
45
- 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>;
46
46
  updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
47
47
  deleteUser(id: string): Promise<void>;
48
48
  listUsers(): Promise<UserData[]>;
@@ -66,23 +66,23 @@ export declare class UserService implements UserRepository {
66
66
  /**
67
67
  * Get roles for a user from database (inline TEXT[] column)
68
68
  */
69
- getUserRoles(userId: string): Promise<Role[]>;
69
+ getUserRoles(uid: string): Promise<Role[]>;
70
70
  /**
71
71
  * Get role IDs for a user
72
72
  */
73
- getUserRoleIds(userId: string): Promise<string[]>;
73
+ getUserRoleIds(uid: string): Promise<string[]>;
74
74
  /**
75
75
  * Set roles for a user (replaces existing roles)
76
76
  */
77
- setUserRoles(userId: string, roleIds: string[]): Promise<void>;
77
+ setUserRoles(uid: string, roleIds: string[]): Promise<void>;
78
78
  /**
79
79
  * Assign a specific role to new user (appends if not present)
80
80
  */
81
- assignDefaultRole(userId: string, roleId: string): Promise<void>;
81
+ assignDefaultRole(uid: string, roleId: string): Promise<void>;
82
82
  /**
83
83
  * Get user with their roles
84
84
  */
85
- getUserWithRoles(userId: string): Promise<{
85
+ getUserWithRoles(uid: string): Promise<{
86
86
  user: UserData;
87
87
  roles: Role[];
88
88
  } | null>;
@@ -90,13 +90,42 @@ export declare class UserService implements UserRepository {
90
90
  export declare class RefreshTokenService {
91
91
  private db;
92
92
  private refreshTokensTable;
93
+ private usersTable;
93
94
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
94
- createToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
95
+ /**
96
+ * Whether the table actually carries a column, so a host application that
97
+ * supplied its own `refresh_tokens` table — one that predates session
98
+ * grouping — degrades instead of throwing on every sign-in.
99
+ */
100
+ private has;
101
+ private col;
102
+ /** The columns to read back, narrowed to the ones this table has. */
103
+ private selection;
104
+ createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
95
105
  findByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
106
+ /**
107
+ * Record that a token was rotated away, keeping the row.
108
+ *
109
+ * The row is what lets `/auth/refresh` distinguish "you already used this,
110
+ * here is a fresh one" from "no idea what this is". Deleting it — which is
111
+ * what this used to do — collapsed both into a 401 and signed the user out
112
+ * for the crime of losing a response.
113
+ */
114
+ markRotated(tokenHash: string): Promise<void>;
115
+ /** Final kill of one sign-in: logout, or revoking a device remotely. */
116
+ revokeSession(sessionId: string): Promise<void>;
117
+ /**
118
+ * Housekeeping: rotation would otherwise leave a row per refresh forever.
119
+ * Superseded rows are only needed for as long as a straggler might still
120
+ * present them, and expired ones are dead weight everywhere.
121
+ */
122
+ prune(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
123
+ getTokensValidAfter(uid: string): Promise<Date | null>;
124
+ setTokensValidAfter(uid: string, at: Date): Promise<void>;
96
125
  deleteByHash(tokenHash: string): Promise<void>;
97
- deleteAllForUser(userId: string): Promise<void>;
98
- listForUser(userId: string): Promise<RefreshTokenInfo[]>;
99
- deleteById(id: string, userId: string): Promise<void>;
126
+ deleteAllForUser(uid: string): Promise<void>;
127
+ listForUser(uid: string): Promise<RefreshTokenInfo[]>;
128
+ deleteById(id: string, uid: string): Promise<void>;
100
129
  }
101
130
  /**
102
131
  * Password reset token service
@@ -109,12 +138,12 @@ export declare class PasswordResetTokenService {
109
138
  /**
110
139
  * Create a password reset token
111
140
  */
112
- createToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
141
+ createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
113
142
  /**
114
143
  * Find a valid (not expired, not used) token by hash
115
144
  */
116
145
  findValidByHash(tokenHash: string): Promise<{
117
- userId: string;
146
+ uid: string;
118
147
  expiresAt: Date;
119
148
  } | null>;
120
149
  /**
@@ -124,7 +153,7 @@ export declare class PasswordResetTokenService {
124
153
  /**
125
154
  * Delete all tokens for a user
126
155
  */
127
- deleteAllForUser(userId: string): Promise<void>;
156
+ deleteAllForUser(uid: string): Promise<void>;
128
157
  /**
129
158
  * Clean up expired tokens
130
159
  */
@@ -139,7 +168,7 @@ export declare class MagicLinkTokenService {
139
168
  private magicLinkTokensTable;
140
169
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
141
170
  private getQualifiedTableName;
142
- createToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
171
+ createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
143
172
  findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
144
173
  markAsUsed(tokenHash: string): Promise<void>;
145
174
  }
@@ -153,18 +182,23 @@ export declare class PostgresTokenRepository implements TokenRepository {
153
182
  private passwordResetTokenService;
154
183
  private magicLinkTokenService;
155
184
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
156
- createRefreshToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
185
+ createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
186
+ markRefreshTokenRotated(tokenHash: string): Promise<void>;
187
+ revokeRefreshTokenSession(sessionId: string): Promise<void>;
188
+ pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
189
+ getTokensValidAfter(uid: string): Promise<Date | null>;
190
+ setTokensValidAfter(uid: string, at: Date): Promise<void>;
157
191
  findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
158
192
  deleteRefreshToken(tokenHash: string): Promise<void>;
159
- deleteAllRefreshTokensForUser(userId: string): Promise<void>;
160
- listRefreshTokensForUser(userId: string): Promise<RefreshTokenInfo[]>;
161
- deleteRefreshTokenById(id: string, userId: string): Promise<void>;
162
- createPasswordResetToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
193
+ deleteAllRefreshTokensForUser(uid: string): Promise<void>;
194
+ listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]>;
195
+ deleteRefreshTokenById(id: string, uid: string): Promise<void>;
196
+ createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
163
197
  findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null>;
164
198
  markPasswordResetTokenUsed(tokenHash: string): Promise<void>;
165
- deleteAllPasswordResetTokensForUser(userId: string): Promise<void>;
199
+ deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
166
200
  deleteExpiredTokens(): Promise<void>;
167
- createMagicLinkToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
201
+ createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
168
202
  findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
169
203
  markMagicLinkTokenUsed(tokenHash: string): Promise<void>;
170
204
  }
@@ -182,8 +216,8 @@ export declare class PostgresAuthRepository implements AuthRepository {
182
216
  getUserById(id: string): Promise<UserData | null>;
183
217
  getUserByEmail(email: string): Promise<UserData | null>;
184
218
  getUserByIdentity(provider: string, providerId: string): Promise<UserData | null>;
185
- getUserIdentities(userId: string): Promise<UserIdentityData[]>;
186
- linkUserIdentity(userId: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
219
+ getUserIdentities(uid: string): Promise<UserIdentityData[]>;
220
+ linkUserIdentity(uid: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void>;
187
221
  updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
188
222
  deleteUser(id: string): Promise<void>;
189
223
  listUsers(): Promise<UserData[]>;
@@ -192,11 +226,11 @@ export declare class PostgresAuthRepository implements AuthRepository {
192
226
  setEmailVerified(id: string, verified: boolean): Promise<void>;
193
227
  setVerificationToken(id: string, token: string | null): Promise<void>;
194
228
  getUserByVerificationToken(token: string): Promise<UserData | null>;
195
- getUserRoles(userId: string): Promise<RoleData[]>;
196
- getUserRoleIds(userId: string): Promise<string[]>;
197
- setUserRoles(userId: string, roleIds: string[]): Promise<void>;
198
- assignDefaultRole(userId: string, roleId: string): Promise<void>;
199
- getUserWithRoles(userId: string): Promise<{
229
+ getUserRoles(uid: string): Promise<RoleData[]>;
230
+ getUserRoleIds(uid: string): Promise<string[]>;
231
+ setUserRoles(uid: string, roleIds: string[]): Promise<void>;
232
+ assignDefaultRole(uid: string, roleId: string): Promise<void>;
233
+ getUserWithRoles(uid: string): Promise<{
200
234
  user: UserData;
201
235
  roles: RoleData[];
202
236
  } | null>;
@@ -205,37 +239,42 @@ export declare class PostgresAuthRepository implements AuthRepository {
205
239
  createRole(_data: CreateRoleData): Promise<RoleData>;
206
240
  updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null>;
207
241
  deleteRole(_id: string): Promise<void>;
208
- createRefreshToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
242
+ createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
243
+ markRefreshTokenRotated(tokenHash: string): Promise<void>;
244
+ revokeRefreshTokenSession(sessionId: string): Promise<void>;
245
+ pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
246
+ getTokensValidAfter(uid: string): Promise<Date | null>;
247
+ setTokensValidAfter(uid: string, at: Date): Promise<void>;
209
248
  findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
210
249
  deleteRefreshToken(tokenHash: string): Promise<void>;
211
- deleteAllRefreshTokensForUser(userId: string): Promise<void>;
212
- listRefreshTokensForUser(userId: string): Promise<RefreshTokenInfo[]>;
213
- deleteRefreshTokenById(id: string, userId: string): Promise<void>;
214
- createPasswordResetToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
250
+ deleteAllRefreshTokensForUser(uid: string): Promise<void>;
251
+ listRefreshTokensForUser(uid: string): Promise<RefreshTokenInfo[]>;
252
+ deleteRefreshTokenById(id: string, uid: string): Promise<void>;
253
+ createPasswordResetToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
215
254
  findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null>;
216
255
  markPasswordResetTokenUsed(tokenHash: string): Promise<void>;
217
- deleteAllPasswordResetTokensForUser(userId: string): Promise<void>;
256
+ deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
218
257
  deleteExpiredTokens(): Promise<void>;
219
- createMagicLinkToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void>;
258
+ createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
220
259
  findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
221
260
  markMagicLinkTokenUsed(tokenHash: string): Promise<void>;
222
261
  private _mfaService;
223
262
  private getMfaService;
224
- createMfaFactor(userId: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
225
- getMfaFactors(userId: string): Promise<MfaFactor[]>;
263
+ createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
264
+ getMfaFactors(uid: string): Promise<MfaFactor[]>;
226
265
  getMfaFactorById(factorId: string): Promise<(MfaFactor & {
227
266
  secretEncrypted: string;
228
267
  }) | null>;
229
268
  verifyMfaFactor(factorId: string): Promise<void>;
230
- deleteMfaFactor(factorId: string, userId: string): Promise<void>;
269
+ deleteMfaFactor(factorId: string, uid: string): Promise<void>;
231
270
  createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo>;
232
271
  getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null>;
233
272
  verifyMfaChallenge(challengeId: string): Promise<void>;
234
- createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void>;
235
- useRecoveryCode(userId: string, codeHash: string): Promise<boolean>;
236
- getUnusedRecoveryCodeCount(userId: string): Promise<number>;
237
- deleteAllRecoveryCodes(userId: string): Promise<void>;
238
- hasVerifiedMfaFactors(userId: string): Promise<boolean>;
273
+ createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
274
+ useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
275
+ getUnusedRecoveryCodeCount(uid: string): Promise<number>;
276
+ deleteAllRecoveryCodes(uid: string): Promise<void>;
277
+ hasVerifiedMfaFactors(uid: string): Promise<boolean>;
239
278
  }
240
279
  /**
241
280
  * PostgreSQL implementation of MfaRepository.
@@ -246,21 +285,21 @@ export declare class MfaService implements MfaRepository {
246
285
  private schemaName;
247
286
  constructor(db: NodePgDatabase, schemaName?: string);
248
287
  private qualify;
249
- createMfaFactor(userId: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
250
- getMfaFactors(userId: string): Promise<MfaFactor[]>;
288
+ createMfaFactor(uid: string, factorType: "totp", secretEncrypted: string, friendlyName?: string): Promise<MfaFactor>;
289
+ getMfaFactors(uid: string): Promise<MfaFactor[]>;
251
290
  getMfaFactorById(factorId: string): Promise<(MfaFactor & {
252
291
  secretEncrypted: string;
253
292
  }) | null>;
254
293
  verifyMfaFactor(factorId: string): Promise<void>;
255
- deleteMfaFactor(factorId: string, userId: string): Promise<void>;
294
+ deleteMfaFactor(factorId: string, uid: string): Promise<void>;
256
295
  createMfaChallenge(factorId: string, ipAddress?: string): Promise<MfaChallengeInfo>;
257
296
  getMfaChallengeById(challengeId: string): Promise<MfaChallengeInfo | null>;
258
297
  verifyMfaChallenge(challengeId: string): Promise<void>;
259
- createRecoveryCodes(userId: string, codeHashes: string[]): Promise<void>;
260
- useRecoveryCode(userId: string, codeHash: string): Promise<boolean>;
261
- getUnusedRecoveryCodeCount(userId: string): Promise<number>;
262
- deleteAllRecoveryCodes(userId: string): Promise<void>;
263
- hasVerifiedMfaFactors(userId: string): Promise<boolean>;
298
+ createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
299
+ useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
300
+ getUnusedRecoveryCodeCount(uid: string): Promise<number>;
301
+ deleteAllRecoveryCodes(uid: string): Promise<void>;
302
+ hasVerifiedMfaFactors(uid: string): Promise<boolean>;
264
303
  }
265
304
  /** PostgreSQL user repository implementation */
266
305
  export type PostgresUserRepository = UserService;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Replay a `pg_dumpall --globals-only` script one statement at a time,
3
+ * tolerating per-statement failures. On a same-cluster restore the roles
4
+ * usually already exist (`CREATE ROLE` → "already exists") and on a managed
5
+ * provider an `ALTER ROLE <superuser>` may be refused; neither should abort
6
+ * recreation of the roles that *are* missing. Returns how many statements
7
+ * applied vs were skipped.
8
+ *
9
+ * `runStatement` executes one SQL statement and rejects on error.
10
+ */
11
+ export declare function applyGlobalsWith(runStatement: (sql: string) => Promise<void>, globalsSql: string, log?: (message: string) => void): Promise<{
12
+ applied: number;
13
+ skipped: number;
14
+ }>;
15
+ /**
16
+ * Delete each pruned dump together with its `.globals.sql` roles sidecar, so
17
+ * pruning never orphans the roles file. The sidecar is best-effort — older
18
+ * backups predate it, so a missing-sidecar failure is swallowed while a
19
+ * failure deleting the dump itself propagates.
20
+ *
21
+ * `deleteObject` removes one key and rejects if it cannot (e.g. not found).
22
+ */
23
+ export declare function pruneWith(keys: string[], deleteObject: (key: string) => Promise<void>): Promise<void>;
@@ -5,8 +5,8 @@ export declare class BackupToolError extends Error {
5
5
  readonly hint?: string | undefined;
6
6
  constructor(message: string, hint?: string | undefined);
7
7
  }
8
- /** Locate `pg_dump` / `pg_restore`, honouring an env override. */
9
- export declare function resolvePgBinary(tool: "pg_dump" | "pg_restore", env?: Record<string, string | undefined>): string | null;
8
+ /** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
9
+ export declare function resolvePgBinary(tool: "pg_dump" | "pg_restore" | "pg_dumpall", env?: Record<string, string | undefined>): string | null;
10
10
  /** Run `<bin> --version` and extract the major version. */
11
11
  export declare function detectToolMajor(bin: string): Promise<number | null>;
12
12
  /** Query the server for its major version via `server_version_num`. */
@@ -27,11 +27,22 @@ export interface BackupResult {
27
27
  localFile: string;
28
28
  fileName: string;
29
29
  sizeBytes: number;
30
+ /**
31
+ * Absolute path of the `.globals.sql` sidecar holding cluster-wide roles
32
+ * (present unless globals capture was disabled or unavailable).
33
+ */
34
+ globalsFile?: string;
35
+ globalsSizeBytes?: number;
30
36
  }
31
37
  /**
32
38
  * Produce a custom-format dump on local disk. When `outDir` is omitted the
33
39
  * file is written to the OS temp directory (used by the upload path, which
34
40
  * cleans it up afterwards).
41
+ *
42
+ * Alongside the `-Fc` dump it writes a `<name>.globals.sql` sidecar via
43
+ * `pg_dumpall --globals-only` so the roles the dump's GRANT/RLS statements
44
+ * depend on can be recreated on restore. Set `includeGlobals: false` to skip
45
+ * it (e.g. when the caller has no privilege to read cluster globals).
35
46
  */
36
47
  export declare function createDump(opts: {
37
48
  connectionString: string;
@@ -41,19 +52,50 @@ export declare function createDump(opts: {
41
52
  excludeSchemas?: string[];
42
53
  noOwner?: boolean;
43
54
  inheritStdio?: boolean;
55
+ includeGlobals?: boolean;
44
56
  env?: Record<string, string | undefined>;
45
57
  }): Promise<BackupResult>;
58
+ /**
59
+ * Cheap integrity check on a freshly written dump: it must be non-empty and
60
+ * `pg_restore --list` must parse its table of contents without error. Used
61
+ * before pruning older backups so a corrupt-but-exit-0 dump never becomes
62
+ * the reason the last good backup is deleted.
63
+ */
64
+ export declare function validateDump(localFile: string, env?: Record<string, string | undefined>): Promise<{
65
+ ok: boolean;
66
+ reason?: string;
67
+ }>;
68
+ /**
69
+ * Replay a `pg_dumpall --globals-only` script to recreate cluster roles
70
+ * before a restore, so the dump's GRANT/RLS statements (which reference
71
+ * `rebase_user` and any owner roles) actually apply. Runs statement by
72
+ * statement and tolerates per-statement failures — on a same-cluster restore
73
+ * the roles usually already exist (`CREATE ROLE` → "already exists"), and on
74
+ * a managed provider an `ALTER ROLE <superuser>` may be refused; neither
75
+ * should abort role recreation. Returns how many statements applied vs were
76
+ * skipped.
77
+ */
78
+ export declare function applyGlobals(connectionString: string, globalsSql: string, log?: (message: string) => void): Promise<{
79
+ applied: number;
80
+ skipped: number;
81
+ }>;
46
82
  /**
47
83
  * Restore a custom-format dump into the database named by
48
84
  * `connectionString`. Destructive when `clean` is set (drops objects
49
85
  * first). Never called automatically — the CLI gates it behind explicit
50
86
  * confirmation.
87
+ *
88
+ * Runs with `--exit-on-error` by default: a restore that logs-and-continues
89
+ * past a failed GRANT (because a role was missing) reports success with RLS
90
+ * un-enforced. Callers should recreate roles first (see {@link applyGlobals})
91
+ * and only set `exitOnError: false` deliberately.
51
92
  */
52
93
  export declare function restoreDump(opts: {
53
94
  connectionString: string;
54
95
  inputFile: string;
55
96
  clean?: boolean;
56
97
  noOwner?: boolean;
98
+ exitOnError?: boolean;
57
99
  inheritStdio?: boolean;
58
100
  env?: Record<string, string | undefined>;
59
101
  }): Promise<void>;
@@ -99,10 +99,50 @@ export declare function buildPgRestoreArgs(opts: {
99
99
  inputFile: string;
100
100
  /** Drop objects before recreating them (destructive but idempotent). */
101
101
  clean?: boolean;
102
- /** Continue past individual errors instead of aborting. */
102
+ /**
103
+ * Abort on the first error instead of logging and continuing. Defaults
104
+ * ON: a restore that silently skips failed GRANT/RLS statements (because
105
+ * a role is missing) "succeeds" with RLS un-enforced — a security hole.
106
+ * Fail loudly instead so the operator knows the restore is incomplete.
107
+ */
103
108
  exitOnError?: boolean;
104
109
  noOwner?: boolean;
105
110
  }): string[];
111
+ /**
112
+ * Assemble the `pg_restore --list` argument vector. Reading a dump's table
113
+ * of contents parses the whole archive without touching a database, so it is
114
+ * a cheap integrity check that the file isn't truncated or corrupt.
115
+ */
116
+ export declare function buildPgRestoreListArgs(inputFile: string): string[];
117
+ /**
118
+ * Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other
119
+ * cluster-wide objects) live outside any single database, so a per-database
120
+ * `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT
121
+ * statements in the main dump fail on restore and RLS is silently lost — so
122
+ * every backup captures the globals into a sidecar `.globals.sql`.
123
+ *
124
+ * `--no-role-passwords` keeps role secrets out of the artifact (backups may
125
+ * be shipped off-box); roles are recreated password-less and re-secured by
126
+ * the operator.
127
+ */
128
+ export declare function buildPgDumpallGlobalsArgs(opts: {
129
+ connectionString: string;
130
+ outFile: string;
131
+ }): string[];
132
+ /**
133
+ * Derive the globals sidecar path/key for a given `.dump` file. Keeps the
134
+ * two artifacts adjacent so listing, uploading and pruning can find one from
135
+ * the other. A name that doesn't end in `.dump` is returned unchanged with a
136
+ * `.globals.sql` suffix appended.
137
+ */
138
+ export declare function globalsFileForDump(dumpPath: string): string;
139
+ /**
140
+ * Split a `pg_dumpall --globals-only` script into individual statements.
141
+ * Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`
142
+ * can run independently and a benign "role already exists" on one doesn't
143
+ * abort the rest. Drops `--` comment lines and blank statements.
144
+ */
145
+ export declare function splitGlobalsStatements(sql: string): string[];
106
146
  /**
107
147
  * Resolve the Postgres connection string the backup commands should use,
108
148
  * mirroring the precedence the branch command already relies on.
@@ -0,0 +1,40 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import "process";
3
+ const require = __createRequire(import.meta.url);
4
+ //#region \0rolldown/runtime.js
5
+ var __create = Object.create;
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __getProtoOf = Object.getPrototypeOf;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
12
+ var __exportAll = (all, no_symbols) => {
13
+ let target = {};
14
+ for (var name in all) __defProp(target, name, {
15
+ get: all[name],
16
+ enumerable: true
17
+ });
18
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
19
+ return target;
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
23
+ key = keys[i];
24
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
25
+ get: ((k) => from[k]).bind(null, key),
26
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
27
+ });
28
+ }
29
+ return to;
30
+ };
31
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
32
+ value: mod,
33
+ enumerable: true
34
+ }) : target, mod));
35
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) {
36
+ if (typeof require !== "undefined") return require.apply(this, arguments);
37
+ throw Error("Calling `require` for \"" + x + "\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.");
38
+ });
39
+ //#endregion
40
+ export { __toESM as i, __exportAll as n, __require as r, __commonJSMin as t };
@@ -4,4 +4,36 @@ export declare function getTableIncludesFromCollections(collections: CollectionC
4
4
  export declare function getTableIncludes(collectionsPath: string): Promise<string[]>;
5
5
  export declare function getDevDatabaseUrl(databaseUrl: string): string;
6
6
  export declare function ensureDevDatabaseExists(databaseUrl: string, devDatabaseUrl: string): Promise<void>;
7
- export declare function getTableExcludes(databaseUrl: string, collectionsPath: string): Promise<string[]>;
7
+ /**
8
+ * Query the live database for every user table/view outside the system
9
+ * catalogs. Separated from {@link getTableExcludes} so its failure mode can
10
+ * be handled explicitly (fail closed) and so tests can inject a stub.
11
+ */
12
+ export declare function queryExistingTables(databaseUrl: string): Promise<string[]>;
13
+ /**
14
+ * Raised when the exclude list could not be built. `db push` MUST abort on
15
+ * this rather than continue: the exclude list is the only thing shielding
16
+ * non-collection (user/system) tables from the auto-approved declarative
17
+ * apply. A partial list — the old fail-open behaviour — meant a transient
18
+ * introspection hiccup dropped every table not present in `schema.sql`.
19
+ */
20
+ export declare class ExcludeIntrospectionError extends Error {
21
+ readonly cause?: unknown | undefined;
22
+ constructor(message: string, cause?: unknown | undefined);
23
+ }
24
+ /**
25
+ * Build the `--exclude` list that protects tables Rebase doesn't manage from
26
+ * the declarative apply. Anything not backing a collection (or its M2M
27
+ * junctions) is excluded so Atlas never drops it.
28
+ *
29
+ * Fails **closed**: if the database can't be introspected we cannot know
30
+ * which tables to protect, so we throw {@link ExcludeIntrospectionError}
31
+ * instead of returning a near-empty list and letting the caller drop
32
+ * everything.
33
+ *
34
+ * `deps` is injectable for tests; production uses the real pg-backed queries.
35
+ */
36
+ export declare function getTableExcludes(databaseUrl: string, collectionsPath: string, deps?: {
37
+ queryExistingTables?: (databaseUrl: string) => Promise<string[]>;
38
+ getIncludes?: (collectionsPath: string) => Promise<string[]>;
39
+ }): Promise<string[]>;