@rebasepro/server-postgres 0.10.0 → 0.10.1-canary.31c773c
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/dist/index.es.js +47 -0
- package/dist/index.es.js.map +1 -1
- package/package.json +6 -6
- package/src/auth/ensure-tables.ts +74 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgres",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.1-canary.31c773c",
|
|
5
5
|
"description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"execa": "^9.6.1",
|
|
48
48
|
"pg": "^8.21.0",
|
|
49
49
|
"ws": "^8.21.0",
|
|
50
|
-
"@rebasepro/codegen": "0.10.
|
|
51
|
-
"@rebasepro/common": "0.10.
|
|
52
|
-
"@rebasepro/
|
|
53
|
-
"@rebasepro/
|
|
54
|
-
"@rebasepro/
|
|
50
|
+
"@rebasepro/codegen": "0.10.1-canary.31c773c",
|
|
51
|
+
"@rebasepro/common": "0.10.1-canary.31c773c",
|
|
52
|
+
"@rebasepro/server": "0.10.1-canary.31c773c",
|
|
53
|
+
"@rebasepro/types": "0.10.1-canary.31c773c",
|
|
54
|
+
"@rebasepro/utils": "0.10.1-canary.31c773c"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@hono/node-server": "^2.0.9",
|
|
@@ -358,6 +358,80 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
|
|
|
358
358
|
`);
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
// ── Migration: reconcile refresh_tokens for the device-session upsert ──
|
|
362
|
+
// The refresh-token rotation does `INSERT ... ON CONFLICT (uid, user_agent,
|
|
363
|
+
// ip_address) DO UPDATE`, which requires the `unique_device_session`
|
|
364
|
+
// constraint. `CREATE TABLE IF NOT EXISTS` never adds it to a table that
|
|
365
|
+
// predates the constraint, so every login on such a database 500s with
|
|
366
|
+
// `42P10 no unique or exclusion constraint matching the ON CONFLICT`.
|
|
367
|
+
// Reconcile EVERY table named refresh_tokens, in whatever schema it lives,
|
|
368
|
+
// rather than only the derived name: a database provisioned by an older
|
|
369
|
+
// era can carry the table in a different schema than the one this run
|
|
370
|
+
// derives, and the login upsert then hits a table without the constraint.
|
|
371
|
+
try {
|
|
372
|
+
const rtTables = await db.execute(sql`
|
|
373
|
+
SELECT table_schema, table_name
|
|
374
|
+
FROM information_schema.tables
|
|
375
|
+
WHERE table_name = 'refresh_tokens'
|
|
376
|
+
`);
|
|
377
|
+
const found = (rtTables.rows as { table_schema: string; table_name: string }[]);
|
|
378
|
+
logger.info(`🔍 refresh_tokens reconcile: found ${found.length} table(s): ${found.map(r => `"${r.table_schema}"."${r.table_name}"`).join(", ") || "(none)"}`);
|
|
379
|
+
for (const { table_schema } of found) {
|
|
380
|
+
const qualified = `"${table_schema}"."refresh_tokens"`;
|
|
381
|
+
try {
|
|
382
|
+
// Old tables may lack these columns entirely.
|
|
383
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS user_agent TEXT`);
|
|
384
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS ip_address TEXT`);
|
|
385
|
+
// The upsert relies on '' (not NULL) so a device with no UA/IP collides.
|
|
386
|
+
await db.execute(sql`UPDATE ${sql.raw(qualified)} SET user_agent = '' WHERE user_agent IS NULL`);
|
|
387
|
+
await db.execute(sql`UPDATE ${sql.raw(qualified)} SET ip_address = '' WHERE ip_address IS NULL`);
|
|
388
|
+
// Drop pre-existing duplicates that would block the unique constraint,
|
|
389
|
+
// keeping one row per (uid, user_agent, ip_address).
|
|
390
|
+
await db.execute(sql`
|
|
391
|
+
DELETE FROM ${sql.raw(qualified)} a
|
|
392
|
+
USING ${sql.raw(qualified)} b
|
|
393
|
+
WHERE a.ctid < b.ctid
|
|
394
|
+
AND a.uid = b.uid
|
|
395
|
+
AND a.user_agent = b.user_agent
|
|
396
|
+
AND a.ip_address = b.ip_address
|
|
397
|
+
`);
|
|
398
|
+
// Log every unique constraint on the table so drift is visible.
|
|
399
|
+
const uniques = await db.execute(sql`
|
|
400
|
+
SELECT conname, pg_get_constraintdef(oid) AS def
|
|
401
|
+
FROM pg_constraint
|
|
402
|
+
WHERE conrelid = ${sql.raw(`'${qualified}'`)}::regclass AND contype = 'u'
|
|
403
|
+
`);
|
|
404
|
+
for (const u of uniques.rows as { conname: string; def: string }[]) {
|
|
405
|
+
logger.info(` ${qualified} unique: ${u.conname} → ${u.def}`);
|
|
406
|
+
}
|
|
407
|
+
// Does a usable unique constraint cover EXACTLY (uid, user_agent, ip_address)?
|
|
408
|
+
// Checking by name alone is not enough: an older era may carry a
|
|
409
|
+
// `unique_device_session` on different columns, which the ON CONFLICT
|
|
410
|
+
// cannot infer, so login keeps 500ing with 42P10.
|
|
411
|
+
const hasCorrect = (uniques.rows as { def: string }[]).some((u) => {
|
|
412
|
+
const cols = (u.def.match(/\(([^)]*)\)/)?.[1] || "")
|
|
413
|
+
.split(",").map((c) => c.trim().replace(/"/g, ""));
|
|
414
|
+
return cols.length === 3 && cols.includes("uid") && cols.includes("user_agent") && cols.includes("ip_address");
|
|
415
|
+
});
|
|
416
|
+
if (hasCorrect) {
|
|
417
|
+
logger.info(`✓ correct device-session unique already present on ${qualified}`);
|
|
418
|
+
} else {
|
|
419
|
+
// Remove a wrong-columned constraint squatting on the name, then add the right one.
|
|
420
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} DROP CONSTRAINT IF EXISTS unique_device_session`);
|
|
421
|
+
await db.execute(sql`
|
|
422
|
+
ALTER TABLE ${sql.raw(qualified)}
|
|
423
|
+
ADD CONSTRAINT unique_device_session UNIQUE (uid, user_agent, ip_address)
|
|
424
|
+
`);
|
|
425
|
+
logger.info(`✅ Added correct unique_device_session constraint to ${qualified}`);
|
|
426
|
+
}
|
|
427
|
+
} catch (perTableError: unknown) {
|
|
428
|
+
logger.warn(`⚠️ refresh_tokens reconcile failed for ${qualified}: ${perTableError instanceof Error ? perTableError.message : String(perTableError)}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (migrationError: unknown) {
|
|
432
|
+
logger.warn(`⚠️ refresh_tokens device-session migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
361
435
|
// ── Migration: Copy roles from legacy junction table to inline column ──
|
|
362
436
|
// If the old rebase.user_roles and rebase.roles tables exist, migrate
|
|
363
437
|
// the data into the new TEXT[] column then drop the legacy tables.
|