@rebasepro/server-postgres 0.9.1-canary.16c42e9 → 0.9.1-canary.26fe4b2

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 (35) hide show
  1. package/README.md +21 -0
  2. package/dist/PostgresBackendDriver.d.ts +18 -0
  3. package/dist/PostgresBootstrapper.d.ts +7 -1
  4. package/dist/auth/services.d.ts +53 -53
  5. package/dist/index.es.js +730 -194
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
  8. package/dist/schema/auth-schema.d.ts +24 -24
  9. package/dist/schema/doctor.d.ts +1 -1
  10. package/dist/schema/introspect-db-logic.d.ts +0 -5
  11. package/dist/schema/introspect-db-naming.d.ts +10 -0
  12. package/dist/security/policy-drift.d.ts +30 -0
  13. package/dist/security/rls-enforcement.d.ts +2 -2
  14. package/dist/services/channel-history.d.ts +118 -0
  15. package/dist/services/realtimeService.d.ts +69 -2
  16. package/package.json +8 -31
  17. package/src/PostgresBackendDriver.ts +56 -5
  18. package/src/PostgresBootstrapper.ts +18 -1
  19. package/src/auth/ensure-tables.ts +97 -17
  20. package/src/auth/services.ts +134 -133
  21. package/src/cli.ts +60 -0
  22. package/src/schema/auth-bootstrap-sql.ts +7 -1
  23. package/src/schema/auth-schema.ts +13 -13
  24. package/src/schema/doctor.ts +45 -20
  25. package/src/schema/introspect-db-inference.ts +1 -1
  26. package/src/schema/introspect-db-logic.ts +1 -10
  27. package/src/schema/introspect-db-naming.ts +15 -0
  28. package/src/schema/introspect-runtime.ts +1 -1
  29. package/src/security/policy-drift.test.ts +106 -1
  30. package/src/security/policy-drift.ts +56 -0
  31. package/src/security/rls-enforcement.ts +11 -5
  32. package/src/services/channel-history.ts +343 -0
  33. package/src/services/realtimeService.ts +198 -10
  34. package/src/services/row-pipeline.ts +27 -3
  35. package/src/websocket.ts +30 -11
@@ -112,13 +112,87 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
112
112
  )
