@rebasepro/server-postgres 0.23.0 → 0.24.1-canary.g56af4a4

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 (180) hide show
  1. package/dist/{BranchService-DGPL6G_C.js → BranchService-Bsn2wfaj.js} +36 -18
  2. package/dist/BranchService-Bsn2wfaj.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +166 -13
  4. package/dist/auth/ensure-tables.d.ts +12 -0
  5. package/dist/auth/services.d.ts +78 -14
  6. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  7. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  8. package/dist/backup/backup-cli.d.ts +14 -0
  9. package/dist/backup/backup-service.d.ts +2 -2
  10. package/dist/backup/pg-binary.d.ts +5 -0
  11. package/dist/{backup-cli-24v4OlSp.js → backup-cli-aUfRv16h.js} +37 -29
  12. package/dist/backup-cli-aUfRv16h.js.map +1 -0
  13. package/dist/{backup-service-HQ9GC4tN.js → backup-service-BTBB7RXn.js} +60 -28
  14. package/dist/backup-service-BTBB7RXn.js.map +1 -0
  15. package/dist/boot-ddl-DVpsFN6r.js +105 -0
  16. package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
  17. package/dist/cli-errors-CaI4ntyD.js +1919 -0
  18. package/dist/cli-errors-CaI4ntyD.js.map +1 -0
  19. package/dist/cli-errors.d.ts +93 -5
  20. package/dist/{cli-flags-BglvpjHv.js → cli-flags-B-Dawocl.js} +5 -2
  21. package/dist/cli-flags-B-Dawocl.js.map +1 -0
  22. package/dist/cli-helpers.d.ts +52 -12
  23. package/dist/cli-output.d.ts +16 -28
  24. package/dist/cli-push.d.ts +87 -0
  25. package/dist/cli.js +625 -190
  26. package/dist/cli.js.map +1 -1
  27. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-drZIsSNf.js} +42 -26
  28. package/dist/column-plan-helpers-drZIsSNf.js.map +1 -0
  29. package/dist/{databasePoolManager-Bj5FbeAs.js → connection-BPoNILnD.js} +19 -110
  30. package/dist/connection-BPoNILnD.js.map +1 -0
  31. package/dist/connection.d.ts +9 -1
  32. package/dist/data-transformer.d.ts +0 -8
  33. package/dist/databasePoolManager-BkEy2w18.js +119 -0
  34. package/dist/databasePoolManager-BkEy2w18.js.map +1 -0
  35. package/dist/deferred-index-build-DP1_SNFo.js +1051 -0
  36. package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
  37. package/dist/{doctor-Cb2thZ8s.js → doctor-C4WiftHm.js} +279 -87
  38. package/dist/doctor-C4WiftHm.js.map +1 -0
  39. package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
  40. package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
  41. package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
  42. package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
  43. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-CHsgyLqg.js} +178 -75
  44. package/dist/ensure-tables-CHsgyLqg.js.map +1 -0
  45. package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
  46. package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
  47. package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
  48. package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
  49. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-B74WN37S.js} +28 -10
  50. package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
  51. package/dist/generate-files-CdEckX6c.js +199 -0
  52. package/dist/generate-files-CdEckX6c.js.map +1 -0
  53. package/dist/generate-postgres-ddl-logic-B64O2oZF.js +152 -0
  54. package/dist/generate-postgres-ddl-logic-B64O2oZF.js.map +1 -0
  55. package/dist/generated-sql.d.ts +34 -0
  56. package/dist/history/HistoryService.d.ts +38 -6
  57. package/dist/history/ensure-history-table.d.ts +42 -4
  58. package/dist/index.es.js +5557 -7612
  59. package/dist/index.es.js.map +1 -1
  60. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-C-Di8dA9.js} +562 -350
  61. package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
  62. package/dist/introspect-db-queries-D6kAqReI.js +339 -0
  63. package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
  64. package/dist/pg-error-utils-vJezlXdI.js +388 -0
  65. package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
  66. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-B1YF_tYq.js} +278 -533
  67. package/dist/plan-schema-B1YF_tYq.js.map +1 -0
  68. package/dist/{policy-drift-DljYdrpW.js → policy-drift-Dm8i-s1J.js} +24 -7
  69. package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
  70. package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
  71. package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
  72. package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
  73. package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
  74. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-CtC3y9Ol.js} +17 -151
  75. package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
  76. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1046
  77. package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
  78. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-DvzqlaL1.js} +137 -17
  79. package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
  80. package/dist/schema/auth-schema.d.ts +172 -2
  81. package/dist/schema/boot-ddl.d.ts +47 -0
  82. package/dist/schema/classify-change.d.ts +29 -1
  83. package/dist/schema/collection-index.d.ts +45 -0
  84. package/dist/schema/column-plan-helpers.d.ts +38 -21
  85. package/dist/schema/deferred-index-build.d.ts +131 -0
  86. package/dist/schema/destructive-sql.d.ts +27 -1
  87. package/dist/schema/doctor-cli.js +110 -6
  88. package/dist/schema/doctor-cli.js.map +1 -1
  89. package/dist/schema/doctor-search-scale.d.ts +45 -0
  90. package/dist/schema/doctor.d.ts +53 -1
  91. package/dist/schema/ensure-collection-policies.d.ts +95 -35
  92. package/dist/schema/ensure-collection-tables.d.ts +56 -3
  93. package/dist/schema/enum-labels-kept.d.ts +26 -0
  94. package/dist/schema/generate-drizzle-schema.js +1 -1
  95. package/dist/schema/generate-files.d.ts +44 -0
  96. package/dist/schema/generate-next-step.d.ts +0 -13
  97. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  98. package/dist/schema/generate-postgres-ddl.js +10 -81
  99. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  100. package/dist/schema/generate-schema-commit.d.ts +12 -0
  101. package/dist/schema/introspect-db-logic.d.ts +43 -0
  102. package/dist/schema/introspect-db-queries.d.ts +12 -2
  103. package/dist/schema/introspect-db-search.d.ts +22 -0
  104. package/dist/schema/introspect-db-storage.d.ts +83 -0
  105. package/dist/schema/introspect-db-structure.d.ts +10 -0
  106. package/dist/schema/introspect-db.js +17 -6
  107. package/dist/schema/introspect-db.js.map +1 -1
  108. package/dist/schema/migration-post-process.d.ts +34 -0
  109. package/dist/schema/plan/diff-plan.d.ts +127 -1
  110. package/dist/schema/plan/plan-schema.d.ts +16 -8
  111. package/dist/schema/plan/render-ddl.d.ts +6 -0
  112. package/dist/schema/plan/types.d.ts +45 -11
  113. package/dist/schema/push-index-prebuild.d.ts +25 -0
  114. package/dist/schema/push-legacy-renames.d.ts +20 -0
  115. package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
  116. package/dist/search-column-BM-GV6vH.js +442 -0
  117. package/dist/search-column-BM-GV6vH.js.map +1 -0
  118. package/dist/security/policy-drift.d.ts +1 -1
  119. package/dist/security/rls-enforcement.d.ts +39 -4
  120. package/dist/services/BranchService.d.ts +22 -1
  121. package/dist/services/FetchService.d.ts +63 -12
  122. package/dist/services/PersistService.d.ts +15 -0
  123. package/dist/services/RelationService.d.ts +106 -3
  124. package/dist/services/cdc/CdcListener.d.ts +26 -10
  125. package/dist/services/cdc/identity-columns.d.ts +19 -0
  126. package/dist/services/cdc/trigger-cdc.d.ts +180 -18
  127. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  128. package/dist/services/channel-history.d.ts +32 -5
  129. package/dist/services/collection-helpers.d.ts +26 -1
  130. package/dist/services/field-viewer.d.ts +15 -1
  131. package/dist/services/pg-notify-listener.d.ts +95 -6
  132. package/dist/services/realtimeService.d.ts +467 -53
  133. package/dist/services/row-pipeline.d.ts +16 -9
  134. package/dist/services/socket-liveness.d.ts +85 -0
  135. package/dist/services/soft-delete.d.ts +12 -0
  136. package/dist/services/sql-script.d.ts +275 -0
  137. package/dist/services/vector-iterative-scan.d.ts +40 -0
  138. package/dist/services/write-depth.d.ts +22 -0
  139. package/dist/services/write-transaction-scope.d.ts +15 -3
  140. package/dist/socket-liveness-D3UD5_BW.js +7570 -0
  141. package/dist/socket-liveness-D3UD5_BW.js.map +1 -0
  142. package/dist/socket-opt-ins.d.ts +64 -0
  143. package/dist/source-DBDbBTL4.js +811 -0
  144. package/dist/source-DBDbBTL4.js.map +1 -0
  145. package/dist/tsx-loader.d.ts +14 -0
  146. package/dist/utils/drizzle-conditions.d.ts +63 -3
  147. package/dist/utils/error-cause.d.ts +7 -0
  148. package/dist/utils/pg-error-utils.d.ts +2 -4
  149. package/dist/utils/result-rows.d.ts +10 -0
  150. package/dist/websocket.d.ts +63 -18
  151. package/package.json +7 -7
  152. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  153. package/dist/backup-cli-24v4OlSp.js.map +0 -1
  154. package/dist/backup-service-HQ9GC4tN.js.map +0 -1
  155. package/dist/cli-errors-B8qHg02P.js +0 -798
  156. package/dist/cli-errors-B8qHg02P.js.map +0 -1
  157. package/dist/cli-flags-BglvpjHv.js.map +0 -1
  158. package/dist/cli-output-CNdMql-L.js +0 -47
  159. package/dist/cli-output-CNdMql-L.js.map +0 -1
  160. package/dist/collection-index-DdnsxiJ_.js +0 -286
  161. package/dist/collection-index-DdnsxiJ_.js.map +0 -1
  162. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  163. package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
  164. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  165. package/dist/ensure-collection-policies-RHUcEp4v.js +0 -105
  166. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  167. package/dist/ensure-collection-tables-BEEjn5cn.js +0 -857
  168. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  169. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  170. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  171. package/dist/generate-drizzle-schema-yzY_BLhr.js +0 -123
  172. package/dist/generate-drizzle-schema-yzY_BLhr.js.map +0 -1
  173. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  174. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  175. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  176. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  177. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  178. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
  179. package/dist/source-Br7L7GOI.js +0 -410
  180. package/dist/source-Br7L7GOI.js.map +0 -1
