@rebasepro/server-postgres 0.0.1-canary.4829d6e

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 (121) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +86 -0
  3. package/dist/PostgresAdapter.d.ts +6 -0
  4. package/dist/PostgresBackendDriver.d.ts +150 -0
  5. package/dist/PostgresBootstrapper.d.ts +51 -0
  6. package/dist/auth/ensure-tables.d.ts +10 -0
  7. package/dist/auth/services.d.ts +250 -0
  8. package/dist/backup/backup-cli.d.ts +3 -0
  9. package/dist/backup/backup-cron.d.ts +53 -0
  10. package/dist/backup/backup-service.d.ts +85 -0
  11. package/dist/backup/index.d.ts +12 -0
  12. package/dist/backup/pg-tools.d.ts +110 -0
  13. package/dist/backup/retention.d.ts +35 -0
  14. package/dist/chunk-DSJWtz9O.js +40 -0
  15. package/dist/cli-errors.d.ts +42 -0
  16. package/dist/cli-helpers.d.ts +7 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/collections/PostgresCollectionRegistry.d.ts +47 -0
  19. package/dist/connection.d.ts +65 -0
  20. package/dist/data-transformer.d.ts +55 -0
  21. package/dist/databasePoolManager.d.ts +20 -0
  22. package/dist/history/HistoryService.d.ts +71 -0
  23. package/dist/history/ensure-history-table.d.ts +7 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.es.js +23535 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/interfaces.d.ts +18 -0
  28. package/dist/schema/auth-bootstrap-sql.d.ts +24 -0
  29. package/dist/schema/auth-default-policies.d.ts +10 -0
  30. package/dist/schema/auth-schema.d.ts +2376 -0
  31. package/dist/schema/doctor-cli.d.ts +2 -0
  32. package/dist/schema/doctor.d.ts +58 -0
  33. package/dist/schema/dynamic-tables.d.ts +31 -0
  34. package/dist/schema/generate-drizzle-schema-logic.d.ts +2 -0
  35. package/dist/schema/generate-drizzle-schema.d.ts +1 -0
  36. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
  37. package/dist/schema/generate-postgres-ddl.d.ts +1 -0
  38. package/dist/schema/introspect-db-inference.d.ts +5 -0
  39. package/dist/schema/introspect-db-logic.d.ts +118 -0
  40. package/dist/schema/introspect-db.d.ts +1 -0
  41. package/dist/schema/introspect-runtime.d.ts +57 -0
  42. package/dist/schema/test-schema.d.ts +24 -0
  43. package/dist/security/policy-drift.d.ts +57 -0
  44. package/dist/security/rls-enforcement.d.ts +122 -0
  45. package/dist/services/BranchService.d.ts +47 -0
  46. package/dist/services/FetchService.d.ts +214 -0
  47. package/dist/services/PersistService.d.ts +39 -0
  48. package/dist/services/RelationService.d.ts +109 -0
  49. package/dist/services/cdc/CdcListener.d.ts +54 -0
  50. package/dist/services/cdc/trigger-cdc.d.ts +64 -0
  51. package/dist/services/collection-helpers.d.ts +38 -0
  52. package/dist/services/dataService.d.ts +110 -0
  53. package/dist/services/index.d.ts +4 -0
  54. package/dist/services/realtimeService.d.ts +298 -0
  55. package/dist/src-Eh-CZosp.js +595 -0
  56. package/dist/src-Eh-CZosp.js.map +1 -0
  57. package/dist/types.d.ts +3 -0
  58. package/dist/utils/drizzle-conditions.d.ts +138 -0
  59. package/dist/utils/pg-array-null-patch.d.ts +16 -0
  60. package/dist/utils/pg-error-utils.d.ts +65 -0
  61. package/dist/utils/table-classification.d.ts +8 -0
  62. package/dist/websocket.d.ts +18 -0
  63. package/package.json +113 -0
  64. package/src/PostgresAdapter.ts +58 -0
  65. package/src/PostgresBackendDriver.ts +1387 -0
  66. package/src/PostgresBootstrapper.ts +581 -0
  67. package/src/auth/ensure-tables.ts +367 -0
  68. package/src/auth/services.ts +1321 -0
  69. package/src/backup/backup-cli.ts +383 -0
  70. package/src/backup/backup-cron.ts +189 -0
  71. package/src/backup/backup-service.ts +299 -0
  72. package/src/backup/index.ts +12 -0
  73. package/src/backup/pg-tools.ts +231 -0
  74. package/src/backup/retention.ts +75 -0
  75. package/src/cli-errors.ts +265 -0
  76. package/src/cli-helpers.ts +196 -0
  77. package/src/cli.ts +786 -0
  78. package/src/collections/PostgresCollectionRegistry.ts +103 -0
  79. package/src/connection.ts +166 -0
  80. package/src/data-transformer.ts +733 -0
  81. package/src/databasePoolManager.ts +87 -0
  82. package/src/history/HistoryService.ts +272 -0
  83. package/src/history/ensure-history-table.ts +46 -0
  84. package/src/index.ts +15 -0
  85. package/src/interfaces.ts +60 -0
  86. package/src/schema/auth-bootstrap-sql.ts +41 -0
  87. package/src/schema/auth-default-policies.ts +125 -0
  88. package/src/schema/auth-schema.ts +233 -0
  89. package/src/schema/doctor-cli.ts +113 -0
  90. package/src/schema/doctor.ts +733 -0
  91. package/src/schema/dynamic-tables.test.ts +302 -0
  92. package/src/schema/dynamic-tables.ts +293 -0
  93. package/src/schema/generate-drizzle-schema-logic.ts +850 -0
  94. package/src/schema/generate-drizzle-schema.ts +131 -0
  95. package/src/schema/generate-postgres-ddl-logic.ts +490 -0
  96. package/src/schema/generate-postgres-ddl.ts +92 -0
  97. package/src/schema/introspect-db-inference.ts +238 -0
  98. package/src/schema/introspect-db-logic.ts +910 -0
  99. package/src/schema/introspect-db.ts +266 -0
  100. package/src/schema/introspect-runtime.test.ts +212 -0
  101. package/src/schema/introspect-runtime.ts +293 -0
  102. package/src/schema/test-schema.ts +11 -0
  103. package/src/security/policy-drift.test.ts +122 -0
  104. package/src/security/policy-drift.ts +159 -0
  105. package/src/security/rls-enforcement.ts +295 -0
  106. package/src/services/BranchService.ts +251 -0
  107. package/src/services/FetchService.ts +1661 -0
  108. package/src/services/PersistService.ts +329 -0
  109. package/src/services/RelationService.ts +1306 -0
  110. package/src/services/cdc/CdcListener.ts +167 -0
  111. package/src/services/cdc/trigger-cdc.ts +169 -0
  112. package/src/services/collection-helpers.ts +151 -0
  113. package/src/services/dataService.ts +246 -0
  114. package/src/services/index.ts +13 -0
  115. package/src/services/realtimeService.ts +1502 -0
  116. package/src/types.ts +4 -0
  117. package/src/utils/drizzle-conditions.ts +1162 -0
  118. package/src/utils/pg-array-null-patch.ts +42 -0
  119. package/src/utils/pg-error-utils.ts +227 -0
  120. package/src/utils/table-classification.ts +16 -0
  121. package/src/websocket.ts +640 -0
