@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,367 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+ import { logger } from "@rebasepro/server";
4
+ import type { CollectionConfig } from "@rebasepro/types";
5
+
6
+
7
+ /**
8
+ * Auto-create auth tables if they don't exist.
9
+ *
10
+ * @param db — Drizzle database instance
11
+ * @param collection — The collection that represents auth users.
12
+ * When omitted, a default `rebase.users` table is created.
13
+ */
14
+ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: CollectionConfig): Promise<void> {
15
+ logger.info("🔍 Checking auth tables...");
16
+
17
+ try {
18
+ // Resolve dynamic user table name and ID type from the collection
19
+ let usersTableName = '"rebase"."users"';
20
+ let userIdType = "TEXT";
21
+ let usersSchema = "rebase";
22
+ let resolvedTable = "users";
23
+ if (collection) {
24
+ resolvedTable = ("table" in collection && typeof collection.table === "string")
25
+ ? collection.table
26
+ : collection.slug;
27
+ usersSchema = ("schema" in collection && typeof collection.schema === "string")
28
+ ? collection.schema
29
+ : "public";
30
+ usersTableName = usersSchema === "public"
31
+ ? `"${resolvedTable}"`
32
+ : `"${usersSchema}"."${resolvedTable}"`;
33
+
34
+ // Derive ID column type from collection properties
35
+ const idProp = collection.properties?.id;
36
+ if (idProp) {
37
+ const isId = ("isId" in idProp) ? (idProp as unknown as Record<string, unknown>).isId : undefined;
38
+ if (isId === "uuid") {
39
+ userIdType = "UUID";
40
+ } else if (isId === "autoincrement") {
41
+ userIdType = "INTEGER";
42
+ }
43
+ // Otherwise keep TEXT as default
44
+ }
45
+ }
46
+
47
+ // Introspect the database to find the actual type of usersTableName's ID column if the table exists
48
+ try {
49
+ const result = await db.execute(sql`
50
+ SELECT data_type
51
+ FROM information_schema.columns
52
+ WHERE table_schema = ${usersSchema}
53
+ AND table_name = ${resolvedTable}
54
+ AND column_name = 'id'
55
+ `);
56
+ if (result && result.rows && result.rows.length > 0) {
57
+ const dbType = String((result.rows[0] as { data_type: string }).data_type).toUpperCase();
58
+ if (dbType === "UUID") {
59
+ userIdType = "UUID";
60
+ } else if (dbType === "INTEGER" || dbType === "SMALLINT" || dbType === "BIGINT") {
61
+ userIdType = "INTEGER";
62
+ } else {
63
+ userIdType = "TEXT";
64
+ }
65
+ logger.info(`✨ Detected ${usersTableName}.id type from database: ${dbType}. Using user_id type: ${userIdType}`);
66
+ }
67
+ } catch (err) {
68
+ // Ignore introspection errors, fallback to derived/default type
69
+ logger.warn(`⚠️ Failed to introspect ${usersTableName}.id type from database, falling back to config type: ${userIdType}`, { error: err });
70
+ }
71
+
72
+
73
+ // ── Create schemas (idempotent) ──────────────────────────────────
74
+ if (usersSchema !== "public") {
75
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema)}`);
76
+ }
77
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
78
+
79
+ const authSchema = usersSchema === "public" ? "rebase" : usersSchema;
80
+ const userIdentitiesTable = `"${authSchema}"."user_identities"`;
81
+ const refreshTokensTableName = `"${authSchema}"."refresh_tokens"`;
82
+ const passwordResetTokensTableName = `"${authSchema}"."password_reset_tokens"`;
83
+ const appConfigTableName = `"${authSchema}"."app_config"`;
84
+
85
+ // ── Create users table (idempotent) ─────────────────────────────
86
+ // The users table MUST be created before any dependent auth tables
87
+ // (user_identities, refresh_tokens, etc.) because they all hold
88
+ // foreign keys referencing users(id). When a developer runs
89
+ // `pnpm dev` for the first time without `db:migrate`, this ensures
90
+ // the server can self-bootstrap.
91
+ const idDefault = userIdType === "UUID"
92
+ ? "DEFAULT gen_random_uuid()"
93
+ : userIdType === "INTEGER"
94
+ ? "GENERATED ALWAYS AS IDENTITY"
95
+ : "DEFAULT gen_random_uuid()::text";
96
+
97
+ await db.execute(sql`
98
+ CREATE TABLE IF NOT EXISTS ${sql.raw(usersTableName)} (
99
+ id ${sql.raw(userIdType)} PRIMARY KEY ${sql.raw(idDefault)},
100
+ email VARCHAR(255) UNIQUE NOT NULL,
101
+ display_name VARCHAR(255),
102
+ photo_url VARCHAR(500),
103
+ roles TEXT[] DEFAULT '{}' NOT NULL,
104
+ password_hash VARCHAR(255),
105
+ email_verified BOOLEAN DEFAULT FALSE NOT NULL,
106
+ email_verification_token VARCHAR(255),
107
+ email_verification_sent_at TIMESTAMP WITH TIME ZONE,
108
+ is_anonymous BOOLEAN DEFAULT FALSE NOT NULL,
109
+ metadata JSONB DEFAULT '{}' NOT NULL,
110
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
111
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
112
+ )
113
+ `);
114
+
115
+ // ── Create dependent auth tables (idempotent) ───────────────────
116
+
117
+ // Create user_identities table
118
+ await db.execute(sql`
119
+ CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (
120
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
121
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
122
+ provider TEXT NOT NULL,
123
+ provider_id TEXT NOT NULL,
124
+ profile_data JSONB,
125
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
126
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
127
+ UNIQUE(provider, provider_id)
128
+ )
129
+ `);
130
+
131
+ // Create indexes on user_identities
132
+ await db.execute(sql`
133
+ CREATE INDEX IF NOT EXISTS idx_user_identities_user
134
+ ON ${sql.raw(userIdentitiesTable)}(user_id)
135
+ `);
136
+
137
+
138
+ // Create refresh tokens table (includes user_agent, ip_address, and unique constraint)
139
+ await db.execute(sql`
140
+ CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
141
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
142
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
143
+ token_hash TEXT NOT NULL UNIQUE,
144
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
145
+ user_agent TEXT,
146
+ ip_address TEXT,
147
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
148
+ CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
149
+ )
150
+ `);
151
+
152
+ // Create index on token_hash for faster lookups
153
+ await db.execute(sql`
154
+ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash
155
+ ON ${sql.raw(refreshTokensTableName)}(token_hash)
156
+ `);
157
+
158
+ // Create index on user_id for cleanup operations
159
+ await db.execute(sql`
160
+ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
161
+ ON ${sql.raw(refreshTokensTableName)}(user_id)
162
+ `);
163
+
164
+ // Create password reset tokens table
165
+ await db.execute(sql`
166
+ CREATE TABLE IF NOT EXISTS ${sql.raw(passwordResetTokensTableName)} (
167
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
168
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
169
+ token_hash TEXT NOT NULL UNIQUE,
170
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
171
+ used_at TIMESTAMP WITH TIME ZONE,
172
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
173
+ )
174
+ `);
175
+
176
+ // Create index on token_hash for password reset lookups
177
+ await db.execute(sql`
178
+ CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash
179
+ ON ${sql.raw(passwordResetTokensTableName)}(token_hash)
180
+ `);
181
+
182
+ // Create index on user_id for password reset cleanup
183
+ await db.execute(sql`
184
+ CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
185
+ ON ${sql.raw(passwordResetTokensTableName)}(user_id)
186
+ `);
187
+
188
+ // Create magic link tokens table
189
+ const magicLinkTokensTableName = `"${authSchema}"."magic_link_tokens"`;
190
+ await db.execute(sql`
191
+ CREATE TABLE IF NOT EXISTS ${sql.raw(magicLinkTokensTableName)} (
192
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
193
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
194
+ token_hash TEXT NOT NULL UNIQUE,
195
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
196
+ used_at TIMESTAMP WITH TIME ZONE,
197
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
198
+ )
199
+ `);
200
+
201
+ // Create index on token_hash for magic link lookups
202
+ await db.execute(sql`
203
+ CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_hash
204
+ ON ${sql.raw(magicLinkTokensTableName)}(token_hash)
205
+ `);
206
+
207
+ // Create index on user_id for magic link cleanup
208
+ await db.execute(sql`
209
+ CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_user
210
+ ON ${sql.raw(magicLinkTokensTableName)}(user_id)
211
+ `);
212
+
213
+ // Create app config table
214
+ await db.execute(sql`
215
+ CREATE TABLE IF NOT EXISTS ${sql.raw(appConfigTableName)} (
216
+ key TEXT PRIMARY KEY,
217
+ value JSONB NOT NULL,
218
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
219
+ )
220
+ `);
221
+
222
+ // Create the `auth` schema with PostgreSQL RLS helper functions.
223
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
224
+
225
+ // Use an advisory transaction lock to serialize function recreation during HMR
226
+ await db.transaction(async (tx) => {
227
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
228
+
229
+ await tx.execute(sql`
230
+ CREATE OR REPLACE FUNCTION auth.uid() RETURNS text AS $$
231
+ SELECT NULLIF(current_setting('app.user_id', true), '');
232
+ $$ LANGUAGE sql STABLE
233
+ `);
234
+
235
+ await tx.execute(sql`
236
+ CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb AS $$
237
+ SELECT COALESCE(
238
+ NULLIF(current_setting('app.jwt', true), ''),
239
+ '{}'
240
+ )::jsonb;
241
+ $$ LANGUAGE sql STABLE
242
+ `);
243
+
244
+ await tx.execute(sql`
245
+ CREATE OR REPLACE FUNCTION auth.roles() RETURNS text AS $$
246
+ SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');
247
+ $$ LANGUAGE sql STABLE
248
+ `);
249
+ });
250
+
251
+ // Seed default roles if none exist
252
+ // (no-op: roles are now stored inline on the users table)
253
+
254
+ // ── Migration: Add is_anonymous column (safe for existing tables) ────
255
+ await db.execute(sql`
256
+ ALTER TABLE ${sql.raw(usersTableName)}
257
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
258
+ `);
259
+
260
+ // ── Migration: Add inline roles column (safe for existing tables) ────
261
+ await db.execute(sql`
262
+ ALTER TABLE ${sql.raw(usersTableName)}
263
+ ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
264
+ `);
265
+
266
+ // ── Migration: Copy roles from legacy junction table to inline column ──
267
+ // If the old rebase.user_roles and rebase.roles tables exist, migrate
268
+ // the data into the new TEXT[] column then drop the legacy tables.
269
+ try {
270
+ const legacyCheck = await db.execute(sql`
271
+ SELECT EXISTS (
272
+ SELECT 1 FROM information_schema.tables
273
+ WHERE table_schema = 'rebase' AND table_name = 'user_roles'
274
+ ) AS has_user_roles
275
+ `);
276
+ const hasLegacyTables = (legacyCheck.rows[0] as { has_user_roles: boolean }).has_user_roles;
277
+
278
+ if (hasLegacyTables) {
279
+ logger.info("🔄 Migrating roles from legacy user_roles table...");
280
+ // Update users' roles column from the junction table
281
+ await db.execute(sql`
282
+ UPDATE ${sql.raw(usersTableName)} u
283
+ SET roles = COALESCE((
284
+ SELECT array_agg(ur.role_id)
285
+ FROM "rebase"."user_roles" ur
286
+ WHERE ur.user_id = u.id
287
+ ), '{}')
288
+ WHERE u.roles = '{}' OR u.roles IS NULL
289
+ `);
290
+
291
+ // Drop legacy tables (junction first due to FK)
292
+ await db.execute(sql`DROP TABLE IF EXISTS "rebase"."user_roles" CASCADE`);
293
+ await db.execute(sql`DROP TABLE IF EXISTS "rebase"."roles" CASCADE`);
294
+ logger.info("✅ Legacy roles tables migrated and dropped");
295
+ }
296
+ } catch (migrationError: unknown) {
297
+ // Non-fatal: log and continue — the column exists and will work
298
+ logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
299
+ }
300
+
301
+ // ── MFA tables ──────────────────────────────────────────────────────
302
+ const mfaFactorsTableName = `"${authSchema}"."mfa_factors"`;
303
+ const mfaChallengesTableName = `"${authSchema}"."mfa_challenges"`;
304
+ const recoveryCodesTableName = `"${authSchema}"."recovery_codes"`;
305
+
306
+ // Create mfa_factors table
307
+ await db.execute(sql`
308
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
309
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
310
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
311
+ factor_type TEXT NOT NULL DEFAULT 'totp',
312
+ secret_encrypted TEXT NOT NULL,
313
+ friendly_name TEXT,
314
+ verified BOOLEAN DEFAULT FALSE,
315
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
316
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
317
+ )
318
+ `);
319
+
320
+ // Create indexes on mfa_factors
321
+ await db.execute(sql`
322
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
323
+ ON ${sql.raw(mfaFactorsTableName)}(user_id)
324
+ `);
325
+
326
+ // Create mfa_challenges table
327
+ await db.execute(sql`
328
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
329
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
330
+ factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
331
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
332
+ verified_at TIMESTAMP WITH TIME ZONE,
333
+ ip_address TEXT,
334
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL
335
+ )
336
+ `);
337
+
338
+ // Create indexes on mfa_challenges
339
+ await db.execute(sql`
340
+ CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
341
+ ON ${sql.raw(mfaChallengesTableName)}(factor_id)
342
+ `);
343
+
344
+ // Create recovery_codes table
345
+ await db.execute(sql`
346
+ CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
347
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
348
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
349
+ code_hash TEXT NOT NULL,
350
+ used_at TIMESTAMP WITH TIME ZONE,
351
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
352
+ )
353
+ `);
354
+
355
+ // Create indexes on recovery_codes
356
+ await db.execute(sql`
357
+ CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
358
+ ON ${sql.raw(recoveryCodesTableName)}(user_id)
359
+ `);
360
+
361
+ logger.info("✅ Auth tables ready");
362
+ } catch (error) {
363
+ logger.error("❌ Failed to create auth tables", { error });
364
+ logger.warn("⚠️ Continuing without creating auth tables.");
365
+ }
366
+ }
367
+