@@ -1,7 +1,6 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import { type RebasePgTable } from "../types.js";
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
- export type { Role };
3
+ import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData, RefreshTokenInfo, RefreshTokenSession, PasswordResetTokenInfo, MagicLinkTokenInfo, UserIdentityData, ListUsersOptions, PaginatedUsersResult, MfaFactor, MfaChallengeInfo, AccountAccessState, PendingEmailChange } from "@rebasepro/server";
5
4
  export interface AuthSchemaTables {
6
5
  users: RebasePgTable;
7
6
  refreshTokens: RebasePgTable;
@@ -38,6 +37,8 @@ export declare class UserService implements UserRepository {
38
37
  private db;
39
38
  private usersTable;
40
39
  private userIdentitiesTable;
40
+ /** Read by `getAccountAccessState`, to say whether one sign-in is still live. */
41
+ private refreshTokensTable;
41
42
  constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
42
43
  private getQualifiedUsersTableName;
43
44
  /**
@@ -83,6 +84,11 @@ export declare class UserService implements UserRepository {
83
84
  updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
84
85
  deleteUser(id: string): Promise<void>;
85
86
  listUsers(): Promise<UserData[]>;
87
+ /**
88
+ * @see UserRepository.firstUserIds — an unordered `LIMIT` read of the
89
+ * primary key, not `listUsersPaginated`'s count and sort of every row.
90
+ */
91
+ firstUserIds(limit: number): Promise<string[]>;
86
92
  listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult>;
87
93
  /**
88
94
  * Update user's password hash; `null` removes the password.
@@ -100,10 +106,6 @@ export declare class UserService implements UserRepository {
100
106
  * Find user by email verification token
101
107
  */
102
108
  getUserByVerificationToken(token: string): Promise<UserData | null>;
103
- /**
104
- * Get roles for a user from database (inline TEXT[] column)
105
- */
106
- getUserRoles(uid: string): Promise<Role[]>;
107
109
  /**
108
110
  * Get role IDs for a user
109
111
  */
@@ -116,12 +118,53 @@ export declare class UserService implements UserRepository {
116
118
  * Assign a specific role to new user (appends if not present)
117
119
  */
118
120
  assignDefaultRole(uid: string, roleId: string): Promise<void>;
121
+ /**
122
+ * One read of what an access token is judged against: whether the account
123
+ * exists, its roles, and its revocation watermark. See
124
+ * `TokenRepository.getAccountAccessState`.
125
+ *
126
+ * The watermark is read only where the table declares it, as
127
+ * `RefreshTokenService.getTokensValidAfter` reads it — a table without the
128
+ * column has no watermark rather than an error on every request.
129
+ */
130
+ getAccountAccessState(uid: string, sessionId?: string): Promise<AccountAccessState | null>;
131
+ /**
132
+ * Whether the users table has `disabled_at`. `ensureAuthTablesExist` adds
133
+ * it to every auth table at boot, but the app's own drizzle object may
134
+ * predate it, so the database is asked — once — rather than the object.
135
+ */
136
+ private disabledColumn?;
137
+ private hasDisabledColumn;
138
+ /**
139
+ * Whether the users table has the address-change columns. Added at boot to
140
+ * every auth table, but the app's drizzle object never declares them, so
141
+ * the database is asked — once — as for `disabled_at`.
142
+ */
143
+ private emailChangeColumns?;
144
+ private hasEmailChangeColumns;
145
+ private requireEmailChangeColumns;
146
+ /** @see UserRepository.setPendingEmailChange */
147
+ setPendingEmailChange(uid: string, change: {
148
+ email: string;
149
+ tokenHash: string;
150
+ } | null): Promise<void>;
151
+ /** @see UserRepository.getPendingEmailChange */
152
+ getPendingEmailChange(uid: string): Promise<PendingEmailChange | null>;
153
+ /** @see UserRepository.findPendingEmailChange */
154
+ findPendingEmailChange(tokenHash: string): Promise<{
155
+ user: UserData;
156
+ change: PendingEmailChange;
157
+ } | null>;
158
+ /** @see UserRepository.applyPendingEmailChange — one write, held to the token. */
159
+ applyPendingEmailChange(uid: string, tokenHash: string): Promise<UserData | null>;
160
+ /** @see UserRepository.setUserDisabled */
161
+ setUserDisabled(uid: string, disabled: boolean): Promise<void>;
119
162
  /**
120
163
  * Get user with their roles
121
164
  */
122
165
  getUserWithRoles(uid: string): Promise<{
123
166
  user: UserData;
124
- roles: Role[];
167
+ roles: string[];
125
168
  } | null>;
126
169
  }
127
170
  export declare class RefreshTokenService {
@@ -191,6 +234,12 @@ export declare class RefreshTokenService {
191
234
  deleteAllForUser(uid: string): Promise<void>;
192
235
  listForUser(uid: string): Promise<RefreshTokenInfo[]>;
193
236
  deleteById(id: string, uid: string): Promise<void>;
237
+ /**
238
+ * Every token past its expiry, whoever holds it. `prune` clears a user's
239
+ * expired rows when that user refreshes; a session nobody comes back to
240
+ * is only ever cleared here.
241
+ */
242
+ deleteExpired(): Promise<void>;
194
243
  }
195
244
  /**
196
245
  * Password reset token service
@@ -235,6 +284,8 @@ export declare class MagicLinkTokenService {
235
284
  private getQualifiedTableName;
236
285
  createToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
237
286
  findValidByHash(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
287
+ /** Links and email codes past their expiry, used or not. */
288
+ deleteExpired(): Promise<void>;
238
289
  markAsUsed(tokenHash: string): Promise<void>;
239
290
  }
240
291
  /**
@@ -262,6 +313,10 @@ export declare class PostgresTokenRepository implements TokenRepository {
262
313
  findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null>;
263
314
  markPasswordResetTokenUsed(tokenHash: string): Promise<void>;
264
315
  deleteAllPasswordResetTokensForUser(uid: string): Promise<void>;
316
+ /**
317
+ * @see TokenRepository.deleteExpiredTokens — reset links, magic links and
318
+ * email codes, and refresh tokens, each past its own expiry.
319
+ */
265
320
  deleteExpiredTokens(): Promise<void>;
266
321
  createMagicLinkToken(uid: string, tokenHash: string, expiresAt: Date): Promise<void>;
267
322
  findValidMagicLinkToken(tokenHash: string): Promise<MagicLinkTokenInfo | null>;
@@ -288,23 +343,30 @@ export declare class PostgresAuthRepository implements AuthRepository {
288
343
  deleteUser(id: string): Promise<void>;
289
344
  listUsers(): Promise<UserData[]>;
290
345
  listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult>;
346
+ firstUserIds(limit: number): Promise<string[]>;
291
347
  updatePassword(id: string, passwordHash: string | null): Promise<void>;
292
348
  setEmailVerified(id: string, verified: boolean): Promise<void>;
293
349
  setVerificationToken(id: string, token: string | null): Promise<void>;
294
350
  getUserByVerificationToken(token: string): Promise<UserData | null>;
295
- getUserRoles(uid: string): Promise<RoleData[]>;
296
351
  getUserRoleIds(uid: string): Promise<string[]>;
297
352
  setUserRoles(uid: string, roleIds: string[]): Promise<void>;
298
353
  assignDefaultRole(uid: string, roleId: string): Promise<void>;
299
354
  getUserWithRoles(uid: string): Promise<{
300
355
  user: UserData;
301
- roles: RoleData[];
356
+ roles: string[];
357
+ } | null>;
358
+ getAccountAccessState(uid: string, sessionId?: string): Promise<AccountAccessState | null>;
359
+ setUserDisabled(uid: string, disabled: boolean): Promise<void>;
360
+ setPendingEmailChange(uid: string, change: {
361
+ email: string;
362
+ tokenHash: string;
363
+ } | null): Promise<void>;
364
+ getPendingEmailChange(uid: string): Promise<PendingEmailChange | null>;
365
+ findPendingEmailChange(tokenHash: string): Promise<{
366
+ user: UserData;
367
+ change: PendingEmailChange;
302
368
  } | null>;
303
- getRoleById(id: string): Promise<RoleData | null>;
304
- listRoles(): Promise<RoleData[]>;
305
- createRole(_data: CreateRoleData): Promise<RoleData>;
306
- updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null>;
307
- deleteRole(_id: string): Promise<void>;
369
+ applyPendingEmailChange(uid: string, tokenHash: string): Promise<UserData | null>;
308
370
  createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<boolean>;
309
371
  markRefreshTokenRotated(tokenHash: string): Promise<void>;
310
372
  revokeRefreshTokenSession(sessionId: string): Promise<void>;
@@ -383,6 +445,8 @@ export declare class MfaService implements MfaRepository {
383
445
  * share a single increment.
384
446
  */
385
447
  recordMfaChallengeAttempt(challengeId: string): Promise<number>;
448
+ /** Challenges past their five minutes, answered or not. */
449
+ deleteExpiredChallenges(): Promise<void>;
386
450
  verifyMfaChallenge(challengeId: string): Promise<void>;
387
451
  createRecoveryCodes(uid: string, codeHashes: string[]): Promise<void>;
388
452
  useRecoveryCode(uid: string, codeHash: string): Promise<boolean>;
@@ -62,6 +62,22 @@ var AUTH_USERS_COLUMNS = [
62
62
  column: "tokens_valid_after",
63
63
  type: "TIMESTAMP WITH TIME ZONE"
64
64
  },
65
+ {
66
+ column: "disabled_at",
67
+ type: "TIMESTAMP WITH TIME ZONE"
68
+ },
69
+ {
70
+ column: "pending_email",
71
+ type: "TEXT"
72
+ },
73
+ {
74
+ column: "email_change_token",
75
+ type: "TEXT"
76
+ },
77
+ {
78
+ column: "email_change_sent_at",
79
+ type: "TIMESTAMP WITH TIME ZONE"
80
+ },
65
81
  {
66
82
  column: "created_at",
67
83
  type: "TIMESTAMP WITH TIME ZONE",
@@ -113,4 +129,4 @@ function isAuthCollection(collection) {
113
129
  //#endregion
114
130
  export { isAuthCollection as i, authUsersColumnDefinition as n, authUsersColumnSql as r, AUTH_USERS_COLUMNS as t };
115
131
 
116
- //# sourceMappingURL=auth-users-columns-D2LBFrMH.js.map
132
+ //# sourceMappingURL=auth-users-columns-C72EMoDJ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-users-columns-D2LBFrMH.js","names":[],"sources":["../src/schema/auth-users-columns.ts"],"sourcesContent":["/**\n * The one description of what an auth user table's columns must be.\n *\n * ## Why this exists\n *\n * Three different code paths create `rebase.users`, and before this module they\n * disagreed about it:\n *\n * | column | `db push` (generator) | `ensureAuthTablesExist` | `ensureCollectionTables` |\n * |------------------|-----------------------|-------------------------|--------------------------|\n * | `email` | `TEXT UNIQUE NOT NULL`| `TEXT NOT NULL` + CHECK | `TEXT` (nullable) |\n * | `roles` | `TEXT[]` | `NOT NULL DEFAULT '{}'` | nullable, no default |\n * | `email_verified` | `BOOLEAN` | `NOT NULL DEFAULT FALSE`| nullable |\n * | `created_at` | `DEFAULT now()` | `NOT NULL DEFAULT NOW()`| no default |\n *\n * They are all `CREATE TABLE IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`, so\n * whichever ran first decided the table and the other two silently no-op'd.\n * Boot order therefore chose the constraints: a managed deploy (collection\n * tables first) got a users table whose `email` was nullable and whose\n * `email_verified` had no default, while the same project pushed from a\n * checkout got the strict one. In the same table, `is_anonymous` came out\n * `NOT NULL DEFAULT false` — because *that* column existed in only one of the\n * three lists, so its owner's definition won by default.\n *\n * A second consequence was drift in the other direction. The scaffold's\n * `users.ts` describes 12 columns; auth needs 14. `db push` is declarative and\n * builds its desired state from the collection alone, so the two columns only\n * auth knows about (`is_anonymous`, `tokens_valid_after`) read as unmanaged\n * drift, and a push run after the server had booted once planned to DROP them.\n *\n * Both problems are the same problem: no single place said what this table is.\n * This is that place. Every creator asks here first and only then falls back to\n * the collection's own property definitions, so a column auth owns has one\n * definition regardless of who gets there first, and a column the developer\n * added to their users collection still behaves like any other field.\n *\n * ## What belongs here\n *\n * Only columns the auth services read or write. A developer's own additions to\n * their users collection (`bio`, `stripe_customer_id`) are ordinary columns and\n * must NOT be listed — they are generated from the collection like every other\n * field, and listing them here would freeze a user's schema.\n *\n * `id` is deliberately absent: its type comes from the collection's id property\n * (uuid / increment / text) and each creator already derives it.\n *\n * Keep in step with {@link AUTH_SCHEMA_VERSION} when a change here makes an\n * older runtime unable to work against a migrated table.\n */\n\n/**\n * A column auth owns.\n *\n * Structured rather than one SQL string because the same facts are needed in\n * three grammars — a `CREATE TABLE` column list, an `ADD COLUMN IF NOT EXISTS`,\n * and a pair of `ALTER COLUMN … SET DEFAULT` / `SET NOT NULL` reconciles for a\n * table that already exists with the wrong shape. Parsing a string back apart\n * for the third of those is how the copies drifted in the first place.\n */\nexport interface AuthUsersColumn {\n /** Physical column name. */\n column: string;\n /** Postgres type. */\n type: string;\n /** Default expression, verbatim, or absent for no default. */\n default?: string;\n /** Whether the column is NOT NULL. */\n notNull?: boolean;\n}\n\n/**\n * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it\n * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.\n * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added\n * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted\n * table that already holds a longer row.\n */\nexport const AUTH_USERS_COLUMNS: readonly AuthUsersColumn[] = [\n { column: \"email\", type: \"TEXT\", notNull: true },\n { column: \"display_name\", type: \"TEXT\" },\n { column: \"photo_url\", type: \"TEXT\" },\n { column: \"roles\", type: \"TEXT[]\", default: \"'{}'\", notNull: true },\n { column: \"password_hash\", type: \"TEXT\" },\n { column: \"email_verified\", type: \"BOOLEAN\", default: \"FALSE\", notNull: true },\n { column: \"email_verification_token\", type: \"TEXT\" },\n { column: \"email_verification_sent_at\", type: \"TIMESTAMP WITH TIME ZONE\" },\n { column: \"is_anonymous\", type: \"BOOLEAN\", default: \"FALSE\", notNull: true },\n { column: \"metadata\", type: \"JSONB\", default: \"'{}'\", notNull: true },\n { column: \"tokens_valid_after\", type: \"TIMESTAMP WITH TIME ZONE\" },\n { column: \"created_at\", type: \"TIMESTAMP WITH TIME ZONE\", default: \"NOW()\", notNull: true },\n { column: \"updated_at\", type: \"TIMESTAMP WITH TIME ZONE\", default: \"NOW()\", notNull: true }\n];\n\nconst BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map(c => [c.column, c]));\n\n/** Type + inline constraints, as they appear after the column name. */\nexport function authUsersColumnSql(spec: AuthUsersColumn): string {\n return [\n spec.type,\n spec.default !== undefined ? `DEFAULT ${spec.default}` : \"\",\n spec.notNull ? \"NOT NULL\" : \"\"\n ].filter(Boolean).join(\" \");\n}\n\n/**\n * The auth-owned definition for a physical column name, or `undefined` when\n * auth does not own it.\n *\n * Callers pass the RESOLVED column name (after `columnName` mapping), because\n * that is the only name the three creators agree on: the scaffold's users\n * collection spells the property `displayName` and the column `display_name`.\n */\nexport function authUsersColumnDefinition(column: string): string | undefined {\n const spec = BY_COLUMN.get(column);\n return spec ? authUsersColumnSql(spec) : undefined;\n}\n\n/**\n * Whether a collection is an auth collection, i.e. whether the definitions in\n * this module apply to its table at all.\n *\n * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:\n * that one takes a `CollectionConfig`, this one is called from DDL code paths\n * that hold looser objects, and both spellings must accept `auth: true` as well\n * as `auth: { enabled: true }`.\n */\nexport function isAuthCollection(collection: unknown): boolean {\n const auth = (collection as { auth?: unknown } | undefined)?.auth;\n if (auth === true) return true;\n return typeof auth === \"object\" && auth !== null && (auth as { enabled?: unknown }).enabled === true;\n}\n"],"mappings":";;;;;;;;;;AA6EA,IAAa,qBAAiD;CAC1D;EAAE,QAAQ;EAAS,MAAM;EAAQ,SAAS;CAAK;CAC/C;EAAE,QAAQ;EAAgB,MAAM;CAAO;CACvC;EAAE,QAAQ;EAAa,MAAM;CAAO;CACpC;EAAE,QAAQ;EAAS,MAAM;EAAU,SAAS;EAAQ,SAAS;CAAK;CAClE;EAAE,QAAQ;EAAiB,MAAM;CAAO;CACxC;EAAE,QAAQ;EAAkB,MAAM;EAAW,SAAS;EAAS,SAAS;CAAK;CAC7E;EAAE,QAAQ;EAA4B,MAAM;CAAO;CACnD;EAAE,QAAQ;EAA8B,MAAM;CAA2B;CACzE;EAAE,QAAQ;EAAgB,MAAM;EAAW,SAAS;EAAS,SAAS;CAAK;CAC3E;EAAE,QAAQ;EAAY,MAAM;EAAS,SAAS;EAAQ,SAAS;CAAK;CACpE;EAAE,QAAQ;EAAsB,MAAM;CAA2B;CACjE;EAAE,QAAQ;EAAc,MAAM;EAA4B,SAAS;EAAS,SAAS;CAAK;CAC1F;EAAE,QAAQ;EAAc,MAAM;EAA4B,SAAS;EAAS,SAAS;CAAK;AAC9F;AAEA,IAAM,YAAY,IAAI,IAAI,mBAAmB,KAAI,MAAK,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;;AAGpE,SAAgB,mBAAmB,MAA+B;CAC9D,OAAO;EACH,KAAK;EACL,KAAK,YAAY,KAAA,IAAY,WAAW,KAAK,YAAY;EACzD,KAAK,UAAU,aAAa;CAChC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;AAC9B;;;;;;;;;AAUA,SAAgB,0BAA0B,QAAoC;CAC1E,MAAM,OAAO,UAAU,IAAI,MAAM;CACjC,OAAO,OAAO,mBAAmB,IAAI,IAAI,KAAA;AAC7C;;;;;;;;;;AAWA,SAAgB,iBAAiB,YAA8B;CAC3D,MAAM,OAAQ,YAA+C;CAC7D,IAAI,SAAS,MAAM,OAAO;CAC1B,OAAO,OAAO,SAAS,YAAY,SAAS,QAAS,KAA+B,YAAY;AACpG"}
1
+ {"version":3,"file":"auth-users-columns-C72EMoDJ.js","names":[],"sources":["../src/schema/auth-users-columns.ts"],"sourcesContent":["/**\n * The one description of what an auth user table's columns must be.\n *\n * ## Why this exists\n *\n * Three different code paths create `rebase.users`, and before this module they\n * disagreed about it:\n *\n * | column | `db push` (generator) | `ensureAuthTablesExist` | `ensureCollectionTables` |\n * |------------------|-----------------------|-------------------------|--------------------------|\n * | `email` | `TEXT UNIQUE NOT NULL`| `TEXT NOT NULL` + CHECK | `TEXT` (nullable) |\n * | `roles` | `TEXT[]` | `NOT NULL DEFAULT '{}'` | nullable, no default |\n * | `email_verified` | `BOOLEAN` | `NOT NULL DEFAULT FALSE`| nullable |\n * | `created_at` | `DEFAULT now()` | `NOT NULL DEFAULT NOW()`| no default |\n *\n * They are all `CREATE TABLE IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`, so\n * whichever ran first decided the table and the other two silently no-op'd.\n * Boot order therefore chose the constraints: a managed deploy (collection\n * tables first) got a users table whose `email` was nullable and whose\n * `email_verified` had no default, while the same project pushed from a\n * checkout got the strict one. In the same table, `is_anonymous` came out\n * `NOT NULL DEFAULT false` — because *that* column existed in only one of the\n * three lists, so its owner's definition won by default.\n *\n * A second consequence was drift in the other direction. The scaffold's\n * `users.ts` describes 12 columns; auth needs 14. `db push` is declarative and\n * builds its desired state from the collection alone, so the two columns only\n * auth knows about (`is_anonymous`, `tokens_valid_after`) read as unmanaged\n * drift, and a push run after the server had booted once planned to DROP them.\n *\n * Both problems are the same problem: no single place said what this table is.\n * This is that place. Every creator asks here first and only then falls back to\n * the collection's own property definitions, so a column auth owns has one\n * definition regardless of who gets there first, and a column the developer\n * added to their users collection still behaves like any other field.\n *\n * ## What belongs here\n *\n * Only columns the auth services read or write. A developer's own additions to\n * their users collection (`bio`, `stripe_customer_id`) are ordinary columns and\n * must NOT be listed — they are generated from the collection like every other\n * field, and listing them here would freeze a user's schema.\n *\n * `id` is deliberately absent: its type comes from the collection's id property\n * (uuid / increment / text) and each creator already derives it.\n *\n * Keep in step with {@link AUTH_SCHEMA_VERSION} when a change here makes an\n * older runtime unable to work against a migrated table.\n */\n\n/**\n * A column auth owns.\n *\n * Structured rather than one SQL string because the same facts are needed in\n * three grammars — a `CREATE TABLE` column list, an `ADD COLUMN IF NOT EXISTS`,\n * and a pair of `ALTER COLUMN … SET DEFAULT` / `SET NOT NULL` reconciles for a\n * table that already exists with the wrong shape. Parsing a string back apart\n * for the third of those is how the copies drifted in the first place.\n */\nexport interface AuthUsersColumn {\n /** Physical column name. */\n column: string;\n /** Postgres type. */\n type: string;\n /** Default expression, verbatim, or absent for no default. */\n default?: string;\n /** Whether the column is NOT NULL. */\n notNull?: boolean;\n}\n\n/**\n * `email` is NOT NULL on purpose, and the anonymous sign-in route depends on it\n * — it synthesizes `anon_<32 hex>@anonymous.local` rather than inserting NULL.\n * The 320-char bound (RFC 5321) is a CHECK rather than a `VARCHAR(n)`, added\n * separately by `ensureAuthTablesExist` so it can be `NOT VALID` on an adopted\n * table that already holds a longer row.\n */\nexport const AUTH_USERS_COLUMNS: readonly AuthUsersColumn[] = [\n { column: \"email\", type: \"TEXT\", notNull: true },\n { column: \"display_name\", type: \"TEXT\" },\n { column: \"photo_url\", type: \"TEXT\" },\n { column: \"roles\", type: \"TEXT[]\", default: \"'{}'\", notNull: true },\n { column: \"password_hash\", type: \"TEXT\" },\n { column: \"email_verified\", type: \"BOOLEAN\", default: \"FALSE\", notNull: true },\n { column: \"email_verification_token\", type: \"TEXT\" },\n { column: \"email_verification_sent_at\", type: \"TIMESTAMP WITH TIME ZONE\" },\n { column: \"is_anonymous\", type: \"BOOLEAN\", default: \"FALSE\", notNull: true },\n { column: \"metadata\", type: \"JSONB\", default: \"'{}'\", notNull: true },\n { column: \"tokens_valid_after\", type: \"TIMESTAMP WITH TIME ZONE\" },\n // When an administrator switched the account off; NULL while it is on.\n { column: \"disabled_at\", type: \"TIMESTAMP WITH TIME ZONE\" },\n // A self-service address change waiting for its link: the address, the\n // hash of the token mailed to it, and when (it lives 24 hours).\n { column: \"pending_email\", type: \"TEXT\" },\n { column: \"email_change_token\", type: \"TEXT\" },\n { column: \"email_change_sent_at\", type: \"TIMESTAMP WITH TIME ZONE\" },\n { column: \"created_at\", type: \"TIMESTAMP WITH TIME ZONE\", default: \"NOW()\", notNull: true },\n { column: \"updated_at\", type: \"TIMESTAMP WITH TIME ZONE\", default: \"NOW()\", notNull: true }\n];\n\nconst BY_COLUMN = new Map(AUTH_USERS_COLUMNS.map(c => [c.column, c]));\n\n/** Type + inline constraints, as they appear after the column name. */\nexport function authUsersColumnSql(spec: AuthUsersColumn): string {\n return [\n spec.type,\n spec.default !== undefined ? `DEFAULT ${spec.default}` : \"\",\n spec.notNull ? \"NOT NULL\" : \"\"\n ].filter(Boolean).join(\" \");\n}\n\n/**\n * The auth-owned definition for a physical column name, or `undefined` when\n * auth does not own it.\n *\n * Callers pass the RESOLVED column name (after `columnName` mapping), because\n * that is the only name the three creators agree on: the scaffold's users\n * collection spells the property `displayName` and the column `display_name`.\n */\nexport function authUsersColumnDefinition(column: string): string | undefined {\n const spec = BY_COLUMN.get(column);\n return spec ? authUsersColumnSql(spec) : undefined;\n}\n\n/**\n * Whether a collection is an auth collection, i.e. whether the definitions in\n * this module apply to its table at all.\n *\n * Duplicated in shape from `@rebasepro/common`'s policy defaults on purpose:\n * that one takes a `CollectionConfig`, this one is called from DDL code paths\n * that hold looser objects, and both spellings must accept `auth: true` as well\n * as `auth: { enabled: true }`.\n */\nexport function isAuthCollection(collection: unknown): boolean {\n const auth = (collection as { auth?: unknown } | undefined)?.auth;\n if (auth === true) return true;\n return typeof auth === \"object\" && auth !== null && (auth as { enabled?: unknown }).enabled === true;\n}\n"],"mappings":";;;;;;;;;;AA6EA,IAAa,qBAAiD;CAC1D;EAAE,QAAQ;EAAS,MAAM;EAAQ,SAAS;CAAK;CAC/C;EAAE,QAAQ;EAAgB,MAAM;CAAO;CACvC;EAAE,QAAQ;EAAa,MAAM;CAAO;CACpC;EAAE,QAAQ;EAAS,MAAM;EAAU,SAAS;EAAQ,SAAS;CAAK;CAClE;EAAE,QAAQ;EAAiB,MAAM;CAAO;CACxC;EAAE,QAAQ;EAAkB,MAAM;EAAW,SAAS;EAAS,SAAS;CAAK;CAC7E;EAAE,QAAQ;EAA4B,MAAM;CAAO;CACnD;EAAE,QAAQ;EAA8B,MAAM;CAA2B;CACzE;EAAE,QAAQ;EAAgB,MAAM;EAAW,SAAS;EAAS,SAAS;CAAK;CAC3E;EAAE,QAAQ;EAAY,MAAM;EAAS,SAAS;EAAQ,SAAS;CAAK;CACpE;EAAE,QAAQ;EAAsB,MAAM;CAA2B;CAEjE;EAAE,QAAQ;EAAe,MAAM;CAA2B;CAG1D;EAAE,QAAQ;EAAiB,MAAM;CAAO;CACxC;EAAE,QAAQ;EAAsB,MAAM;CAAO;CAC7C;EAAE,QAAQ;EAAwB,MAAM;CAA2B;CACnE;EAAE,QAAQ;EAAc,MAAM;EAA4B,SAAS;EAAS,SAAS;CAAK;CAC1F;EAAE,QAAQ;EAAc,MAAM;EAA4B,SAAS;EAAS,SAAS;CAAK;AAC9F;AAEA,IAAM,YAAY,IAAI,IAAI,mBAAmB,KAAI,MAAK,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;;AAGpE,SAAgB,mBAAmB,MAA+B;CAC9D,OAAO;EACH,KAAK;EACL,KAAK,YAAY,KAAA,IAAY,WAAW,KAAK,YAAY;EACzD,KAAK,UAAU,aAAa;CAChC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;AAC9B;;;;;;;;;AAUA,SAAgB,0BAA0B,QAAoC;CAC1E,MAAM,OAAO,UAAU,IAAI,MAAM;CACjC,OAAO,OAAO,mBAAmB,IAAI,IAAI,KAAA;AAC7C;;;;;;;;;;AAWA,SAAgB,iBAAiB,YAA8B;CAC3D,MAAM,OAAQ,YAA+C;CAC7D,IAAI,SAAS,MAAM,OAAO;CAC1B,OAAO,OAAO,SAAS,YAAY,SAAS,QAAS,KAA+B,YAAY;AACpG"}
@@ -4,6 +4,20 @@
4
4
  * backup surface stays self-contained.
5
5
  */
6
6
  import arg from "arg";
7
+ import type { StorageController } from "@rebasepro/server";
8
+ import { BackupDestination } from "./pg-tools.js";
9
+ /**
10
+ * The controller for an object-storage destination, or `null` for a local one.
11
+ *
12
+ * Built by `backupStorageFromEnv` from `@rebasepro/server` — the function the
13
+ * Backups panel's routes use — so the CLI and the panel read a bucket the same
14
+ * way. This file had its own copy, and the two had already drifted once
15
+ * (`S3_FORCE_PATH_STYLE` read as `=== "true"`, so a MinIO backup addressed the
16
+ * bucket as a host).
17
+ *
18
+ * Exported for its test.
19
+ */
20
+ export declare function resolveStorageForDestination(dest: BackupDestination, env: Record<string, string | undefined>): Promise<StorageController | null>;
7
21
  export declare function backupCommand(rawArgs: string[]): Promise<void>;
8
22
  /** Parse a `rebase db restore …` line; the first positional is the backup. */
9
23
  export declare function parseRestoreLine(rawArgs: string[]): arg.Result<{
@@ -1,4 +1,5 @@
1
1
  import type { StorageController } from "@rebasepro/server";
2
+ import { resolvePgBinary } from "./pg-binary.js";
2
3
  import { BackupDestination, type RowSecurityIdentity, VersionCompatibility } from "./pg-tools.js";
3
4
  import { BackupObject, RetentionOptions } from "./retention.js";
4
5
  /**
@@ -11,8 +12,7 @@ export declare class BackupToolError extends Error {
11
12
  readonly hint?: string | undefined;
12
13
  constructor(message: string, hint?: string | undefined);
13
14
  }
14
- /** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
15
- export declare function resolvePgBinary(tool: "pg_dump" | "pg_restore" | "pg_dumpall", env?: Record<string, string | undefined>): string | null;
15
+ export { resolvePgBinary };
16
16
  /** Run `<bin> --version` and extract the major version. */
17
17
  export declare function detectToolMajor(bin: string): Promise<number | null>;
18
18
  /** Query the server for its major version via `server_version_num`. */
@@ -0,0 +1,5 @@
1
+ export type PgTool = "pg_dump" | "pg_restore" | "pg_dumpall";
2
+ /** The variable that points at `tool` when it is not on the PATH. */
3
+ export declare function pgToolVariable(tool: PgTool): string;
4
+ /** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
5
+ export declare function resolvePgBinary(tool: PgTool, env?: Record<string, string | undefined>): string | null;
@@ -1,15 +1,15 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { a as __toESM, t as require___vite_browser_external } from "./__vite-browser-external-BnuHet1e.js";
4
- import { n as outError, r as outWarn, t as out } from "./cli-output-CNdMql-L.js";
5
- import { D as parseBackupDestination, P as withDatabaseName, T as globalsFileForDump, h as validateDump, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as listBackups, m as uploadBackup, n as applyGlobals, o as discardPartialDump, p as restoreDump, s as ensureDatabaseExists, t as BackupToolError, u as preflight } from "./backup-service-HQ9GC4tN.js";
6
- import { t as chalk } from "./source-Br7L7GOI.js";
4
+ import { a as outWarn, i as outError, r as out, t as chalk } from "./source-DBDbBTL4.js";
5
+ import { A as parseDbNameFromUrl, E as globalsFileForDump, F as withDatabaseName, M as resolveConnectionString, O as parseBackupDestination, f as restoreDump, i as createDump, l as listBackups, m as validateDump, n as applyGlobals, o as discardPartialDump, p as uploadBackup, s as ensureDatabaseExists, t as BackupToolError, u as preflight } from "./backup-service-BTBB7RXn.js";
7
6
  import { t as require_arg } from "./arg-Dni7MzLB.js";
8
- import { r as parseDriverLine } from "./cli-flags-BglvpjHv.js";
9
- import { parseEnvBoolean } from "@rebasepro/types";
10
- import fs from "fs";
7
+ import { r as parseDriverLine } from "./cli-flags-B-Dawocl.js";
11
8
  import path from "path";
9
+ import fs from "fs";
12
10
  import os from "os";
11
+ import { Readable } from "stream";
12
+ import { pipeline } from "stream/promises";
13
13
  //#region src/backup/backup-cli.ts
14
14
  var import_arg = /* @__PURE__ */ __toESM(require_arg(), 1);
15
15
  var import___vite_browser_external = /* @__PURE__ */ __toESM(require___vite_browser_external(), 1);
@@ -19,30 +19,38 @@ function formatBytes(bytes) {
19
19
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
20
20
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
21
21
  }
22
+ /** A web stream's chunks, for piping into a file. */
23
+ async function* webChunks(stream) {
24
+ const reader = stream.getReader();
25
+ try {
26
+ for (;;) {
27
+ const { done, value } = await reader.read();
28
+ if (done) return;
29
+ yield value;
30
+ }
31
+ } finally {
32
+ reader.releaseLock();
33
+ }
34
+ }
22
35
  /**
23
- * Build a StorageController for an object-storage destination from the same
24
- * `S3_*` env vars the backend uses. Returns `null` for local destinations.
36
+ * The controller for an object-storage destination, or `null` for a local one.
37
+ *
38
+ * Built by `backupStorageFromEnv` from `@rebasepro/server` — the function the
39
+ * Backups panel's routes use — so the CLI and the panel read a bucket the same
40
+ * way. This file had its own copy, and the two had already drifted once
41
+ * (`S3_FORCE_PATH_STYLE` read as `=== "true"`, so a MinIO backup addressed the
42
+ * bucket as a host).
43
+ *
44
+ * Exported for its test.
25
45
  */
26
46
  async function resolveStorageForDestination(dest, env) {
27
47
  if (dest.kind === "local") return null;
28
- if (dest.kind === "gcs") {
29
- const { GCSStorageController } = await import("@rebasepro/server");
30
- return new GCSStorageController({
31
- type: "gcs",
32
- bucket: dest.bucket
33
- });
48
+ const { backupStorageFromEnv } = await import("@rebasepro/server");
49
+ try {
50
+ return backupStorageFromEnv(dest, env);
51
+ } catch (error) {
52
+ throw new BackupToolError(error instanceof Error ? error.message : String(error), "Set the same S3_* variables your backend uses for storage.");
34
53
  }
35
- if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) throw new BackupToolError("S3 destination requires S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY in the environment.", "Set the same S3_* variables your backend uses for storage.");
36
- const { S3StorageController } = await import("@rebasepro/server");
37
- return new S3StorageController({
38
- type: "s3",
39
- bucket: dest.bucket,
40
- region: env.S3_REGION || "auto",
41
- accessKeyId: env.S3_ACCESS_KEY_ID,
42
- secretAccessKey: env.S3_SECRET_ACCESS_KEY,
43
- endpoint: env.S3_ENDPOINT,
44
- forcePathStyle: parseEnvBoolean(env.S3_FORCE_PATH_STYLE)
45
- });
46
54
  }
47
55
  function requireConnection() {
48
56
  const conn = resolveConnectionString(process.env);
@@ -196,12 +204,12 @@ async function restoreCommand(rawArgs) {
196
204
  if (!storage) throw new BackupToolError("Could not resolve storage for the given URL.");
197
205
  const key = backupArg.replace(/^(s3|gs):\/\/[^/]+\//, "");
198
206
  const bucket = backupArg.replace(/^(s3|gs):\/\/([^/]+)\/.*$/, "$2");
199
- const file = await storage.getObject(key, bucket);
200
- if (!file) throw new BackupToolError(`Backup not found in storage: ${backupArg}`);
201
207
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "rebase-restore-"));
202
208
  localFile = path.join(tmpDir, path.basename(key));
203
- fs.writeFileSync(localFile, Buffer.from(await file.arrayBuffer()));
204
209
  cleanupTemp = true;
210
+ const object = storage.getObjectStream ? await storage.getObjectStream(key, { bucket }) : await storage.getObject(key, bucket).then((file) => file && { stream: file.stream() });
211
+ if (!object) throw new BackupToolError(`Backup not found in storage: ${backupArg}`);
212
+ await pipeline(Readable.from(webChunks(object.stream)), fs.createWriteStream(localFile));
205
213
  const globalsObj = await storage.getObject(globalsFileForDump(key), bucket);
206
214
  if (globalsObj) globalsSql = Buffer.from(await globalsObj.arrayBuffer()).toString("utf-8");
207
215
  } else {
@@ -409,4 +417,4 @@ ${chalk.green.bold("Usage")}
409
417
  //#endregion
410
418
  export { backupCommand, backupsCommand, restoreCommand };
411
419
 
412
- //# sourceMappingURL=backup-cli-24v4OlSp.js.map
420
+ //# sourceMappingURL=backup-cli-aUfRv16h.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-cli-aUfRv16h.js","names":[],"sources":["../src/backup/backup-cli.ts"],"sourcesContent":["/**\n * CLI handlers for `rebase db backup`, `rebase db restore`, and\n * `rebase db backups list`. Kept out of the main `cli.ts` dispatcher so the\n * backup surface stays self-contained.\n */\nimport arg from \"arg\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport os from \"os\";\nimport readline from \"readline\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport chalk from \"chalk\";\n// Aliased: `out` is already a local in two of the commands below (the `--out`\n// backup destination).\nimport { out as print, outWarn, outError } from \"../cli-output\";\nimport { parseDriverLine } from \"../cli-flags\";\nimport type { StorageController } from \"@rebasepro/server\";\nimport {\n BackupDestination,\n globalsFileForDump,\n parseBackupDestination,\n parseDbNameFromUrl,\n resolveConnectionString,\n withDatabaseName\n} from \"./pg-tools\";\nimport {\n applyGlobals,\n BackupToolError,\n createDump,\n discardPartialDump,\n ensureDatabaseExists,\n listBackups,\n preflight,\n restoreDump,\n uploadBackup,\n validateDump\n} from \"./backup-service\";\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;\n}\n\n/** A web stream's chunks, for piping into a file. */\nasync function* webChunks(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {\n const reader = stream.getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) return;\n yield value;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * The controller for an object-storage destination, or `null` for a local one.\n *\n * Built by `backupStorageFromEnv` from `@rebasepro/server` — the function the\n * Backups panel's routes use — so the CLI and the panel read a bucket the same\n * way. This file had its own copy, and the two had already drifted once\n * (`S3_FORCE_PATH_STYLE` read as `=== \"true\"`, so a MinIO backup addressed the\n * bucket as a host).\n *\n * Exported for its test.\n */\nexport async function resolveStorageForDestination(\n dest: BackupDestination,\n env: Record<string, string | undefined>\n): Promise<StorageController | null> {\n if (dest.kind === \"local\") return null;\n const { backupStorageFromEnv } = await import(\"@rebasepro/server\");\n try {\n return backupStorageFromEnv(dest, env);\n } catch (error) {\n throw new BackupToolError(\n error instanceof Error ? error.message : String(error),\n \"Set the same S3_* variables your backend uses for storage.\"\n );\n }\n}\n\nfunction requireConnection(): string {\n const conn = resolveConnectionString(process.env);\n if (!conn) {\n outError(chalk.red(\"✗ DATABASE_URL is not set. Make sure your .env file is configured.\"));\n process.exit(1);\n }\n return conn;\n}\n\nasync function promptConfirm(question: string): Promise<boolean> {\n // Non-interactive shells (CI, pipes) can't answer — treat as \"no\".\n if (!process.stdin.isTTY) return false;\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n try {\n const answer: string = await new Promise((resolve) => rl.question(question, resolve));\n return /^y(es)?$/i.test(answer.trim());\n } finally {\n rl.close();\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db backup\n// ─────────────────────────────────────────────────────────────────────────\nexport async function backupCommand(rawArgs: string[]): Promise<void> {\n const args = arg(\n {\n \"--out\": String,\n // The same alias `build` and `cloud env pull` carry. `--output` is\n // the canonical spelling in this CLI and this command took only\n // `--out`, so `rebase db backup --output ./backups` wrote to\n // ./backups *and* to the default directory — the flag was ignored\n // and its value became a positional.\n \"--output\": \"--out\",\n \"--exclude-schema\": [String],\n \"--no-owner\": Boolean,\n \"--enable-row-security\": Boolean,\n \"--row-security-role\": String,\n \"-o\": \"--out\"\n },\n { argv: rawArgs.slice(2), permissive: true }\n );\n\n if (args._.includes(\"--help\") || rawArgs.includes(\"--help\")) {\n printBackupHelp();\n return;\n }\n\n const connectionString = requireConnection();\n const dbName = parseDbNameFromUrl(connectionString) ?? \"database\";\n const out = args[\"--out\"] || process.env.BACKUP_DESTINATION || path.join(process.cwd(), \"backups\");\n const dest = parseBackupDestination(out);\n\n print(\"\");\n print(chalk.bold(\" 💾 Rebase DB Backup\"));\n print(chalk.gray(` Database: ${dbName}`));\n print(chalk.gray(` Destination: ${out}`));\n print(\"\");\n\n // Version pre-flight (doctor-style).\n const pf = await preflight(\"pg_dump\", connectionString);\n if (!pf.compatible) {\n outError(chalk.red(` ✗ ${pf.reason}`));\n process.exit(1);\n }\n print(chalk.gray(` Using pg_dump ${pf.toolMajor} against server ${pf.serverMajor}.`));\n\n // Opt-in, and loud. With row security on, pg_dump stops refusing to read\n // rows it cannot see and starts leaving them out — an exit-0 backup that\n // is quietly short. Anyone choosing that should know they chose it.\n const rowSecurity = args[\"--enable-row-security\"]\n ? { uid: \"rebase-db-backup\", roles: [args[\"--row-security-role\"] || \"admin\"] }\n : undefined;\n\n if (rowSecurity) {\n outWarn(\"\");\n outWarn(chalk.yellow(\" ⚠ Dumping with row-level security ON.\"));\n outWarn(chalk.gray(` Reading as roles [${rowSecurity.roles.join(\", \")}], which satisfies the generated`));\n outWarn(chalk.gray(\" `admin_full_access` policy. This backup contains exactly the rows those\"));\n outWarn(chalk.gray(\" policies admit — a table whose policies have no admin rule comes out short,\"));\n outWarn(chalk.gray(\" and pg_dump will not say so. Prefer granting the dumping role BYPASSRLS.\"));\n outWarn(\"\");\n }\n\n try {\n if (dest.kind === \"local\") {\n // Honour an explicit `…/name.dump` path; otherwise treat it as a\n // directory and auto-name the file.\n const explicitFile = dest.path.endsWith(\".dump\");\n const dump = await createDump({\n connectionString,\n dbName,\n outDir: explicitFile ? path.dirname(dest.path) : dest.path,\n fileName: explicitFile ? path.basename(dest.path) : undefined,\n excludeSchemas: args[\"--exclude-schema\"],\n noOwner: args[\"--no-owner\"],\n inheritStdio: true,\n rowSecurity\n });\n await assertDumpValid(dump.localFile);\n print(\"\");\n print(chalk.green(` ✓ Backup written to ${dump.localFile} (${formatBytes(dump.sizeBytes)})`));\n if (dump.globalsFile) {\n print(chalk.gray(` ✓ Roles captured to ${dump.globalsFile} (needed so RLS survives a restore).`));\n }\n } else {\n const storage = await resolveStorageForDestination(dest, process.env);\n const dump = await createDump({\n connectionString,\n dbName,\n excludeSchemas: args[\"--exclude-schema\"],\n noOwner: args[\"--no-owner\"],\n inheritStdio: true,\n rowSecurity\n });\n try {\n await assertDumpValid(dump.localFile);\n const uploaded = await uploadBackup(storage!, dump.localFile, dest);\n print(\"\");\n print(chalk.green(` ✓ Backup uploaded to ${uploaded.storageUrl} (${formatBytes(dump.sizeBytes)})`));\n // Upload the roles sidecar next to the dump so a restore can\n // recreate roles the dump's GRANT/RLS statements depend on.\n if (dump.globalsFile && fs.existsSync(dump.globalsFile)) {\n const globalsUpload = await uploadBackup(storage!, dump.globalsFile, dest);\n print(chalk.gray(` ✓ Roles uploaded to ${globalsUpload.storageUrl} (needed so RLS survives a restore).`));\n }\n print(chalk.gray(\" Ensure this bucket is private — backups may contain secrets and PII.\"));\n } finally {\n if (fs.existsSync(dump.localFile)) fs.unlinkSync(dump.localFile);\n if (dump.globalsFile && fs.existsSync(dump.globalsFile)) fs.unlinkSync(dump.globalsFile);\n }\n }\n print(\"\");\n } catch (err) {\n reportError(err);\n process.exit(1);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db restore <backup>\n// ─────────────────────────────────────────────────────────────────────────\nconst RESTORE_FLAGS = {\n \"--target-db\": String,\n \"--create-db\": Boolean,\n \"--clean\": Boolean,\n \"--no-owner\": Boolean,\n \"--continue-on-error\": Boolean,\n \"--yes\": Boolean,\n \"-y\": \"--yes\"\n} satisfies arg.Spec;\n\n/** Parse a `rebase db restore …` line; the first positional is the backup. */\nexport function parseRestoreLine(rawArgs: string[]) {\n return parseDriverLine(RESTORE_FLAGS, rawArgs);\n}\n\nexport async function restoreCommand(rawArgs: string[]): Promise<void> {\n const args = parseRestoreLine(rawArgs);\n\n const backupArg = args._[0];\n if (!backupArg || rawArgs.includes(\"--help\")) {\n printRestoreHelp();\n if (!backupArg && !rawArgs.includes(\"--help\")) process.exit(1);\n return;\n }\n\n const baseConnection = requireConnection();\n\n // Choose the target connection: an explicit --target-db (or --create-db's\n // implied fresh db) swaps the database name so the live one isn't clobbered.\n const targetDb = args[\"--target-db\"] ?? parseDbNameFromUrl(baseConnection) ?? undefined;\n const targetConnection = args[\"--target-db\"]\n ? withDatabaseName(baseConnection, args[\"--target-db\"])\n : baseConnection;\n\n print(\"\");\n print(chalk.bold(\" ♻️ Rebase DB Restore\"));\n print(chalk.gray(` Source: ${backupArg}`));\n print(chalk.gray(` Target: ${targetDb ?? \"(from DATABASE_URL)\"}`));\n print(\"\");\n\n // Resolve the local file to restore from (download object-storage keys).\n // Also resolve the `.globals.sql` roles sidecar so cluster roles can be\n // recreated before the restore — without them the dump's GRANT/RLS\n // statements fail and RLS is silently lost.\n let localFile: string;\n let globalsSql: string | null = null;\n let cleanupTemp = false;\n try {\n if (/^(s3|gs):\\/\\//.test(backupArg)) {\n const dest = parseBackupDestination(backupArg.replace(/\\/[^/]+$/, \"\"));\n const storage = await resolveStorageForDestination(dest, process.env);\n if (!storage) throw new BackupToolError(\"Could not resolve storage for the given URL.\");\n const key = backupArg.replace(/^(s3|gs):\\/\\/[^/]+\\//, \"\");\n const bucket = backupArg.replace(/^(s3|gs):\\/\\/([^/]+)\\/.*$/, \"$2\");\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"rebase-restore-\"));\n localFile = path.join(tmpDir, path.basename(key));\n cleanupTemp = true;\n // Streamed to disk: buffered, a dump was held twice in memory and\n // one over 2 GiB could not be written in one call at all.\n const object = storage.getObjectStream\n ? await storage.getObjectStream(key, { bucket })\n : await storage.getObject(key, bucket).then((file) => file && { stream: file.stream() });\n if (!object) throw new BackupToolError(`Backup not found in storage: ${backupArg}`);\n await pipeline(Readable.from(webChunks(object.stream)), fs.createWriteStream(localFile));\n const globalsObj = await storage.getObject(globalsFileForDump(key), bucket);\n if (globalsObj) globalsSql = Buffer.from(await globalsObj.arrayBuffer()).toString(\"utf-8\");\n } else {\n localFile = path.resolve(backupArg);\n if (!fs.existsSync(localFile)) {\n outError(chalk.red(` ✗ Backup file not found: ${localFile}`));\n process.exit(1);\n }\n const globalsPath = globalsFileForDump(localFile);\n if (fs.existsSync(globalsPath)) globalsSql = fs.readFileSync(globalsPath, \"utf-8\");\n }\n\n // Version pre-flight. Check against the base connection — the server\n // version is identical for every database, and the target may not\n // exist yet when --create-db is used.\n const pf = await preflight(\"pg_restore\", baseConnection);\n if (!pf.compatible) {\n outError(chalk.red(` ✗ ${pf.reason}`));\n process.exit(1);\n }\n\n // `--create-db` needs a name before anything else can be decided.\n if (args[\"--create-db\"] && !targetDb) {\n outError(chalk.red(\" ✗ --create-db requires a resolvable target database name (use --target-db).\"));\n process.exit(1);\n }\n\n // Destructive-action gate. Restores overwrite data; never run without\n // an explicit yes (interactive confirmation or --yes).\n //\n // Ahead of `--create-db`, not after it. Creating the database first\n // meant an aborted run had already changed the cluster, while printing\n // \"No changes were made\" — and it left an empty database behind that a\n // second, confirmed run then reported as \"already exists\". The gate is\n // now the first thing that can stop the command, so its own message is\n // true whichever way the answer goes.\n if (!args[\"--yes\"]) {\n outWarn(chalk.yellow(\n ` ⚠️ This will restore into \"${targetDb ?? \"the target database\"}\" and may overwrite existing data.`\n ));\n const confirmed = await promptConfirm(chalk.yellow(\" Type 'yes' to continue: \"));\n if (!confirmed) {\n print(chalk.gray(\" Aborted. No changes were made.\"));\n process.exit(1);\n }\n }\n\n // Create the target database when requested.\n if (args[\"--create-db\"]) {\n const created = await ensureDatabaseExists(baseConnection, targetDb!);\n print(chalk.gray(created ? ` ✓ Created database \"${targetDb}\".` : ` • Database \"${targetDb}\" already exists.`));\n }\n\n // Recreate cluster roles before restoring so GRANT/RLS statements in\n // the dump apply. Best-effort and idempotent (see applyGlobals).\n if (globalsSql) {\n print(chalk.gray(\" Recreating cluster roles from the backup's roles sidecar…\"));\n const { applied, skipped } = await applyGlobals(\n targetConnection,\n globalsSql,\n (m) => print(chalk.gray(m))\n );\n print(chalk.gray(` ✓ Roles: ${applied} applied, ${skipped} skipped (already present or not permitted).`));\n } else {\n outWarn(chalk.yellow(\" ⚠️ No roles sidecar (.globals.sql) accompanies this backup.\"));\n outWarn(chalk.yellow(\" If the dump grants to roles that don't exist (e.g. rebase_user), the restore\"));\n outWarn(chalk.yellow(\" will fail — recreate those roles first, or use a backup that includes its globals.\"));\n }\n\n await restoreDump({\n connectionString: targetConnection,\n inputFile: localFile,\n clean: args[\"--clean\"],\n noOwner: args[\"--no-owner\"],\n // Fail loudly by default so a skipped GRANT never leaves RLS off.\n exitOnError: !args[\"--continue-on-error\"],\n inheritStdio: true\n });\n\n print(\"\");\n print(chalk.green(` ✓ Restore completed into \"${targetDb ?? \"the target database\"}\".`));\n print(\"\");\n } catch (err) {\n reportError(err);\n process.exit(1);\n } finally {\n if (cleanupTemp && typeof localFile! === \"string\" && fs.existsSync(localFile!)) {\n fs.rmSync(path.dirname(localFile!), { recursive: true, force: true });\n }\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db backups list\n// ─────────────────────────────────────────────────────────────────────────\nexport async function backupsCommand(rawArgs: string[]): Promise<void> {\n const action = rawArgs[2];\n if (!action || action === \"--help\") {\n printBackupsHelp();\n return;\n }\n if (action !== \"list\") {\n outError(chalk.red(`Unknown backups action: \"${action}\". Valid: list`));\n process.exit(1);\n }\n\n const args = arg({ \"--out\": String, \"--output\": \"--out\", \"-o\": \"--out\" }, { argv: rawArgs.slice(3), permissive: true });\n const out = args[\"--out\"] || process.env.BACKUP_DESTINATION || path.join(process.cwd(), \"backups\");\n const dest = parseBackupDestination(out);\n\n try {\n const storage = await resolveStorageForDestination(dest, process.env);\n const backups = await listBackups(dest, storage ?? undefined);\n print(\"\");\n if (backups.length === 0) {\n print(chalk.gray(` No backups found at ${out}.`));\n } else {\n print(chalk.bold(` 💾 ${backups.length} backup(s) at ${out}:`));\n print(\"\");\n for (const b of backups) {\n const when = b.createdAt ? b.createdAt.toISOString() : \"unknown date\";\n const name = dest.kind === \"local\" ? path.basename(b.key) : b.key;\n // An empty file is called out rather than listed as a peer of\n // the real ones. Older failures could leave a 0-byte dump here\n // (fixed at the source now), and retention protects the newest\n // by date whatever they contain — so a corpse left in place can\n // hold a `keepMinimum` slot against a backup that matters.\n const empty = b.sizeBytes === 0;\n const size = b.sizeBytes === undefined ? \"\" : ` — ${formatBytes(b.sizeBytes)}`;\n if (empty) {\n print(` ${chalk.red(\"○\")} ${chalk.bold(name)} ${chalk.gray(`— ${when}`)}${chalk.red(\" — EMPTY, not restorable\")}`);\n } else {\n print(` ${chalk.green(\"●\")} ${chalk.bold(name)} ${chalk.gray(`— ${when}${size}`)}`);\n }\n }\n if (backups.some(b => b.sizeBytes === 0)) {\n print(\"\");\n print(chalk.yellow(\" ⚠ Empty files above are leftovers from a failed backup. Delete them:\"));\n print(chalk.gray(\" they count as recent backups for retention but restore nothing.\"));\n }\n }\n print(\"\");\n } catch (err) {\n reportError(err);\n process.exit(1);\n }\n}\n\n/**\n * Verify a freshly written dump; abort the command if it looks corrupt.\n *\n * Discards the artifact on the way out. Refusing to *report* success was never\n * enough on its own: the file stayed on disk, `rebase db backups list` showed\n * it as an ordinary entry, and retention — which ranks by timestamp and never\n * looks at size — would protect it as one of the `keepMinimum` newest while\n * pruning a real backup underneath it. The roles sidecar goes too; the two are\n * uploaded and pruned as a pair, so half a pair is not a backup either.\n */\nasync function assertDumpValid(localFile: string): Promise<void> {\n const check = await validateDump(localFile);\n if (!check.ok) {\n discardPartialDump(globalsFileForDump(localFile));\n discardPartialDump(localFile);\n throw new BackupToolError(\n `The backup failed validation and was discarded: ${check.reason}`,\n \"The dump was corrupt or truncated, so nothing was kept. Investigate before relying on this destination.\"\n );\n }\n}\n\nfunction reportError(err: unknown): void {\n if (err instanceof BackupToolError) {\n outError(chalk.red(` ✗ ${err.message}`));\n if (err.hint) outError(chalk.gray(` ${err.hint}`));\n } else {\n outError(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));\n }\n}\n\nfunction printBackupHelp(): void {\n print(`\n${chalk.bold(\"rebase db backup\")} — Create a database backup (pg_dump, custom format)\n\n${chalk.green.bold(\"Usage\")}\n rebase db backup [--out <path|s3://bucket/prefix>] [options]\n\n${chalk.green.bold(\"Options\")}\n ${chalk.blue(\"--out, -o\")} <dest> Local path or s3://…/gs://… URL (default: ./backups)\n ${chalk.blue(\"--exclude-schema\")} <s> Exclude a schema (repeatable)\n ${chalk.blue(\"--no-owner\")} Omit ownership commands from the dump\n ${chalk.blue(\"--enable-row-security\")} Dump as an admin subject instead of failing on RLS\n ${chalk.red(\"(may produce a partial dump — see below)\")}\n ${chalk.blue(\"--row-security-role\")} <r> Role to read as with the flag above (default: admin)\n\n${chalk.green.bold(\"Row-level security\")}\n On a managed Postgres the dumping role usually owns nothing and has no\n BYPASSRLS, so pg_dump refuses:\n\n ERROR: query would be affected by row-level security policy for table \"...\"\n\n That refusal is the safe behaviour. --enable-row-security replaces it by\n reading as an admin subject: Rebase sets app.uid/app.user_roles so the\n generated admin_full_access policy admits the dump. The dump then contains\n exactly what those policies admit ${chalk.red(\"and no error is raised for what they do not\")} —\n a table whose policies lack an admin rule comes out short, silently.\n\n Granting the dumping role BYPASSRLS is the option that keeps a backup\n meaning \"every row\".\n\n${chalk.green.bold(\"Notes\")}\n Backups may contain secrets and PII. Use private storage destinations and\n enable encryption-at-rest. See docs/backups.md.\n`);\n}\n\nfunction printRestoreHelp(): void {\n print(`\n${chalk.bold(\"rebase db restore\")} — Restore a database from a backup (pg_restore)\n\n${chalk.green.bold(\"Usage\")}\n rebase db restore <backup> [options]\n\n${chalk.green.bold(\"Arguments\")}\n <backup> Local .dump file, or s3://…/gs://… object key\n\n${chalk.green.bold(\"Options\")}\n ${chalk.blue(\"--target-db\")} <name> Restore into this database instead of DATABASE_URL's\n ${chalk.blue(\"--create-db\")} Create the target database first if it doesn't exist\n ${chalk.blue(\"--clean\")} Drop existing objects before recreating them\n ${chalk.blue(\"--no-owner\")} Ignore ownership from the dump\n ${chalk.blue(\"--continue-on-error\")} Log and continue past errors ${chalk.red(\"(may leave RLS un-enforced!)\")}\n ${chalk.blue(\"--yes, -y\")} Skip the interactive confirmation ${chalk.red(\"(destructive!)\")}\n\n${chalk.red.bold(\"Warning\")}\n Restore is destructive and never runs automatically. Without --yes it\n requires an interactive 'yes'. Prefer --create-db/--target-db to restore\n into a fresh database rather than overwriting a live one.\n\n By default the restore aborts on the first error (--exit-on-error) so a\n skipped GRANT never silently leaves RLS un-enforced. Roles are recreated\n from the backup's .globals.sql sidecar first; keep that file next to the\n dump. Use --continue-on-error only when you understand the consequences.\n`);\n}\n\nfunction printBackupsHelp(): void {\n print(`\n${chalk.bold(\"rebase db backups\")} — Manage stored backups\n\n${chalk.green.bold(\"Usage\")}\n rebase db backups list [--out <path|s3://bucket/prefix>]\n rebase db backup list [--out <path|s3://bucket/prefix>] ${chalk.gray(\"(the same thing)\")}\n`);\n}\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
- import { A as resolveLocalBin, F as forLibpq, P as execa } from "./rls-bootstrap-sql-B8EclDyM.js";
5
- import fs from "fs";
4
+ import { F as resolveLocalBin } from "./cli-errors-CaI4ntyD.js";
5
+ import { o as execa, s as forLibpq } from "./rls-bootstrap-sql-RQUljnJ1.js";
6
6
  import path from "path";
7
+ import fs from "fs";
7
8
  import os from "os";
8
9
  //#region src/backup/pg-tools.ts
9
10
  /**
@@ -320,6 +321,29 @@ function selectBackupsToPrune(backups, options) {
320
321
  return toDelete.map((b) => b.key);
321
322
  }
322
323
  //#endregion
324
+ //#region src/backup/pg-binary.ts
325
+ /**
326
+ * Where the PostgreSQL client tools are, without spawning them.
327
+ *
328
+ * Kept apart from `backup-service.ts`, which pulls in `execa` (ESM-only), so
329
+ * `createBackupCron` can check for the tools when it is registered.
330
+ */
331
+ var OVERRIDE_VARIABLE = {
332
+ pg_dump: "PG_DUMP_PATH",
333
+ pg_restore: "PG_RESTORE_PATH",
334
+ pg_dumpall: "PG_DUMPALL_PATH"
335
+ };
336
+ /** The variable that points at `tool` when it is not on the PATH. */
337
+ function pgToolVariable(tool) {
338
+ return OVERRIDE_VARIABLE[tool];
339
+ }
340
+ /** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
341
+ function resolvePgBinary(tool, env = process.env) {
342
+ const override = env[OVERRIDE_VARIABLE[tool]];
343
+ if (override && fs.existsSync(override)) return override;
344
+ return resolveLocalBin(tool);
345
+ }
346
+ //#endregion
323
347
  //#region src/backup/backup-logic.ts
324
348
  /**
325
349
  * Pure-ish orchestration for the backup/restore paths, kept free of `execa`
@@ -438,12 +462,6 @@ var BackupToolError = class extends Error {
438
462
  this.name = "BackupToolError";
439
463
  }
440
464
  };
441
- /** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
442
- function resolvePgBinary(tool, env = process.env) {
443
- const overrideVar = tool === "pg_dump" ? env.PG_DUMP_PATH : tool === "pg_restore" ? env.PG_RESTORE_PATH : env.PG_DUMPALL_PATH;
444
- if (overrideVar && fs.existsSync(overrideVar)) return overrideVar;
445
- return resolveLocalBin(tool);
446
- }
447
465
  /** Run `<bin> --version` and extract the major version. */
448
466
  async function detectToolMajor(bin) {
449
467
  try {
@@ -655,8 +673,7 @@ async function ensureDatabaseExists(adminConnectionString, dbName) {
655
673
  async function uploadBackup(storage, localFile, dest) {
656
674
  const fileName = path.basename(localFile);
657
675
  const key = joinStorageKey(dest.prefix, fileName);
658
- const buffer = fs.readFileSync(localFile);
659
- const file = new File([buffer], fileName, { type: "application/octet-stream" });
676
+ const file = new File([await fs.openAsBlob(localFile)], fileName, { type: "application/octet-stream" });
660
677
  return {
661
678
  key,
662
679
  storageUrl: (await storage.putObject({
@@ -664,35 +681,50 @@ async function uploadBackup(storage, localFile, dest) {
664
681
  key,
665
682
  bucket: dest.bucket,
666
683
  metadata: { "rebase-backup": "1" }
667
- })).storageUrl
684
+ }, { uploadLimits: false })).storageUrl
668
685
  };
669
686
  }
687
+ /** Stats in flight at once while listing a local backup folder. */
688
+ var LOCAL_STAT_CONCURRENCY = 32;
689
+ /** `fn` over `items`, at most `limit` at a time; results in input order. */
690
+ async function mapBounded(items, limit, fn) {
691
+ const results = new Array(items.length);
692
+ let next = 0;
693
+ const worker = async () => {
694
+ while (next < items.length) {
695
+ const i = next++;
696
+ results[i] = await fn(items[i]);
697
+ }
698
+ };
699
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
700
+ return results;
701
+ }
670
702
  /**
671
703
  * List existing backups at a destination. For local destinations reads the
672
704
  * directory; for object storage lists the prefix via the controller.
673
705
  */
674
706
  async function listBackups(dest, storage) {
675
707
  if (dest.kind === "local") {
676
- if (!fs.existsSync(dest.path)) return [];
677
- const dir = fs.statSync(dest.path).isDirectory() ? dest.path : path.dirname(dest.path);
678
- return fs.readdirSync(dir).filter((f) => f.endsWith(".dump")).map((f) => {
679
- const full = path.join(dir, f);
680
- const stats = fs.statSync(full);
708
+ const stat = await fs.promises.stat(dest.path).catch(() => null);
709
+ if (!stat) return [];
710
+ const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);
711
+ const dumps = (await fs.promises.readdir(dir)).filter((f) => f.endsWith(".dump"));
712
+ const stats = await mapBounded(dumps, LOCAL_STAT_CONCURRENCY, (f) => fs.promises.stat(path.join(dir, f)));
713
+ return dumps.map((f, i) => {
714
+ const createdAt = parseBackupTimestamp(f) ?? stats[i].mtime;
681
715
  return {
682
- key: full,
683
- createdAt: parseBackupTimestamp(f) ?? stats.mtime,
684
- sizeBytes: stats.size
716
+ key: path.join(dir, f),
717
+ createdAt,
718
+ sizeBytes: stats[i].size
685
719
  };
686
720
  }).sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));
687
721
  }
688
722
  if (!storage) throw new BackupToolError(`Listing ${dest.kind} backups requires a configured storage backend.`);
689
- return (await storage.listObjects(dest.prefix ? `${dest.prefix}/` : "", {
690
- bucket: dest.bucket,
691
- maxResults: 1e3
692
- })).items.map((item) => item.fullPath).filter((key) => key.endsWith(".dump")).map((key) => ({
693
- key,
694
- createdAt: parseBackupTimestamp(key) ?? void 0
695
- })).sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));
723
+ const { listBackupObjects } = await import("@rebasepro/server");
724
+ return (await listBackupObjects(dest, storage)).map((info) => ({
725
+ key: info.key,
726
+ createdAt: info.createdAt ? new Date(info.createdAt) : void 0
727
+ }));
696
728
  }
697
729
  /**
698
730
  * Apply a retention policy to a destination, deleting the backups selected
@@ -710,6 +742,6 @@ async function pruneBackups(dest, options, storage) {
710
742
  return toDelete;
711
743
  }
712
744
  //#endregion
713
- export { parsePgToolMajor as A, checkToolServerCompatibility as C, parseBackupDestination as D, joinStorageKey as E, serverVersionNumToMajor as M, splitGlobalsStatements as N, parseBackupTimestamp as O, withDatabaseName as P, buildRowSecurityPgOptions as S, globalsFileForDump as T, buildBackupFilename as _, detectToolMajor as a, buildPgRestoreArgs as b, getServerVersionMajor as c, pruneBackups as d, resolvePgBinary as f, selectBackupsToPrune as g, validateDump as h, createDump as i, resolveConnectionString as j, parseDbNameFromUrl as k, listBackups as l, uploadBackup as m, applyGlobals as n, discardPartialDump as o, restoreDump as p, backup_service_exports as r, ensureDatabaseExists as s, BackupToolError as t, preflight as u, buildPgDumpArgs as v, diagnoseRowSecurityDumpFailure as w, buildPgRestoreListArgs as x, buildPgDumpallGlobalsArgs as y };
745
+ export { parseDbNameFromUrl as A, buildRowSecurityPgOptions as C, joinStorageKey as D, globalsFileForDump as E, withDatabaseName as F, resolveConnectionString as M, serverVersionNumToMajor as N, parseBackupDestination as O, splitGlobalsStatements as P, buildPgRestoreListArgs as S, diagnoseRowSecurityDumpFailure as T, selectBackupsToPrune as _, detectToolMajor as a, buildPgDumpallGlobalsArgs as b, getServerVersionMajor as c, pruneBackups as d, restoreDump as f, resolvePgBinary as g, pgToolVariable as h, createDump as i, parsePgToolMajor as j, parseBackupTimestamp as k, listBackups as l, validateDump as m, applyGlobals as n, discardPartialDump as o, uploadBackup as p, backup_service_exports as r, ensureDatabaseExists as s, BackupToolError as t, preflight as u, buildBackupFilename as v, checkToolServerCompatibility as w, buildPgRestoreArgs as x, buildPgDumpArgs as y };
714
746
 
715
- //# sourceMappingURL=backup-service-HQ9GC4tN.js.map
747
+ //# sourceMappingURL=backup-service-BTBB7RXn.js.map