@@ -0,0 +1,233 @@
1
+ import { pgSchema, pgTable, varchar, uuid, timestamp, boolean, jsonb, text, unique } from "drizzle-orm/pg-core";
2
+ import { relations } from "drizzle-orm";
3
+
4
+ /**
5
+ * Factory function to dynamically create the auth tables bound to the specified schema names.
6
+ */
7
+ export function createAuthSchema(usersSchemaName = "rebase") {
8
+ const usersSchema = usersSchemaName === "public" ? null : pgSchema(usersSchemaName);
9
+
10
+ const tableCreator = (usersSchema ? usersSchema.table.bind(usersSchema) : pgTable) as typeof pgTable;
11
+ const usersTableCreator = tableCreator;
12
+
13
+ /**
14
+ * Users table - stores both email/password and OAuth users
15
+ */
16
+ const users = usersTableCreator("users", {
17
+ id: uuid("id").defaultRandom().primaryKey(),
18
+ email: varchar("email", { length: 255 }).notNull().unique(),
19
+ passwordHash: varchar("password_hash", { length: 255 }), // NULL for OAuth-only users
20
+ displayName: varchar("display_name", { length: 255 }),
21
+ photoUrl: varchar("photo_url", { length: 500 }),
22
+ emailVerified: boolean("email_verified").default(false).notNull(),
23
+ emailVerificationToken: varchar("email_verification_token", { length: 255 }),
24
+ emailVerificationSentAt: timestamp("email_verification_sent_at"),
25
+ isAnonymous: boolean("is_anonymous").default(false).notNull(),
26
+ roles: text("roles").array().default([]).notNull(),
27
+ metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}).notNull(),
28
+ createdAt: timestamp("created_at").defaultNow().notNull(),
29
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
30
+ });
31
+
32
+
33
+ /**
34
+ * Refresh tokens for long-lived sessions
35
+ */
36
+ const refreshTokens = tableCreator("refresh_tokens", {
37
+ id: uuid("id").defaultRandom().primaryKey(),
38
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
39
+ tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
40
+ expiresAt: timestamp("expires_at").notNull(),
41
+ userAgent: varchar("user_agent", { length: 500 }),
42
+ ipAddress: varchar("ip_address", { length: 45 }),
43
+ createdAt: timestamp("created_at").defaultNow().notNull()
44
+ }, (table) => ({
45
+ uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
46
+ }));
47
+
48
+ /**
49
+ * Password reset tokens for forgot password flow
50
+ */
51
+ const passwordResetTokens = tableCreator("password_reset_tokens", {
52
+ id: uuid("id").defaultRandom().primaryKey(),
53
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
54
+ tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
55
+ expiresAt: timestamp("expires_at").notNull(),
56
+ usedAt: timestamp("used_at"),
57
+ createdAt: timestamp("created_at").defaultNow().notNull()
58
+ });
59
+
60
+ /**
61
+ * App config - key/value store for custom settings
62
+ */
63
+ const appConfig = tableCreator("app_config", {
64
+ key: varchar("key", { length: 100 }).primaryKey(),
65
+ value: jsonb("value").notNull(),
66
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
67
+ });
68
+
69
+ /**
70
+ * User identities - maps external OAuth profiles back to local users
71
+ */
72
+ const userIdentities = tableCreator("user_identities", {
73
+ id: uuid("id").defaultRandom().primaryKey(),
74
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
75
+ provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
76
+ providerId: varchar("provider_id", { length: 255 }).notNull(),
77
+ profileData: jsonb("profile_data"),
78
+ createdAt: timestamp("created_at").defaultNow().notNull(),
79
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
80
+ }, (table) => ({
81
+ uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
82
+ }));
83
+
84
+ /**
85
+ * MFA factors table - stores enrolled MFA methods
86
+ */
87
+ const mfaFactors = tableCreator("mfa_factors", {
88
+ id: uuid("id").defaultRandom().primaryKey(),
89
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
90
+ factorType: varchar("factor_type", { length: 20 }).notNull(), // 'totp'
91
+ secretEncrypted: varchar("secret_encrypted", { length: 500 }).notNull(),
92
+ friendlyName: varchar("friendly_name", { length: 255 }),
93
+ verified: boolean("verified").default(false).notNull(),
94
+ createdAt: timestamp("created_at").defaultNow().notNull(),
95
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
96
+ });
97
+
98
+ /**
99
+ * MFA challenges table - tracks active MFA verification attempts
100
+ */
101
+ const mfaChallenges = tableCreator("mfa_challenges", {
102
+ id: uuid("id").defaultRandom().primaryKey(),
103
+ factorId: uuid("factor_id").notNull().references(() => mfaFactors.id, { onDelete: "cascade" }),
104
+ createdAt: timestamp("created_at").defaultNow().notNull(),
105
+ verifiedAt: timestamp("verified_at"),
106
+ ipAddress: varchar("ip_address", { length: 45 }),
107
+ expiresAt: timestamp("expires_at").notNull()
108
+ });
109
+
110
+ /**
111
+ * Recovery codes table - backup codes for MFA
112
+ */
113
+ const recoveryCodes = tableCreator("recovery_codes", {
114
+ id: uuid("id").defaultRandom().primaryKey(),
115
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
116
+ codeHash: varchar("code_hash", { length: 255 }).notNull(),
117
+ usedAt: timestamp("used_at"),
118
+ createdAt: timestamp("created_at").defaultNow().notNull()
119
+ });
120
+
121
+ /**
122
+ * Magic link tokens for passwordless email login
123
+ */
124
+ const magicLinkTokens = tableCreator("magic_link_tokens", {
125
+ id: uuid("id").defaultRandom().primaryKey(),
126
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
127
+ tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
128
+ expiresAt: timestamp("expires_at").notNull(),
129
+ usedAt: timestamp("used_at"),
130
+ createdAt: timestamp("created_at").defaultNow().notNull()
131
+ });
132
+
133
+ return {
134
+ usersSchema,
135
+ users,
136
+ refreshTokens,
137
+ passwordResetTokens,
138
+ appConfig,
139
+ userIdentities,
140
+ mfaFactors,
141
+ mfaChallenges,
142
+ recoveryCodes,
143
+ magicLinkTokens
144
+ };
145
+ }
146
+
147
+ // Instantiate default schema and tables using the default "rebase" schema
148
+ const defaultAuthSchema = createAuthSchema("rebase");
149
+
150
+ export const usersSchema = defaultAuthSchema.usersSchema;
151
+
152
+ export const users = defaultAuthSchema.users;
153
+ export const refreshTokens = defaultAuthSchema.refreshTokens;
154
+ export const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
155
+ export const appConfig = defaultAuthSchema.appConfig;
156
+ export const userIdentities = defaultAuthSchema.userIdentities;
157
+ export const mfaFactors = defaultAuthSchema.mfaFactors;
158
+ export const mfaChallenges = defaultAuthSchema.mfaChallenges;
159
+ export const recoveryCodes = defaultAuthSchema.recoveryCodes;
160
+ export const magicLinkTokens = defaultAuthSchema.magicLinkTokens;
161
+
162
+ // Relations
163
+ export const usersRelations = relations(users, ({ many }) => ({
164
+ refreshTokens: many(refreshTokens),
165
+ passwordResetTokens: many(passwordResetTokens),
166
+ userIdentities: many(userIdentities),
167
+ mfaFactors: many(mfaFactors),
168
+ recoveryCodes: many(recoveryCodes),
169
+ magicLinkTokens: many(magicLinkTokens)
170
+ }));
171
+
172
+ export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
173
+ user: one(users, {
174
+ fields: [refreshTokens.userId],
175
+ references: [users.id]
176
+ })
177
+ }));
178
+
179
+ export const passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({
180
+ user: one(users, {
181
+ fields: [passwordResetTokens.userId],
182
+ references: [users.id]
183
+ })
184
+ }));
185
+
186
+ export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
187
+ user: one(users, {
188
+ fields: [userIdentities.userId],
189
+ references: [users.id]
190
+ })
191
+ }));
192
+
193
+ export const mfaFactorsRelations = relations(mfaFactors, ({ one, many }) => ({
194
+ user: one(users, {
195
+ fields: [mfaFactors.userId],
196
+ references: [users.id]
197
+ }),
198
+ challenges: many(mfaChallenges)
199
+ }));
200
+
201
+ export const mfaChallengesRelations = relations(mfaChallenges, ({ one }) => ({
202
+ factor: one(mfaFactors, {
203
+ fields: [mfaChallenges.factorId],
204
+ references: [mfaFactors.id]
205
+ })
206
+ }));
207
+
208
+ export const recoveryCodesRelations = relations(recoveryCodes, ({ one }) => ({
209
+ user: one(users, {
210
+ fields: [recoveryCodes.userId],
211
+ references: [users.id]
212
+ })
213
+ }));
214
+
215
+ export const magicLinkTokensRelations = relations(magicLinkTokens, ({ one }) => ({
216
+ user: one(users, {
217
+ fields: [magicLinkTokens.userId],
218
+ references: [users.id]
219
+ })
220
+ }));
221
+
222
+ // Type exports
223
+ export type User = typeof users.$inferSelect;
224
+ export type NewUser = typeof users.$inferInsert;
225
+ export type RefreshToken = typeof refreshTokens.$inferSelect;
226
+ export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
227
+ export type AppConfig = typeof appConfig.$inferSelect;
228
+ export type UserIdentity = typeof userIdentities.$inferSelect;
229
+ export type NewUserIdentity = typeof userIdentities.$inferInsert;
230
+ export type MfaFactorRow = typeof mfaFactors.$inferSelect;
231
+ export type MfaChallengeRow = typeof mfaChallenges.$inferSelect;
232
+ export type RecoveryCodeRow = typeof recoveryCodes.$inferSelect;
233
+ export type MagicLinkToken = typeof magicLinkTokens.$inferSelect;
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for `rebase doctor`.
4
+ * Invoked via tsx by the server-postgres CLI plugin.
5
+ */
6
+ import path from "path";
7
+ import chalk from "chalk";
8
+ import fs from "fs";
9
+ import { runDoctor, loadCollections } from "./doctor";
10
+ import { checkPolicyDrift, formatPolicyDrift, hasDrift } from "../security/policy-drift";
11
+ import { validatePolicyPgRoles } from "../security/rls-enforcement";
12
+ import { logger } from "@rebasepro/server";
13
+
14
+ /**
15
+ * The RLS half of the doctor: policies actually deployed vs the ones the
16
+ * collections describe, plus policy roles this server could never satisfy.
17
+ *
18
+ * Returns true when something is wrong, so callers can set the exit code.
19
+ */
20
+ async function runPolicyChecks(collectionsPath: string, databaseUrl?: string): Promise<boolean> {
21
+ if (!databaseUrl) {
22
+ logger.warn(chalk.yellow(" ⚠ No DATABASE_URL — skipping RLS policy checks"));
23
+ return false;
24
+ }
25
+
26
+ let problems = false;
27
+ const { Pool } = await import("pg");
28
+ const pool = new Pool({ connectionString: databaseUrl });
29
+ try {
30
+ const collections = await loadCollections(path.resolve(process.cwd(), collectionsPath));
31
+ const runSql = async (text: string) => (await pool.query(text)).rows as Record<string, unknown>[];
32
+
33
+ // A policy naming a role the server never runs as filters every row, so
34
+ // the collection reads as empty. Report it without booting a server.
35
+ try {
36
+ await validatePolicyPgRoles(runSql, collections as never);
37
+ logger.info(chalk.green(" ✓ Policy roles are usable by this server"));
38
+ } catch (err) {
39
+ problems = true;
40
+ logger.info("");
41
+ logger.error(chalk.red(err instanceof Error ? err.message : String(err)));
42
+ }
43
+
44
+ const drift = await checkPolicyDrift(pool as never, collections);
45
+ logger.info("");
46
+ if (hasDrift(drift)) {
47
+ problems = true;
48
+ logger.info(chalk.yellow(" RLS policies: database does not match your collections"));
49
+ logger.info(formatPolicyDrift(drift));
50
+ } else {
51
+ logger.info(chalk.green(" ✓ RLS policies match your collections"));
52
+ }
53
+ } catch (err) {
54
+ logger.warn(chalk.yellow(" ⚠ Could not check RLS policies"), { error: err });
55
+ } finally {
56
+ await pool.end();
57
+ }
58
+ return problems;
59
+ }
60
+
61
+ async function main() {
62
+ const collectionsArg = process.argv.find((a) => a.startsWith("--collections="));
63
+ const schemaArg = process.argv.find((a) => a.startsWith("--schema="));
64
+ const sdkArg = process.argv.find((a) => a.startsWith("--sdk="));
65
+ // --policies runs only the RLS checks: no schema diff, no SDK types. Useful
66
+ // as a CI gate against a deployed database, where the collection files and
67
+ // generated schema are not what you are asking about.
68
+ const policiesOnly = process.argv.includes("--policies");
69
+
70
+ const collectionsPath = collectionsArg?.split("=")[1] ?? path.join("..", "config", "collections");
71
+ const schemaPath = schemaArg?.split("=")[1] ?? path.join("src", "schema.generated.ts");
72
+ const sdkPath = sdkArg?.split("=")[1] ?? path.join("..", "generated", "sdk", "database.types.ts");
73
+
74
+ // Load .env
75
+ try {
76
+ const dotenv = await import("dotenv");
77
+ const envPath = process.env.DOTENV_CONFIG_PATH;
78
+ if (envPath) {
79
+ dotenv.config({ path: envPath });
80
+ } else {
81
+ dotenv.config();
82
+ }
83
+ } catch {
84
+ // dotenv may not be installed
85
+ }
86
+
87
+ const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
88
+
89
+ if (policiesOnly) {
90
+ // Non-zero so this can gate CI — the whole point of the flag.
91
+ if (await runPolicyChecks(collectionsPath, databaseUrl)) process.exit(1);
92
+ return;
93
+ }
94
+
95
+ const report = await runDoctor({
96
+ collectionsPath: path.resolve(process.cwd(), collectionsPath),
97
+ schemaPath: path.resolve(process.cwd(), schemaPath),
98
+ sdkPath: path.resolve(process.cwd(), sdkPath),
99
+ databaseUrl: databaseUrl ?? undefined
100
+ });
101
+
102
+ const policiesDrifted = await runPolicyChecks(collectionsPath, databaseUrl);
103
+
104
+ // Exit with non-zero code if there are errors
105
+ if (report.summary.errors > 0 || policiesDrifted) {
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ main().catch((err) => {
111
+ logger.error(chalk.red(" ✗ Doctor failed"), { error: err });
112
+ process.exit(1);
113
+ });