@rebasepro/server-postgres 0.9.1-canary.a639738 → 0.9.1-canary.a8dbf1c
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.
- package/README.md +21 -0
- package/dist/PostgresBackendDriver.d.ts +18 -0
- package/dist/PostgresBootstrapper.d.ts +7 -1
- package/dist/auth/services.d.ts +53 -53
- package/dist/index.es.js +708 -191
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-bootstrap-sql.d.ts +1 -1
- package/dist/schema/auth-schema.d.ts +24 -24
- package/dist/schema/introspect-db-logic.d.ts +0 -5
- package/dist/schema/introspect-db-naming.d.ts +10 -0
- package/dist/security/rls-enforcement.d.ts +2 -2
- package/dist/services/channel-history.d.ts +118 -0
- package/dist/services/realtimeService.d.ts +69 -2
- package/package.json +7 -31
- package/src/PostgresBackendDriver.ts +56 -5
- package/src/PostgresBootstrapper.ts +18 -1
- package/src/auth/ensure-tables.ts +97 -17
- package/src/auth/services.ts +134 -133
- package/src/schema/auth-bootstrap-sql.ts +7 -1
- package/src/schema/auth-schema.ts +13 -13
- package/src/schema/introspect-db-inference.ts +1 -1
- package/src/schema/introspect-db-logic.ts +1 -10
- package/src/schema/introspect-db-naming.ts +15 -0
- package/src/schema/introspect-runtime.ts +1 -1
- package/src/security/rls-enforcement.ts +11 -5
- package/src/services/channel-history.ts +343 -0
- package/src/services/realtimeService.ts +198 -10
- 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
|
-
|
|
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)}(
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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)}(
|
|
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
|
-
|
|
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
|
|
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)}(
|
|
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
|
-
|
|
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
|
|
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)}(
|
|
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
|
|
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
|
-
|
|
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)}(
|
|
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
|
-
|
|
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)}(
|
|
453
|
+
ON ${sql.raw(recoveryCodesTableName)}(uid)
|
|
374
454
|
`);
|
|
375
455
|
|
|
376
456
|
// ── Migration: clear stale FORCE ROW LEVEL SECURITY (older RLS model) ──
|