@rebasepro/server-postgres 0.10.1-canary.18115ba → 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/auth/services.d.ts +4 -43
- package/dist/index.es.js +71 -212
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-schema.d.ts +0 -170
- package/package.json +6 -6
- package/src/auth/ensure-tables.ts +56 -70
- package/src/auth/services.ts +48 -186
- package/src/schema/auth-schema.ts +3 -41
|
@@ -232,23 +232,6 @@ export declare function createAuthSchema(usersSchemaName?: string): {
|
|
|
232
232
|
}, {}, {
|
|
233
233
|
$type: Record<string, unknown>;
|
|
234
234
|
}>;
|
|
235
|
-
tokensValidAfter: import("drizzle-orm/pg-core").PgColumn<{
|
|
236
|
-
name: "tokens_valid_after";
|
|
237
|
-
tableName: "users";
|
|
238
|
-
dataType: "date";
|
|
239
|
-
columnType: "PgTimestamp";
|
|
240
|
-
data: Date;
|
|
241
|
-
driverParam: string;
|
|
242
|
-
notNull: false;
|
|
243
|
-
hasDefault: false;
|
|
244
|
-
isPrimaryKey: false;
|
|
245
|
-
isAutoincrement: false;
|
|
246
|
-
hasRuntimeDefault: false;
|
|
247
|
-
enumValues: undefined;
|
|
248
|
-
baseColumn: never;
|
|
249
|
-
identity: undefined;
|
|
250
|
-
generated: undefined;
|
|
251
|
-
}, {}, {}>;
|
|
252
235
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
253
236
|
name: "created_at";
|
|
254
237
|
tableName: "users";
|
|
@@ -324,23 +307,6 @@ export declare function createAuthSchema(usersSchemaName?: string): {
|
|
|
324
307
|
identity: undefined;
|
|
325
308
|
generated: undefined;
|
|
326
309
|
}, {}, {}>;
|
|
327
|
-
sessionId: import("drizzle-orm/pg-core").PgColumn<{
|
|
328
|
-
name: "session_id";
|
|
329
|
-
tableName: "refresh_tokens";
|
|
330
|
-
dataType: "string";
|
|
331
|
-
columnType: "PgUUID";
|
|
332
|
-
data: string;
|
|
333
|
-
driverParam: string;
|
|
334
|
-
notNull: true;
|
|
335
|
-
hasDefault: true;
|
|
336
|
-
isPrimaryKey: false;
|
|
337
|
-
isAutoincrement: false;
|
|
338
|
-
hasRuntimeDefault: false;
|
|
339
|
-
enumValues: undefined;
|
|
340
|
-
baseColumn: never;
|
|
341
|
-
identity: undefined;
|
|
342
|
-
generated: undefined;
|
|
343
|
-
}, {}, {}>;
|
|
344
310
|
tokenHash: import("drizzle-orm/pg-core").PgColumn<{
|
|
345
311
|
name: "token_hash";
|
|
346
312
|
tableName: "refresh_tokens";
|
|
@@ -377,57 +343,6 @@ export declare function createAuthSchema(usersSchemaName?: string): {
|
|
|
377
343
|
identity: undefined;
|
|
378
344
|
generated: undefined;
|
|
379
345
|
}, {}, {}>;
|
|
380
|
-
revoked: import("drizzle-orm/pg-core").PgColumn<{
|
|
381
|
-
name: "revoked";
|
|
382
|
-
tableName: "refresh_tokens";
|
|
383
|
-
dataType: "boolean";
|
|
384
|
-
columnType: "PgBoolean";
|
|
385
|
-
data: boolean;
|
|
386
|
-
driverParam: boolean;
|
|
387
|
-
notNull: true;
|
|
388
|
-
hasDefault: true;
|
|
389
|
-
isPrimaryKey: false;
|
|
390
|
-
isAutoincrement: false;
|
|
391
|
-
hasRuntimeDefault: false;
|
|
392
|
-
enumValues: undefined;
|
|
393
|
-
baseColumn: never;
|
|
394
|
-
identity: undefined;
|
|
395
|
-
generated: undefined;
|
|
396
|
-
}, {}, {}>;
|
|
397
|
-
rotatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
398
|
-
name: "rotated_at";
|
|
399
|
-
tableName: "refresh_tokens";
|
|
400
|
-
dataType: "date";
|
|
401
|
-
columnType: "PgTimestamp";
|
|
402
|
-
data: Date;
|
|
403
|
-
driverParam: string;
|
|
404
|
-
notNull: false;
|
|
405
|
-
hasDefault: false;
|
|
406
|
-
isPrimaryKey: false;
|
|
407
|
-
isAutoincrement: false;
|
|
408
|
-
hasRuntimeDefault: false;
|
|
409
|
-
enumValues: undefined;
|
|
410
|
-
baseColumn: never;
|
|
411
|
-
identity: undefined;
|
|
412
|
-
generated: undefined;
|
|
413
|
-
}, {}, {}>;
|
|
414
|
-
sessionStartedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
415
|
-
name: "session_started_at";
|
|
416
|
-
tableName: "refresh_tokens";
|
|
417
|
-
dataType: "date";
|
|
418
|
-
columnType: "PgTimestamp";
|
|
419
|
-
data: Date;
|
|
420
|
-
driverParam: string;
|
|
421
|
-
notNull: true;
|
|
422
|
-
hasDefault: true;
|
|
423
|
-
isPrimaryKey: false;
|
|
424
|
-
isAutoincrement: false;
|
|
425
|
-
hasRuntimeDefault: false;
|
|
426
|
-
enumValues: undefined;
|
|
427
|
-
baseColumn: never;
|
|
428
|
-
identity: undefined;
|
|
429
|
-
generated: undefined;
|
|
430
|
-
}, {}, {}>;
|
|
431
346
|
userAgent: import("drizzle-orm/pg-core").PgColumn<{
|
|
432
347
|
name: "user_agent";
|
|
433
348
|
tableName: "refresh_tokens";
|
|
@@ -1483,23 +1398,6 @@ export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
1483
1398
|
}, {}, {
|
|
1484
1399
|
$type: Record<string, unknown>;
|
|
1485
1400
|
}>;
|
|
1486
|
-
tokensValidAfter: import("drizzle-orm/pg-core").PgColumn<{
|
|
1487
|
-
name: "tokens_valid_after";
|
|
1488
|
-
tableName: "users";
|
|
1489
|
-
dataType: "date";
|
|
1490
|
-
columnType: "PgTimestamp";
|
|
1491
|
-
data: Date;
|
|
1492
|
-
driverParam: string;
|
|
1493
|
-
notNull: false;
|
|
1494
|
-
hasDefault: false;
|
|
1495
|
-
isPrimaryKey: false;
|
|
1496
|
-
isAutoincrement: false;
|
|
1497
|
-
hasRuntimeDefault: false;
|
|
1498
|
-
enumValues: undefined;
|
|
1499
|
-
baseColumn: never;
|
|
1500
|
-
identity: undefined;
|
|
1501
|
-
generated: undefined;
|
|
1502
|
-
}, {}, {}>;
|
|
1503
1401
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1504
1402
|
name: "created_at";
|
|
1505
1403
|
tableName: "users";
|
|
@@ -1575,23 +1473,6 @@ export declare const refreshTokens: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
1575
1473
|
identity: undefined;
|
|
1576
1474
|
generated: undefined;
|
|
1577
1475
|
}, {}, {}>;
|
|
1578
|
-
sessionId: import("drizzle-orm/pg-core").PgColumn<{
|
|
1579
|
-
name: "session_id";
|
|
1580
|
-
tableName: "refresh_tokens";
|
|
1581
|
-
dataType: "string";
|
|
1582
|
-
columnType: "PgUUID";
|
|
1583
|
-
data: string;
|
|
1584
|
-
driverParam: string;
|
|
1585
|
-
notNull: true;
|
|
1586
|
-
hasDefault: true;
|
|
1587
|
-
isPrimaryKey: false;
|
|
1588
|
-
isAutoincrement: false;
|
|
1589
|
-
hasRuntimeDefault: false;
|
|
1590
|
-
enumValues: undefined;
|
|
1591
|
-
baseColumn: never;
|
|
1592
|
-
identity: undefined;
|
|
1593
|
-
generated: undefined;
|
|
1594
|
-
}, {}, {}>;
|
|
1595
1476
|
tokenHash: import("drizzle-orm/pg-core").PgColumn<{
|
|
1596
1477
|
name: "token_hash";
|
|
1597
1478
|
tableName: "refresh_tokens";
|
|
@@ -1628,57 +1509,6 @@ export declare const refreshTokens: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
1628
1509
|
identity: undefined;
|
|
1629
1510
|
generated: undefined;
|
|
1630
1511
|
}, {}, {}>;
|
|
1631
|
-
revoked: import("drizzle-orm/pg-core").PgColumn<{
|
|
1632
|
-
name: "revoked";
|
|
1633
|
-
tableName: "refresh_tokens";
|
|
1634
|
-
dataType: "boolean";
|
|
1635
|
-
columnType: "PgBoolean";
|
|
1636
|
-
data: boolean;
|
|
1637
|
-
driverParam: boolean;
|
|
1638
|
-
notNull: true;
|
|
1639
|
-
hasDefault: true;
|
|
1640
|
-
isPrimaryKey: false;
|
|
1641
|
-
isAutoincrement: false;
|
|
1642
|
-
hasRuntimeDefault: false;
|
|
1643
|
-
enumValues: undefined;
|
|
1644
|
-
baseColumn: never;
|
|
1645
|
-
identity: undefined;
|
|
1646
|
-
generated: undefined;
|
|
1647
|
-
}, {}, {}>;
|
|
1648
|
-
rotatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1649
|
-
name: "rotated_at";
|
|
1650
|
-
tableName: "refresh_tokens";
|
|
1651
|
-
dataType: "date";
|
|
1652
|
-
columnType: "PgTimestamp";
|
|
1653
|
-
data: Date;
|
|
1654
|
-
driverParam: string;
|
|
1655
|
-
notNull: false;
|
|
1656
|
-
hasDefault: false;
|
|
1657
|
-
isPrimaryKey: false;
|
|
1658
|
-
isAutoincrement: false;
|
|
1659
|
-
hasRuntimeDefault: false;
|
|
1660
|
-
enumValues: undefined;
|
|
1661
|
-
baseColumn: never;
|
|
1662
|
-
identity: undefined;
|
|
1663
|
-
generated: undefined;
|
|
1664
|
-
}, {}, {}>;
|
|
1665
|
-
sessionStartedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1666
|
-
name: "session_started_at";
|
|
1667
|
-
tableName: "refresh_tokens";
|
|
1668
|
-
dataType: "date";
|
|
1669
|
-
columnType: "PgTimestamp";
|
|
1670
|
-
data: Date;
|
|
1671
|
-
driverParam: string;
|
|
1672
|
-
notNull: true;
|
|
1673
|
-
hasDefault: true;
|
|
1674
|
-
isPrimaryKey: false;
|
|
1675
|
-
isAutoincrement: false;
|
|
1676
|
-
hasRuntimeDefault: false;
|
|
1677
|
-
enumValues: undefined;
|
|
1678
|
-
baseColumn: never;
|
|
1679
|
-
identity: undefined;
|
|
1680
|
-
generated: undefined;
|
|
1681
|
-
}, {}, {}>;
|
|
1682
1512
|
userAgent: import("drizzle-orm/pg-core").PgColumn<{
|
|
1683
1513
|
name: "user_agent";
|
|
1684
1514
|
tableName: "refresh_tokens";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgres",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.10.1-canary.
|
|
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.1-canary.
|
|
51
|
-
"@rebasepro/common": "0.10.1-canary.
|
|
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",
|
|
@@ -107,7 +107,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
|
|
|
107
107
|
email_verification_sent_at TIMESTAMP WITH TIME ZONE,
|
|
108
108
|
is_anonymous BOOLEAN DEFAULT FALSE NOT NULL,
|
|
109
109
|
metadata JSONB DEFAULT '{}' NOT NULL,
|
|
110
|
-
tokens_valid_after TIMESTAMP WITH TIME ZONE,
|
|
111
110
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
112
111
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
|
113
112
|
)
|
|
@@ -210,23 +209,17 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
|
|
|
210
209
|
`);
|
|
211
210
|
|
|
212
211
|
|
|
213
|
-
// Create refresh tokens table
|
|
214
|
-
// sign-in by session_id — deliberately without a uniqueness rule on
|
|
215
|
-
// (uid, user_agent, ip_address); see the schema module for why that
|
|
216
|
-
// constraint had to go.
|
|
212
|
+
// Create refresh tokens table (includes user_agent, ip_address, and unique constraint)
|
|
217
213
|
await db.execute(sql`
|
|
218
214
|
CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
|
|
219
215
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
220
216
|
uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
|
|
221
|
-
session_id TEXT NOT NULL DEFAULT gen_random_uuid()::text,
|
|
222
217
|
token_hash TEXT NOT NULL UNIQUE,
|
|
223
218
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
224
|
-
revoked BOOLEAN DEFAULT FALSE NOT NULL,
|
|
225
|
-
rotated_at TIMESTAMP WITH TIME ZONE,
|
|
226
|
-
session_started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
|
|
227
219
|
user_agent TEXT,
|
|
228
220
|
ip_address TEXT,
|
|
229
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
221
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
222
|
+
CONSTRAINT unique_device_session UNIQUE (uid, user_agent, ip_address)
|
|
230
223
|
)
|
|
231
224
|
`);
|
|
232
225
|
|
|
@@ -355,7 +348,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
|
|
|
355
348
|
"email_verification_sent_at TIMESTAMP WITH TIME ZONE",
|
|
356
349
|
"is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
|
|
357
350
|
"metadata JSONB DEFAULT '{}' NOT NULL",
|
|
358
|
-
"tokens_valid_after TIMESTAMP WITH TIME ZONE",
|
|
359
351
|
"created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL",
|
|
360
352
|
"updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"
|
|
361
353
|
];
|
|
@@ -366,26 +358,16 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
|
|
|
366
358
|
`);
|
|
367
359
|
}
|
|
368
360
|
|
|
369
|
-
// ── Migration: refresh_tokens
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
// rotations so `users.tokens_valid_after` cannot be outrun.
|
|
380
|
-
// 2. The removal of `unique_device_session`. It made (uid,
|
|
381
|
-
// user_agent, ip_address) the identity of a session, which evicted
|
|
382
|
-
// a second browser profile behind one NAT and churned rows as
|
|
383
|
-
// phones changed networks. UA and IP are metadata now.
|
|
384
|
-
//
|
|
385
|
-
// Every existing row is adopted rather than dropped: it keeps its
|
|
386
|
-
// token_hash, gets a session of its own, and stays unrevoked — so the
|
|
387
|
-
// sessions live in browsers right now survive the upgrade rather than
|
|
388
|
-
// everyone being signed out by the fix for being signed out.
|
|
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.
|
|
389
371
|
try {
|
|
390
372
|
const rtTables = await db.execute(sql`
|
|
391
373
|
SELECT table_schema, table_name
|
|
@@ -397,53 +379,57 @@ export async function ensureAuthTablesExist(db: NodePgDatabase, collection?: Col
|
|
|
397
379
|
for (const { table_schema } of found) {
|
|
398
380
|
const qualified = `"${table_schema}"."refresh_tokens"`;
|
|
399
381
|
try {
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
await db.execute(sql`
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_started_at TIMESTAMP WITH TIME ZONE`);
|
|
409
|
-
|
|
410
|
-
// One session per pre-existing row: under the old model a row
|
|
411
|
-
// WAS a device session, and there is no record of which rows
|
|
412
|
-
// descended from the same sign-in.
|
|
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).
|
|
413
390
|
await db.execute(sql`
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
WHERE
|
|
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
|
|
417
397
|
`);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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'
|
|
422
403
|
`);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
+
}
|
|
441
427
|
} catch (perTableError: unknown) {
|
|
442
428
|
logger.warn(`⚠️ refresh_tokens reconcile failed for ${qualified}: ${perTableError instanceof Error ? perTableError.message : String(perTableError)}`);
|
|
443
429
|
}
|
|
444
430
|
}
|
|
445
431
|
} catch (migrationError: unknown) {
|
|
446
|
-
logger.warn(`⚠️ refresh_tokens session migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
|
|
432
|
+
logger.warn(`⚠️ refresh_tokens device-session migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);
|
|
447
433
|
}
|
|
448
434
|
|
|
449
435
|
// ── Migration: Copy roles from legacy junction table to inline column ──
|