113
113
  `);
114
114
 
115
+ // ── Migration: auth FK column user_id → uid, phase 1 (expand) ───────
116
+ // Must run BEFORE the dependent tables below: CREATE TABLE IF NOT
117
+ // EXISTS never revisits an existing table, so a database provisioned
118
+ // before this migration still lacks `uid` — and the
119
+ // CREATE INDEX ... (uid) statements that follow would fail on it.
120
+ //
121
+ // Deliberately NOT a plain RENAME. Both Cloud Run and Kubernetes roll
122
+ // deploys, so old and new pods serve the same database at the same time,
123
+ // and a rollback puts old code back in front of a migrated database. A
124
+ // rename breaks every auth query on whichever side is out of step.
125
+ // Instead: add `uid`, backfill it, drop the NOT NULL on `user_id`, and
126
+ // keep the two in sync with a trigger, so a backend of either era can
127
+ // read and write. `scripts/drop-legacy-auth-user-id.sql` removes the
128
+ // column once no old backend remains (phase 2, contract).
129
+ //
130
+ // Idempotent throughout: every step is guarded on catalogue state.
131
+ await db.execute(sql`
132
+ CREATE OR REPLACE FUNCTION ${sql.raw(`"${authSchema}"`)}.sync_uid_user_id() RETURNS trigger AS $$
133
+ BEGIN
134
+ IF NEW.uid IS NULL AND NEW.user_id IS NOT NULL THEN
135
+ NEW.uid := NEW.user_id;
136
+ ELSIF NEW.user_id IS NULL AND NEW.uid IS NOT NULL THEN
137
+ NEW.user_id := NEW.uid;
138
+ END IF;
139
+ RETURN NEW;
140
+ END $$ LANGUAGE plpgsql
141
+ `);
142
+
143
+ for (const authTable of [
144
+ "user_identities",
145
+ "refresh_tokens",
146
+ "password_reset_tokens",
147
+ "magic_link_tokens",
148
+ "mfa_factors",
149
+ "recovery_codes"
150
+ ]) {
151
+ const qualified = `"${authSchema}"."${authTable}"`;
152
+ await db.execute(sql`
153
+ DO $$
154
+ DECLARE
155
+ has_legacy boolean;
156
+ has_uid boolean;
157
+ BEGIN
158
+ SELECT
159
+ bool_or(column_name = 'user_id'),
160
+ bool_or(column_name = 'uid')
161
+ INTO has_legacy, has_uid
162
+ FROM information_schema.columns
163
+ WHERE table_schema = ${sql.raw(`'${authSchema}'`)}
164
+ AND table_name = ${sql.raw(`'${authTable}'`)};
165
+
166
+ -- Table absent, or already uid-only (a fresh install, or
167
+ -- phase 2 already run): nothing to do.
168
+ IF has_legacy IS NOT TRUE THEN
169
+ RETURN;
170
+ END IF;
171
+
172
+ IF has_uid IS NOT TRUE THEN
173
+ EXECUTE ${sql.raw(`'ALTER TABLE ${qualified} ADD COLUMN uid ${userIdType} REFERENCES ${usersTableName}(id) ON DELETE CASCADE'`)};
174
+ EXECUTE ${sql.raw(`'UPDATE ${qualified} SET uid = user_id WHERE uid IS NULL'`)};
175
+ EXECUTE ${sql.raw(`'CREATE INDEX IF NOT EXISTS idx_${authTable}_uid ON ${qualified}(uid)'`)};
176
+ END IF;
177
+
178
+ -- New code inserts uid and never user_id, so the legacy
179
+ -- column can no longer be NOT NULL. The trigger below
180
+ -- backfills it, but the constraint is checked first.
181
+ EXECUTE ${sql.raw(`'ALTER TABLE ${qualified} ALTER COLUMN user_id DROP NOT NULL'`)};
182
+
183
+ EXECUTE ${sql.raw(`'DROP TRIGGER IF EXISTS sync_uid_user_id ON ${qualified}'`)};
184
+ EXECUTE ${sql.raw(`'CREATE TRIGGER sync_uid_user_id BEFORE INSERT OR UPDATE ON ${qualified} FOR EACH ROW EXECUTE FUNCTION "${authSchema}".sync_uid_user_id()'`)};
185
+ END $$
186
+ `);
187
+ }
188
+
115
189
  // ── Create dependent auth tables (idempotent) ───────────────────
116
190
 
117
191
  // Create user_identities table
118
192
  await db.execute(sql`
119
193
  CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (
120
194
  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,
195
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
122
196
  provider TEXT NOT NULL,
123
197
  provider_id TEXT NOT NULL,
124
198
  profile_data JSONB,
@@ -131,7 +205,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
131
205
  // Create indexes on user_identities
132
206
  await db.execute(sql`
133
207
  CREATE INDEX IF NOT EXISTS idx_user_identities_user
134
- ON ${sql.raw(userIdentitiesTable)}(user_id)
208
+ ON ${sql.raw(userIdentitiesTable)}(uid)
135
209
  `);
136
210
 
137
211
 
@@ -139,13 +213,13 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
139
213
  await db.execute(sql`
140
214
  CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
141
215
  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,
216
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
143
217
  token_hash TEXT NOT NULL UNIQUE,
144
218
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
145
219
  user_agent TEXT,
146
220
  ip_address TEXT,
147
221
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
148
- CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
222
+ CONSTRAINT unique_device_session UNIQUE (uid, user_agent, ip_address)
149
223
  )
150
224
  `);
151
225
 
@@ -155,17 +229,17 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
155
229
  ON ${sql.raw(refreshTokensTableName)}(token_hash)
156
230
  `);
157
231
 
158
- // Create index on user_id for cleanup operations
232
+ // Create index on uid for cleanup operations
159
233
  await db.execute(sql`
160
234
  CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
161
- ON ${sql.raw(refreshTokensTableName)}(user_id)
235
+ ON ${sql.raw(refreshTokensTableName)}(uid)
162
236
  `);
163
237
 
164
238
  // Create password reset tokens table
165
239
  await db.execute(sql`
166
240
  CREATE TABLE IF NOT EXISTS ${sql.raw(passwordResetTokensTableName)} (
167
241
  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,
242
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
169
243
  token_hash TEXT NOT NULL UNIQUE,
170
244
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
171
245
  used_at TIMESTAMP WITH TIME ZONE,
@@ -179,10 +253,10 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
179
253
  ON ${sql.raw(passwordResetTokensTableName)}(token_hash)
180
254
  `);
181
255
 
182
- // Create index on user_id for password reset cleanup
256
+ // Create index on uid for password reset cleanup
183
257
  await db.execute(sql`
184
258
  CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
185
- ON ${sql.raw(passwordResetTokensTableName)}(user_id)
259
+ ON ${sql.raw(passwordResetTokensTableName)}(uid)
186
260
  `);
187
261
 
188
262
  // Create magic link tokens table
@@ -190,7 +264,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
190
264
  await db.execute(sql`
191
265
  CREATE TABLE IF NOT EXISTS ${sql.raw(magicLinkTokensTableName)} (
192
266
  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,
267
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
194
268
  token_hash TEXT NOT NULL UNIQUE,
195
269
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
196
270
  used_at TIMESTAMP WITH TIME ZONE,
@@ -204,10 +278,10 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
204
278
  ON ${sql.raw(magicLinkTokensTableName)}(token_hash)
205
279
  `);
206
280
 
207
- // Create index on user_id for magic link cleanup
281
+ // Create index on uid for magic link cleanup
208
282
  await db.execute(sql`
209
283
  CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_user
210
- ON ${sql.raw(magicLinkTokensTableName)}(user_id)
284
+ ON ${sql.raw(magicLinkTokensTableName)}(uid)
211
285
  `);
212
286
 
213
287
  // Create app config table
@@ -226,9 +300,15 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
226
300
  await db.transaction(async (tx) => {
227
301
  await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
228
302
 
303
+ // Falls back to the pre-rename `app.user_id` so a database that has
304
+ // taken the new schema but is still served by an older backend keeps
305
+ // resolving the principal. Keep in sync with AUTH_BOOTSTRAP_SQL.
229
306
  await tx.execute(sql`
230
307
  CREATE OR REPLACE FUNCTION auth.uid() RETURNS text AS $$
231
- SELECT NULLIF(current_setting('app.user_id', true), '');
308
+ SELECT COALESCE(
309
+ NULLIF(current_setting('app.uid', true), ''),
310
+ NULLIF(current_setting('app.user_id', true), '')
311
+ );
232
312
  $$ LANGUAGE sql STABLE
233
313
  `);
234
314
 
@@ -322,7 +402,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
322
402
  await db.execute(sql`
323
403
  CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
324
404
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
325
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
405
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
326
406
  factor_type TEXT NOT NULL DEFAULT 'totp',
327
407
  secret_encrypted TEXT NOT NULL,
328
408
  friendly_name TEXT,
@@ -335,7 +415,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
335
415
  // Create indexes on mfa_factors
336
416
  await db.execute(sql`
337
417
  CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
338
- ON ${sql.raw(mfaFactorsTableName)}(user_id)
418
+ ON ${sql.raw(mfaFactorsTableName)}(uid)
339
419
  `);
340
420
 
341
421
  // Create mfa_challenges table
@@ -360,7 +440,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
360
440
  await db.execute(sql`
361
441
  CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
362
442
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
363
- user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
443
+ uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
364
444
  code_hash TEXT NOT NULL,
365
445
  used_at TIMESTAMP WITH TIME ZONE,
366
446
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
@@ -370,7 +450,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
370
450
  // Create indexes on recovery_codes
371
451
  await db.execute(sql`
372
452
  CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
373
- ON ${sql.raw(recoveryCodesTableName)}(user_id)
453
+ ON ${sql.raw(recoveryCodesTableName)}(uid)
374
454
  `);
375
455
 
376
456
  // ── Migration: clear stale FORCE ROW LEVEL SECURITY (older RLS model) ──