@byline/db-mysql 6.7.1 → 6.8.0
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/database/schema/auth.d.ts +464 -0
- package/dist/database/schema/auth.js +55 -1
- package/dist/database/schema/index.d.ts +1 -1
- package/dist/database/schema/index.js +1 -1
- package/dist/database/schema/schema-pins.test.node.js +3 -0
- package/dist/modules/admin/admin-store.js +9 -2
- package/dist/modules/admin/auth-maintenance-repository.d.ts +11 -0
- package/dist/modules/admin/auth-maintenance-repository.js +94 -0
- package/dist/modules/admin/passkeys-repository.d.ts +11 -0
- package/dist/modules/admin/passkeys-repository.js +165 -0
- package/dist/modules/admin/refresh-tokens-repository.js +4 -5
- package/package.json +16 -16
|
@@ -60,6 +60,23 @@ export declare const adminUsers: import("drizzle-orm/mysql-core").MySqlTableWith
|
|
|
60
60
|
identity: undefined;
|
|
61
61
|
generated: undefined;
|
|
62
62
|
}, {}, {}>;
|
|
63
|
+
webauthn_user_handle: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
64
|
+
name: "webauthn_user_handle";
|
|
65
|
+
tableName: "byline_admin_users";
|
|
66
|
+
dataType: "custom";
|
|
67
|
+
columnType: "MySqlCustomColumn";
|
|
68
|
+
data: string;
|
|
69
|
+
driverParam: Buffer<ArrayBufferLike>;
|
|
70
|
+
notNull: false;
|
|
71
|
+
hasDefault: false;
|
|
72
|
+
isPrimaryKey: false;
|
|
73
|
+
isAutoincrement: false;
|
|
74
|
+
hasRuntimeDefault: false;
|
|
75
|
+
enumValues: undefined;
|
|
76
|
+
baseColumn: never;
|
|
77
|
+
identity: undefined;
|
|
78
|
+
generated: undefined;
|
|
79
|
+
}, {}, {}>;
|
|
63
80
|
session_version: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
64
81
|
name: "session_version";
|
|
65
82
|
tableName: "byline_admin_users";
|
|
@@ -1151,3 +1168,450 @@ export declare const adminSignInRateLimits: import("drizzle-orm/mysql-core").MyS
|
|
|
1151
1168
|
};
|
|
1152
1169
|
dialect: 'mysql';
|
|
1153
1170
|
}>;
|
|
1171
|
+
/** Credential identities survive revocation and account deletion as tombstones. */
|
|
1172
|
+
export declare const adminPasskeyCredentials: import("drizzle-orm/mysql-core").MySqlTableWithColumns<{
|
|
1173
|
+
name: "byline_admin_passkey_credentials";
|
|
1174
|
+
schema: undefined;
|
|
1175
|
+
columns: {
|
|
1176
|
+
id: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1177
|
+
name: "id";
|
|
1178
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1179
|
+
dataType: "custom";
|
|
1180
|
+
columnType: "MySqlCustomColumn";
|
|
1181
|
+
data: string;
|
|
1182
|
+
driverParam: string;
|
|
1183
|
+
notNull: true;
|
|
1184
|
+
hasDefault: false;
|
|
1185
|
+
isPrimaryKey: true;
|
|
1186
|
+
isAutoincrement: false;
|
|
1187
|
+
hasRuntimeDefault: false;
|
|
1188
|
+
enumValues: undefined;
|
|
1189
|
+
baseColumn: never;
|
|
1190
|
+
identity: undefined;
|
|
1191
|
+
generated: undefined;
|
|
1192
|
+
}, {}, {}>;
|
|
1193
|
+
admin_user_id: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1194
|
+
name: "admin_user_id";
|
|
1195
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1196
|
+
dataType: "custom";
|
|
1197
|
+
columnType: "MySqlCustomColumn";
|
|
1198
|
+
data: string;
|
|
1199
|
+
driverParam: string;
|
|
1200
|
+
notNull: false;
|
|
1201
|
+
hasDefault: false;
|
|
1202
|
+
isPrimaryKey: false;
|
|
1203
|
+
isAutoincrement: false;
|
|
1204
|
+
hasRuntimeDefault: false;
|
|
1205
|
+
enumValues: undefined;
|
|
1206
|
+
baseColumn: never;
|
|
1207
|
+
identity: undefined;
|
|
1208
|
+
generated: undefined;
|
|
1209
|
+
}, {}, {}>;
|
|
1210
|
+
credential_id: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1211
|
+
name: "credential_id";
|
|
1212
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1213
|
+
dataType: "custom";
|
|
1214
|
+
columnType: "MySqlCustomColumn";
|
|
1215
|
+
data: string;
|
|
1216
|
+
driverParam: Buffer<ArrayBufferLike>;
|
|
1217
|
+
notNull: true;
|
|
1218
|
+
hasDefault: false;
|
|
1219
|
+
isPrimaryKey: false;
|
|
1220
|
+
isAutoincrement: false;
|
|
1221
|
+
hasRuntimeDefault: false;
|
|
1222
|
+
enumValues: undefined;
|
|
1223
|
+
baseColumn: never;
|
|
1224
|
+
identity: undefined;
|
|
1225
|
+
generated: undefined;
|
|
1226
|
+
}, {}, {}>;
|
|
1227
|
+
public_key: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1228
|
+
name: "public_key";
|
|
1229
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1230
|
+
dataType: "string";
|
|
1231
|
+
columnType: "MySqlText";
|
|
1232
|
+
data: string;
|
|
1233
|
+
driverParam: string;
|
|
1234
|
+
notNull: true;
|
|
1235
|
+
hasDefault: false;
|
|
1236
|
+
isPrimaryKey: false;
|
|
1237
|
+
isAutoincrement: false;
|
|
1238
|
+
hasRuntimeDefault: false;
|
|
1239
|
+
enumValues: [string, ...string[]];
|
|
1240
|
+
baseColumn: never;
|
|
1241
|
+
identity: undefined;
|
|
1242
|
+
generated: undefined;
|
|
1243
|
+
}, {}, {}>;
|
|
1244
|
+
counter: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1245
|
+
name: "counter";
|
|
1246
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1247
|
+
dataType: "number";
|
|
1248
|
+
columnType: "MySqlBigInt53";
|
|
1249
|
+
data: number;
|
|
1250
|
+
driverParam: string | number;
|
|
1251
|
+
notNull: true;
|
|
1252
|
+
hasDefault: false;
|
|
1253
|
+
isPrimaryKey: false;
|
|
1254
|
+
isAutoincrement: false;
|
|
1255
|
+
hasRuntimeDefault: false;
|
|
1256
|
+
enumValues: undefined;
|
|
1257
|
+
baseColumn: never;
|
|
1258
|
+
identity: undefined;
|
|
1259
|
+
generated: undefined;
|
|
1260
|
+
}, {}, {}>;
|
|
1261
|
+
transports: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1262
|
+
name: "transports";
|
|
1263
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1264
|
+
dataType: "json";
|
|
1265
|
+
columnType: "MySqlJson";
|
|
1266
|
+
data: string[];
|
|
1267
|
+
driverParam: string;
|
|
1268
|
+
notNull: true;
|
|
1269
|
+
hasDefault: false;
|
|
1270
|
+
isPrimaryKey: false;
|
|
1271
|
+
isAutoincrement: false;
|
|
1272
|
+
hasRuntimeDefault: false;
|
|
1273
|
+
enumValues: undefined;
|
|
1274
|
+
baseColumn: never;
|
|
1275
|
+
identity: undefined;
|
|
1276
|
+
generated: undefined;
|
|
1277
|
+
}, {}, {
|
|
1278
|
+
$type: string[];
|
|
1279
|
+
}>;
|
|
1280
|
+
backup_eligible: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1281
|
+
name: "backup_eligible";
|
|
1282
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1283
|
+
dataType: "boolean";
|
|
1284
|
+
columnType: "MySqlBoolean";
|
|
1285
|
+
data: boolean;
|
|
1286
|
+
driverParam: number | boolean;
|
|
1287
|
+
notNull: true;
|
|
1288
|
+
hasDefault: false;
|
|
1289
|
+
isPrimaryKey: false;
|
|
1290
|
+
isAutoincrement: false;
|
|
1291
|
+
hasRuntimeDefault: false;
|
|
1292
|
+
enumValues: undefined;
|
|
1293
|
+
baseColumn: never;
|
|
1294
|
+
identity: undefined;
|
|
1295
|
+
generated: undefined;
|
|
1296
|
+
}, {}, {}>;
|
|
1297
|
+
backed_up: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1298
|
+
name: "backed_up";
|
|
1299
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1300
|
+
dataType: "boolean";
|
|
1301
|
+
columnType: "MySqlBoolean";
|
|
1302
|
+
data: boolean;
|
|
1303
|
+
driverParam: number | boolean;
|
|
1304
|
+
notNull: true;
|
|
1305
|
+
hasDefault: false;
|
|
1306
|
+
isPrimaryKey: false;
|
|
1307
|
+
isAutoincrement: false;
|
|
1308
|
+
hasRuntimeDefault: false;
|
|
1309
|
+
enumValues: undefined;
|
|
1310
|
+
baseColumn: never;
|
|
1311
|
+
identity: undefined;
|
|
1312
|
+
generated: undefined;
|
|
1313
|
+
}, {}, {}>;
|
|
1314
|
+
name: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1315
|
+
name: "name";
|
|
1316
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1317
|
+
dataType: "string";
|
|
1318
|
+
columnType: "MySqlVarChar";
|
|
1319
|
+
data: string;
|
|
1320
|
+
driverParam: string | number;
|
|
1321
|
+
notNull: true;
|
|
1322
|
+
hasDefault: false;
|
|
1323
|
+
isPrimaryKey: false;
|
|
1324
|
+
isAutoincrement: false;
|
|
1325
|
+
hasRuntimeDefault: false;
|
|
1326
|
+
enumValues: [string, ...string[]];
|
|
1327
|
+
baseColumn: never;
|
|
1328
|
+
identity: undefined;
|
|
1329
|
+
generated: undefined;
|
|
1330
|
+
}, {}, {}>;
|
|
1331
|
+
created_at: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1332
|
+
name: "created_at";
|
|
1333
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1334
|
+
dataType: "date";
|
|
1335
|
+
columnType: "MySqlDateTime";
|
|
1336
|
+
data: Date;
|
|
1337
|
+
driverParam: string | number;
|
|
1338
|
+
notNull: true;
|
|
1339
|
+
hasDefault: false;
|
|
1340
|
+
isPrimaryKey: false;
|
|
1341
|
+
isAutoincrement: false;
|
|
1342
|
+
hasRuntimeDefault: false;
|
|
1343
|
+
enumValues: undefined;
|
|
1344
|
+
baseColumn: never;
|
|
1345
|
+
identity: undefined;
|
|
1346
|
+
generated: undefined;
|
|
1347
|
+
}, {}, {}>;
|
|
1348
|
+
last_used_at: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1349
|
+
name: "last_used_at";
|
|
1350
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1351
|
+
dataType: "date";
|
|
1352
|
+
columnType: "MySqlDateTime";
|
|
1353
|
+
data: Date;
|
|
1354
|
+
driverParam: string | number;
|
|
1355
|
+
notNull: false;
|
|
1356
|
+
hasDefault: false;
|
|
1357
|
+
isPrimaryKey: false;
|
|
1358
|
+
isAutoincrement: false;
|
|
1359
|
+
hasRuntimeDefault: false;
|
|
1360
|
+
enumValues: undefined;
|
|
1361
|
+
baseColumn: never;
|
|
1362
|
+
identity: undefined;
|
|
1363
|
+
generated: undefined;
|
|
1364
|
+
}, {}, {}>;
|
|
1365
|
+
revoked_at: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1366
|
+
name: "revoked_at";
|
|
1367
|
+
tableName: "byline_admin_passkey_credentials";
|
|
1368
|
+
dataType: "date";
|
|
1369
|
+
columnType: "MySqlDateTime";
|
|
1370
|
+
data: Date;
|
|
1371
|
+
driverParam: string | number;
|
|
1372
|
+
notNull: false;
|
|
1373
|
+
hasDefault: false;
|
|
1374
|
+
isPrimaryKey: false;
|
|
1375
|
+
isAutoincrement: false;
|
|
1376
|
+
hasRuntimeDefault: false;
|
|
1377
|
+
enumValues: undefined;
|
|
1378
|
+
baseColumn: never;
|
|
1379
|
+
identity: undefined;
|
|
1380
|
+
generated: undefined;
|
|
1381
|
+
}, {}, {}>;
|
|
1382
|
+
};
|
|
1383
|
+
dialect: 'mysql';
|
|
1384
|
+
}>;
|
|
1385
|
+
/** No FK cascades: short-lived authority is checked against live account/login state. */
|
|
1386
|
+
export declare const adminPasskeyAttempts: import("drizzle-orm/mysql-core").MySqlTableWithColumns<{
|
|
1387
|
+
name: "byline_admin_passkey_attempts";
|
|
1388
|
+
schema: undefined;
|
|
1389
|
+
columns: {
|
|
1390
|
+
token_hash: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1391
|
+
name: "token_hash";
|
|
1392
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1393
|
+
dataType: "custom";
|
|
1394
|
+
columnType: "MySqlCustomColumn";
|
|
1395
|
+
data: string;
|
|
1396
|
+
driverParam: Buffer<ArrayBufferLike>;
|
|
1397
|
+
notNull: true;
|
|
1398
|
+
hasDefault: false;
|
|
1399
|
+
isPrimaryKey: true;
|
|
1400
|
+
isAutoincrement: false;
|
|
1401
|
+
hasRuntimeDefault: false;
|
|
1402
|
+
enumValues: undefined;
|
|
1403
|
+
baseColumn: never;
|
|
1404
|
+
identity: undefined;
|
|
1405
|
+
generated: undefined;
|
|
1406
|
+
}, {}, {}>;
|
|
1407
|
+
purpose: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1408
|
+
name: "purpose";
|
|
1409
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1410
|
+
dataType: "string";
|
|
1411
|
+
columnType: "MySqlVarChar";
|
|
1412
|
+
data: "enrollment" | "fresh-authentication" | "grant" | "sign-in";
|
|
1413
|
+
driverParam: string | number;
|
|
1414
|
+
notNull: true;
|
|
1415
|
+
hasDefault: false;
|
|
1416
|
+
isPrimaryKey: false;
|
|
1417
|
+
isAutoincrement: false;
|
|
1418
|
+
hasRuntimeDefault: false;
|
|
1419
|
+
enumValues: [string, ...string[]];
|
|
1420
|
+
baseColumn: never;
|
|
1421
|
+
identity: undefined;
|
|
1422
|
+
generated: undefined;
|
|
1423
|
+
}, {}, {
|
|
1424
|
+
$type: "enrollment" | "fresh-authentication" | "grant" | "sign-in";
|
|
1425
|
+
}>;
|
|
1426
|
+
challenge: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1427
|
+
name: "challenge";
|
|
1428
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1429
|
+
dataType: "string";
|
|
1430
|
+
columnType: "MySqlVarChar";
|
|
1431
|
+
data: string;
|
|
1432
|
+
driverParam: string | number;
|
|
1433
|
+
notNull: false;
|
|
1434
|
+
hasDefault: false;
|
|
1435
|
+
isPrimaryKey: false;
|
|
1436
|
+
isAutoincrement: false;
|
|
1437
|
+
hasRuntimeDefault: false;
|
|
1438
|
+
enumValues: [string, ...string[]];
|
|
1439
|
+
baseColumn: never;
|
|
1440
|
+
identity: undefined;
|
|
1441
|
+
generated: undefined;
|
|
1442
|
+
}, {}, {}>;
|
|
1443
|
+
actor_user_id: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1444
|
+
name: "actor_user_id";
|
|
1445
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1446
|
+
dataType: "custom";
|
|
1447
|
+
columnType: "MySqlCustomColumn";
|
|
1448
|
+
data: string;
|
|
1449
|
+
driverParam: string;
|
|
1450
|
+
notNull: false;
|
|
1451
|
+
hasDefault: false;
|
|
1452
|
+
isPrimaryKey: false;
|
|
1453
|
+
isAutoincrement: false;
|
|
1454
|
+
hasRuntimeDefault: false;
|
|
1455
|
+
enumValues: undefined;
|
|
1456
|
+
baseColumn: never;
|
|
1457
|
+
identity: undefined;
|
|
1458
|
+
generated: undefined;
|
|
1459
|
+
}, {}, {}>;
|
|
1460
|
+
actor_login_id: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1461
|
+
name: "actor_login_id";
|
|
1462
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1463
|
+
dataType: "custom";
|
|
1464
|
+
columnType: "MySqlCustomColumn";
|
|
1465
|
+
data: string;
|
|
1466
|
+
driverParam: string;
|
|
1467
|
+
notNull: false;
|
|
1468
|
+
hasDefault: false;
|
|
1469
|
+
isPrimaryKey: false;
|
|
1470
|
+
isAutoincrement: false;
|
|
1471
|
+
hasRuntimeDefault: false;
|
|
1472
|
+
enumValues: undefined;
|
|
1473
|
+
baseColumn: never;
|
|
1474
|
+
identity: undefined;
|
|
1475
|
+
generated: undefined;
|
|
1476
|
+
}, {}, {}>;
|
|
1477
|
+
actor_generation: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1478
|
+
name: "actor_generation";
|
|
1479
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1480
|
+
dataType: "number";
|
|
1481
|
+
columnType: "MySqlInt";
|
|
1482
|
+
data: number;
|
|
1483
|
+
driverParam: string | number;
|
|
1484
|
+
notNull: false;
|
|
1485
|
+
hasDefault: false;
|
|
1486
|
+
isPrimaryKey: false;
|
|
1487
|
+
isAutoincrement: false;
|
|
1488
|
+
hasRuntimeDefault: false;
|
|
1489
|
+
enumValues: undefined;
|
|
1490
|
+
baseColumn: never;
|
|
1491
|
+
identity: undefined;
|
|
1492
|
+
generated: undefined;
|
|
1493
|
+
}, {}, {}>;
|
|
1494
|
+
action: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1495
|
+
name: "action";
|
|
1496
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1497
|
+
dataType: "string";
|
|
1498
|
+
columnType: "MySqlVarChar";
|
|
1499
|
+
data: "enroll" | "remove";
|
|
1500
|
+
driverParam: string | number;
|
|
1501
|
+
notNull: false;
|
|
1502
|
+
hasDefault: false;
|
|
1503
|
+
isPrimaryKey: false;
|
|
1504
|
+
isAutoincrement: false;
|
|
1505
|
+
hasRuntimeDefault: false;
|
|
1506
|
+
enumValues: [string, ...string[]];
|
|
1507
|
+
baseColumn: never;
|
|
1508
|
+
identity: undefined;
|
|
1509
|
+
generated: undefined;
|
|
1510
|
+
}, {}, {
|
|
1511
|
+
$type: "enroll" | "remove";
|
|
1512
|
+
}>;
|
|
1513
|
+
target_user_id: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1514
|
+
name: "target_user_id";
|
|
1515
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1516
|
+
dataType: "custom";
|
|
1517
|
+
columnType: "MySqlCustomColumn";
|
|
1518
|
+
data: string;
|
|
1519
|
+
driverParam: string;
|
|
1520
|
+
notNull: false;
|
|
1521
|
+
hasDefault: false;
|
|
1522
|
+
isPrimaryKey: false;
|
|
1523
|
+
isAutoincrement: false;
|
|
1524
|
+
hasRuntimeDefault: false;
|
|
1525
|
+
enumValues: undefined;
|
|
1526
|
+
baseColumn: never;
|
|
1527
|
+
identity: undefined;
|
|
1528
|
+
generated: undefined;
|
|
1529
|
+
}, {}, {}>;
|
|
1530
|
+
credential_record_id: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1531
|
+
name: "credential_record_id";
|
|
1532
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1533
|
+
dataType: "custom";
|
|
1534
|
+
columnType: "MySqlCustomColumn";
|
|
1535
|
+
data: string;
|
|
1536
|
+
driverParam: string;
|
|
1537
|
+
notNull: false;
|
|
1538
|
+
hasDefault: false;
|
|
1539
|
+
isPrimaryKey: false;
|
|
1540
|
+
isAutoincrement: false;
|
|
1541
|
+
hasRuntimeDefault: false;
|
|
1542
|
+
enumValues: undefined;
|
|
1543
|
+
baseColumn: never;
|
|
1544
|
+
identity: undefined;
|
|
1545
|
+
generated: undefined;
|
|
1546
|
+
}, {}, {}>;
|
|
1547
|
+
proof_credential_id: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1548
|
+
name: "proof_credential_id";
|
|
1549
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1550
|
+
dataType: "custom";
|
|
1551
|
+
columnType: "MySqlCustomColumn";
|
|
1552
|
+
data: string;
|
|
1553
|
+
driverParam: string;
|
|
1554
|
+
notNull: false;
|
|
1555
|
+
hasDefault: false;
|
|
1556
|
+
isPrimaryKey: false;
|
|
1557
|
+
isAutoincrement: false;
|
|
1558
|
+
hasRuntimeDefault: false;
|
|
1559
|
+
enumValues: undefined;
|
|
1560
|
+
baseColumn: never;
|
|
1561
|
+
identity: undefined;
|
|
1562
|
+
generated: undefined;
|
|
1563
|
+
}, {}, {}>;
|
|
1564
|
+
created_at: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1565
|
+
name: "created_at";
|
|
1566
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1567
|
+
dataType: "date";
|
|
1568
|
+
columnType: "MySqlDateTime";
|
|
1569
|
+
data: Date;
|
|
1570
|
+
driverParam: string | number;
|
|
1571
|
+
notNull: true;
|
|
1572
|
+
hasDefault: false;
|
|
1573
|
+
isPrimaryKey: false;
|
|
1574
|
+
isAutoincrement: false;
|
|
1575
|
+
hasRuntimeDefault: false;
|
|
1576
|
+
enumValues: undefined;
|
|
1577
|
+
baseColumn: never;
|
|
1578
|
+
identity: undefined;
|
|
1579
|
+
generated: undefined;
|
|
1580
|
+
}, {}, {}>;
|
|
1581
|
+
expires_at: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1582
|
+
name: "expires_at";
|
|
1583
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1584
|
+
dataType: "date";
|
|
1585
|
+
columnType: "MySqlDateTime";
|
|
1586
|
+
data: Date;
|
|
1587
|
+
driverParam: string | number;
|
|
1588
|
+
notNull: true;
|
|
1589
|
+
hasDefault: false;
|
|
1590
|
+
isPrimaryKey: false;
|
|
1591
|
+
isAutoincrement: false;
|
|
1592
|
+
hasRuntimeDefault: false;
|
|
1593
|
+
enumValues: undefined;
|
|
1594
|
+
baseColumn: never;
|
|
1595
|
+
identity: undefined;
|
|
1596
|
+
generated: undefined;
|
|
1597
|
+
}, {}, {}>;
|
|
1598
|
+
consumed_at: import("drizzle-orm/mysql-core").MySqlColumn<{
|
|
1599
|
+
name: "consumed_at";
|
|
1600
|
+
tableName: "byline_admin_passkey_attempts";
|
|
1601
|
+
dataType: "date";
|
|
1602
|
+
columnType: "MySqlDateTime";
|
|
1603
|
+
data: Date;
|
|
1604
|
+
driverParam: string | number;
|
|
1605
|
+
notNull: false;
|
|
1606
|
+
hasDefault: false;
|
|
1607
|
+
isPrimaryKey: false;
|
|
1608
|
+
isAutoincrement: false;
|
|
1609
|
+
hasRuntimeDefault: false;
|
|
1610
|
+
enumValues: undefined;
|
|
1611
|
+
baseColumn: never;
|
|
1612
|
+
identity: undefined;
|
|
1613
|
+
generated: undefined;
|
|
1614
|
+
}, {}, {}>;
|
|
1615
|
+
};
|
|
1616
|
+
dialect: 'mysql';
|
|
1617
|
+
}>;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* reference.
|
|
25
25
|
*/
|
|
26
26
|
import { relations, sql } from 'drizzle-orm';
|
|
27
|
-
import { boolean, datetime, foreignKey, index, int, json, mysqlTable, primaryKey, text, unique, varchar, } from 'drizzle-orm/mysql-core';
|
|
27
|
+
import { bigint, boolean, customType, datetime, foreignKey, index, int, json, mysqlTable, primaryKey, text, unique, varchar, } from 'drizzle-orm/mysql-core';
|
|
28
28
|
import { createdAt, timestamps, uuidChar } from './common.js';
|
|
29
29
|
// Foreign keys below use the table-level `foreignKey()` builder with an
|
|
30
30
|
// explicit, short `fk_<table>_<column>` name rather than the Postgres
|
|
@@ -34,8 +34,14 @@ import { createdAt, timestamps, uuidChar } from './common.js';
|
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
// byline_admin_users
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
|
+
const passkeyBytes = customType({
|
|
38
|
+
dataType: (config) => `varbinary(${config?.length ?? 1366})`,
|
|
39
|
+
toDriver: (value) => Buffer.from(value, 'ascii'),
|
|
40
|
+
fromDriver: (value) => value.toString('ascii'),
|
|
41
|
+
});
|
|
37
42
|
export const adminUsers = mysqlTable('byline_admin_users', {
|
|
38
43
|
id: uuidChar('id').primaryKey(),
|
|
44
|
+
webauthn_user_handle: passkeyBytes('webauthn_user_handle', { length: 43 }).unique(),
|
|
39
45
|
session_version: int('session_version').notNull().default(0),
|
|
40
46
|
vid: int('vid').notNull().default(1),
|
|
41
47
|
given_name: varchar('given_name', { length: 100 }),
|
|
@@ -176,6 +182,8 @@ export const adminLoginSessions = mysqlTable('byline_admin_login_sessions', {
|
|
|
176
182
|
foreignColumns: [adminUsers.id],
|
|
177
183
|
}).onDelete('cascade'),
|
|
178
184
|
index('idx_admin_login_sessions_user').on(table.admin_user_id),
|
|
185
|
+
index('idx_admin_login_sessions_expiry').on(table.expires_at),
|
|
186
|
+
index('idx_admin_login_sessions_revoked').on(table.revoked_at),
|
|
179
187
|
]);
|
|
180
188
|
export const adminRefreshTokens = mysqlTable('byline_admin_refresh_tokens', {
|
|
181
189
|
id: uuidChar('id').primaryKey(),
|
|
@@ -211,6 +219,7 @@ export const adminRefreshTokens = mysqlTable('byline_admin_refresh_tokens', {
|
|
|
211
219
|
foreignColumns: [adminUsers.id],
|
|
212
220
|
}).onDelete('cascade'),
|
|
213
221
|
index('idx_admin_refresh_tokens_sid').on(table.sid),
|
|
222
|
+
index('idx_admin_refresh_tokens_legacy_expiry').on(table.sid, table.expires_at),
|
|
214
223
|
index('idx_byline_admin_refresh_tokens_user').on(table.admin_user_id),
|
|
215
224
|
index('idx_byline_admin_refresh_tokens_token_hash').on(table.token_hash),
|
|
216
225
|
]);
|
|
@@ -253,3 +262,48 @@ export const adminSignInRateLimits = mysqlTable('byline_admin_sign_in_rate_limit
|
|
|
253
262
|
attempts: int('attempts').notNull(),
|
|
254
263
|
expires_at: datetime('expires_at', { fsp: 3 }).notNull(),
|
|
255
264
|
}, (table) => [index('idx_admin_sign_in_rate_limits_expiry').on(table.expires_at)]);
|
|
265
|
+
/** Credential identities survive revocation and account deletion as tombstones. */
|
|
266
|
+
export const adminPasskeyCredentials = mysqlTable('byline_admin_passkey_credentials', {
|
|
267
|
+
id: uuidChar('id').primaryKey(),
|
|
268
|
+
admin_user_id: uuidChar('admin_user_id'),
|
|
269
|
+
credential_id: passkeyBytes('credential_id', { length: 1366 }).notNull().unique(),
|
|
270
|
+
public_key: text('public_key').notNull(),
|
|
271
|
+
counter: bigint('counter', { mode: 'number' }).notNull(),
|
|
272
|
+
transports: json('transports').$type().notNull(),
|
|
273
|
+
backup_eligible: boolean('backup_eligible').notNull(),
|
|
274
|
+
backed_up: boolean('backed_up').notNull(),
|
|
275
|
+
name: varchar('name', { length: 80 }).notNull(),
|
|
276
|
+
created_at: datetime('created_at', { fsp: 6 }).notNull(),
|
|
277
|
+
last_used_at: datetime('last_used_at', { fsp: 6 }),
|
|
278
|
+
revoked_at: datetime('revoked_at', { fsp: 6 }),
|
|
279
|
+
}, (table) => [
|
|
280
|
+
foreignKey({
|
|
281
|
+
name: 'fk_admin_passkeys_user',
|
|
282
|
+
columns: [table.admin_user_id],
|
|
283
|
+
foreignColumns: [adminUsers.id],
|
|
284
|
+
}).onDelete('set null'),
|
|
285
|
+
index('idx_admin_passkeys_user').on(table.admin_user_id),
|
|
286
|
+
]);
|
|
287
|
+
/** No FK cascades: short-lived authority is checked against live account/login state. */
|
|
288
|
+
export const adminPasskeyAttempts = mysqlTable('byline_admin_passkey_attempts', {
|
|
289
|
+
token_hash: passkeyBytes('token_hash', { length: 64 }).primaryKey(),
|
|
290
|
+
purpose: varchar('purpose', { length: 24 })
|
|
291
|
+
.$type()
|
|
292
|
+
.notNull(),
|
|
293
|
+
// Compared only by the verifier in application code, never by SQL collation.
|
|
294
|
+
challenge: varchar('challenge', { length: 43 }),
|
|
295
|
+
actor_user_id: uuidChar('actor_user_id'),
|
|
296
|
+
actor_login_id: uuidChar('actor_login_id'),
|
|
297
|
+
actor_generation: int('actor_generation'),
|
|
298
|
+
action: varchar('action', { length: 8 }).$type(),
|
|
299
|
+
target_user_id: uuidChar('target_user_id'),
|
|
300
|
+
credential_record_id: uuidChar('credential_record_id'),
|
|
301
|
+
proof_credential_id: uuidChar('proof_credential_id'),
|
|
302
|
+
created_at: datetime('created_at', { fsp: 6 }).notNull(),
|
|
303
|
+
expires_at: datetime('expires_at', { fsp: 6 }).notNull(),
|
|
304
|
+
consumed_at: datetime('consumed_at', { fsp: 6 }),
|
|
305
|
+
}, (table) => [
|
|
306
|
+
index('idx_admin_passkey_attempts_expiry').on(table.expires_at),
|
|
307
|
+
index('idx_admin_passkey_attempts_consumed').on(table.consumed_at),
|
|
308
|
+
index('idx_admin_passkey_attempts_login').on(table.actor_login_id),
|
|
309
|
+
]);
|
|
@@ -4102,4 +4102,4 @@ export declare const recurringTasks: import("drizzle-orm/mysql-core").MySqlTable
|
|
|
4102
4102
|
};
|
|
4103
4103
|
dialect: 'mysql';
|
|
4104
4104
|
}>;
|
|
4105
|
-
export { adminLoginSessions, adminPermissions, adminPermissionsRelations, adminRefreshTokens, adminRefreshTokensRelations, adminRoleAdminUser, adminRoleAdminUserRelations, adminRoles, adminRolesRelations, adminSignInRateLimits, adminUserPreferences, adminUsers, adminUsersRelations, } from './auth.js';
|
|
4105
|
+
export { adminLoginSessions, adminPasskeyAttempts, adminPasskeyCredentials, adminPermissions, adminPermissionsRelations, adminRefreshTokens, adminRefreshTokensRelations, adminRoleAdminUser, adminRoleAdminUserRelations, adminRoles, adminRolesRelations, adminSignInRateLimits, adminUserPreferences, adminUsers, adminUsersRelations, } from './auth.js';
|
|
@@ -996,4 +996,4 @@ export const recurringTasks = mysqlTable('byline_recurring_tasks', {
|
|
|
996
996
|
// Auth schema — byline_admin_users, byline_admin_roles, etc.
|
|
997
997
|
// See ./auth.ts for definitions and rationale.
|
|
998
998
|
// ---------------------------------------------------------------------------
|
|
999
|
-
export { adminLoginSessions, adminPermissions, adminPermissionsRelations, adminRefreshTokens, adminRefreshTokensRelations, adminRoleAdminUser, adminRoleAdminUserRelations, adminRoles, adminRolesRelations, adminSignInRateLimits, adminUserPreferences, adminUsers, adminUsersRelations, } from './auth.js';
|
|
999
|
+
export { adminLoginSessions, adminPasskeyAttempts, adminPasskeyCredentials, adminPermissions, adminPermissionsRelations, adminRefreshTokens, adminRefreshTokensRelations, adminRoleAdminUser, adminRoleAdminUserRelations, adminRoles, adminRolesRelations, adminSignInRateLimits, adminUserPreferences, adminUsers, adminUsersRelations, } from './auth.js';
|
|
@@ -131,6 +131,9 @@ function columnByteWidth(columnName, sqlType) {
|
|
|
131
131
|
const typeName = typeMatch?.[1]?.toLowerCase();
|
|
132
132
|
const args = typeMatch?.[2];
|
|
133
133
|
switch (typeName) {
|
|
134
|
+
case 'binary':
|
|
135
|
+
case 'varbinary':
|
|
136
|
+
return Number(args);
|
|
134
137
|
case 'boolean':
|
|
135
138
|
case 'tinyint':
|
|
136
139
|
return 1;
|
|
@@ -11,7 +11,9 @@ import { createAdminPermissionsRepository } from './admin-permissions-repository
|
|
|
11
11
|
import { createAdminPreferencesRepository } from './admin-preferences-repository.js';
|
|
12
12
|
import { createAdminRolesRepository } from './admin-roles-repository.js';
|
|
13
13
|
import { createAdminUsersRepository } from './admin-users-repository.js';
|
|
14
|
+
import { createAuthMaintenanceRepository } from './auth-maintenance-repository.js';
|
|
14
15
|
import { createLoginSessionsRepository } from './login-sessions-repository.js';
|
|
16
|
+
import { createPasskeysRepository } from './passkeys-repository.js';
|
|
15
17
|
import { createRefreshTokensRepository } from './refresh-tokens-repository.js';
|
|
16
18
|
import { createSignInRateLimitStore } from './sign-in-rate-limit-store.js';
|
|
17
19
|
/**
|
|
@@ -24,6 +26,9 @@ import { createSignInRateLimitStore } from './sign-in-rate-limit-store.js';
|
|
|
24
26
|
* Construct once per process, alongside the `mysqlAdapter` call.
|
|
25
27
|
*/
|
|
26
28
|
export function createAdminStore(db) {
|
|
29
|
+
return createScopedAdminStore(db, null);
|
|
30
|
+
}
|
|
31
|
+
function createScopedAdminStore(db, lockedAccounts) {
|
|
27
32
|
return {
|
|
28
33
|
async withSessionLock(adminUserId, work) {
|
|
29
34
|
return db.transaction(async (tx) => {
|
|
@@ -32,7 +37,7 @@ export function createAdminStore(db) {
|
|
|
32
37
|
.from(adminUsers)
|
|
33
38
|
.where(eq(adminUsers.id, adminUserId))
|
|
34
39
|
.for('update');
|
|
35
|
-
const scoped =
|
|
40
|
+
const scoped = createScopedAdminStore(tx, [adminUserId]);
|
|
36
41
|
const user = await scoped.adminUsers.getByIdForSignIn(adminUserId);
|
|
37
42
|
return work(scoped, user);
|
|
38
43
|
});
|
|
@@ -46,9 +51,11 @@ export function createAdminStore(db) {
|
|
|
46
51
|
.where(eq(adminUsers.id, id))
|
|
47
52
|
.for('update');
|
|
48
53
|
}
|
|
49
|
-
return work(
|
|
54
|
+
return work(createScopedAdminStore(tx, adminUserIds));
|
|
50
55
|
});
|
|
51
56
|
},
|
|
57
|
+
authMaintenance: createAuthMaintenanceRepository(db, lockedAccounts),
|
|
58
|
+
passkeys: createPasskeysRepository(db, lockedAccounts),
|
|
52
59
|
loginSessions: createLoginSessionsRepository(db),
|
|
53
60
|
signInRateLimits: createSignInRateLimitStore(db),
|
|
54
61
|
adminUsers: createAdminUsersRepository(db),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { AuthMaintenanceRepository } from '@byline/admin/auth';
|
|
9
|
+
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
|
10
|
+
import type * as schema from '../../database/schema/index.js';
|
|
11
|
+
export declare function createAuthMaintenanceRepository(db: MySql2Database<typeof schema>, lockedAccounts: readonly string[] | null): AuthMaintenanceRepository;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { and, eq, inArray, isNull, lte, notInArray } from 'drizzle-orm';
|
|
9
|
+
import { adminPasskeyAttempts as attempts, adminLoginSessions as logins, adminRefreshTokens as refresh, } from '../../database/schema/auth.js';
|
|
10
|
+
export function createAuthMaintenanceRepository(db, lockedAccounts) {
|
|
11
|
+
return {
|
|
12
|
+
async purgeAttempts(before) {
|
|
13
|
+
// Separate indexed scans avoid sorting or materializing an unbounded OR result.
|
|
14
|
+
const expired = await db
|
|
15
|
+
.select({ id: attempts.token_hash })
|
|
16
|
+
.from(attempts)
|
|
17
|
+
.where(lte(attempts.expires_at, before))
|
|
18
|
+
.limit(100);
|
|
19
|
+
const ids = expired.map((row) => row.id);
|
|
20
|
+
if (ids.length < 100) {
|
|
21
|
+
const consumed = await db
|
|
22
|
+
.select({ id: attempts.token_hash })
|
|
23
|
+
.from(attempts)
|
|
24
|
+
.where(and(lte(attempts.consumed_at, before), ids.length ? notInArray(attempts.token_hash, ids) : undefined))
|
|
25
|
+
.limit(100 - ids.length);
|
|
26
|
+
ids.push(...consumed.map((row) => row.id));
|
|
27
|
+
}
|
|
28
|
+
if (!ids.length)
|
|
29
|
+
return 0;
|
|
30
|
+
const [removed] = await db.delete(attempts).where(inArray(attempts.token_hash, ids));
|
|
31
|
+
return removed.affectedRows;
|
|
32
|
+
},
|
|
33
|
+
async findTerminalLogin(before) {
|
|
34
|
+
const projection = { id: logins.id, adminUserId: logins.admin_user_id };
|
|
35
|
+
const [expired] = await db
|
|
36
|
+
.select(projection)
|
|
37
|
+
.from(logins)
|
|
38
|
+
.where(lte(logins.expires_at, before))
|
|
39
|
+
.limit(1);
|
|
40
|
+
if (expired)
|
|
41
|
+
return expired;
|
|
42
|
+
const [revoked] = await db
|
|
43
|
+
.select(projection)
|
|
44
|
+
.from(logins)
|
|
45
|
+
.where(lte(logins.revoked_at, before))
|
|
46
|
+
.limit(1);
|
|
47
|
+
return revoked ?? null;
|
|
48
|
+
},
|
|
49
|
+
async pruneTerminalLogin({ id, adminUserId, before }) {
|
|
50
|
+
if (!lockedAccounts?.includes(adminUserId))
|
|
51
|
+
throw new Error('Login cleanup requires the owning account lock');
|
|
52
|
+
const [login] = await db
|
|
53
|
+
.select()
|
|
54
|
+
.from(logins)
|
|
55
|
+
.where(and(eq(logins.id, id), eq(logins.admin_user_id, adminUserId)))
|
|
56
|
+
.for('update');
|
|
57
|
+
if (!login || (login.expires_at > before && (!login.revoked_at || login.revoked_at > before)))
|
|
58
|
+
return { refreshDeleted: 0, loginDeleted: false };
|
|
59
|
+
const children = await db
|
|
60
|
+
.select({ id: refresh.id })
|
|
61
|
+
.from(refresh)
|
|
62
|
+
.where(eq(refresh.sid, id))
|
|
63
|
+
.limit(100);
|
|
64
|
+
let refreshDeleted = 0;
|
|
65
|
+
if (children.length) {
|
|
66
|
+
const [removed] = await db.delete(refresh).where(inArray(refresh.id, children.map((row) => row.id)));
|
|
67
|
+
refreshDeleted = removed.affectedRows;
|
|
68
|
+
}
|
|
69
|
+
const [remaining] = await db
|
|
70
|
+
.select({ id: refresh.id })
|
|
71
|
+
.from(refresh)
|
|
72
|
+
.where(eq(refresh.sid, id))
|
|
73
|
+
.limit(1);
|
|
74
|
+
if (remaining)
|
|
75
|
+
return { refreshDeleted, loginDeleted: false };
|
|
76
|
+
// The account and login-row locks prevent native renewal or issuance from
|
|
77
|
+
// adding children between this empty check and deletion. The FK cascade
|
|
78
|
+
// has no work to do; the task never relies on it to drain a token family.
|
|
79
|
+
await db.delete(logins).where(eq(logins.id, id));
|
|
80
|
+
return { refreshDeleted, loginDeleted: true };
|
|
81
|
+
},
|
|
82
|
+
async purgeLegacyRefreshTokens(before) {
|
|
83
|
+
const rows = await db
|
|
84
|
+
.select({ id: refresh.id })
|
|
85
|
+
.from(refresh)
|
|
86
|
+
.where(and(isNull(refresh.sid), lte(refresh.expires_at, before)))
|
|
87
|
+
.limit(100);
|
|
88
|
+
if (!rows.length)
|
|
89
|
+
return 0;
|
|
90
|
+
const [removed] = await db.delete(refresh).where(and(inArray(refresh.id, rows.map((row) => row.id)), isNull(refresh.sid), lte(refresh.expires_at, before)));
|
|
91
|
+
return removed.affectedRows;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { type PasskeysRepository } from '@byline/admin/auth';
|
|
9
|
+
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
|
10
|
+
import type * as schema from '../../database/schema/index.js';
|
|
11
|
+
export declare function createPasskeysRepository(db: MySql2Database<typeof schema>, lockedAccounts: readonly string[] | null): PasskeysRepository;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { assertPasskeyAttempt, assertPasskeyBytes, assertPasskeyClaim, assertPasskeyTokenHash, } from '@byline/admin/auth';
|
|
9
|
+
import { and, count, eq, gt, isNull, sql } from 'drizzle-orm';
|
|
10
|
+
import { v7 as uuidv7 } from 'uuid';
|
|
11
|
+
import { adminLoginSessions, adminRefreshTokens, adminUsers, adminPasskeyAttempts as attempts, adminPasskeyCredentials as credentials, } from '../../database/schema/auth.js';
|
|
12
|
+
import { auditLog } from '../../database/schema/index.js';
|
|
13
|
+
export function createPasskeysRepository(db, lockedAccounts) {
|
|
14
|
+
function requireLock(id) {
|
|
15
|
+
if (id === null || !lockedAccounts?.includes(id))
|
|
16
|
+
throw new Error('Passkey write requires the owning account lock');
|
|
17
|
+
}
|
|
18
|
+
async function requireCredentialLock(id) {
|
|
19
|
+
const [row] = await db
|
|
20
|
+
.select({ owner: credentials.admin_user_id })
|
|
21
|
+
.from(credentials)
|
|
22
|
+
.where(eq(credentials.id, id));
|
|
23
|
+
if (!row)
|
|
24
|
+
throw new Error('Passkey credential not found');
|
|
25
|
+
requireLock(row.owner);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
async invalidateAccountLogins(adminUserId, at) {
|
|
29
|
+
requireLock(adminUserId);
|
|
30
|
+
await db
|
|
31
|
+
.update(adminUsers)
|
|
32
|
+
.set({ session_version: sql `${adminUsers.session_version} + 1` })
|
|
33
|
+
.where(eq(adminUsers.id, adminUserId));
|
|
34
|
+
await db
|
|
35
|
+
.update(adminLoginSessions)
|
|
36
|
+
.set({ revoked_at: at })
|
|
37
|
+
.where(and(eq(adminLoginSessions.admin_user_id, adminUserId), isNull(adminLoginSessions.revoked_at)));
|
|
38
|
+
await db
|
|
39
|
+
.update(adminRefreshTokens)
|
|
40
|
+
.set({ revoked_at: at })
|
|
41
|
+
.where(and(eq(adminRefreshTokens.admin_user_id, adminUserId), isNull(adminRefreshTokens.revoked_at)));
|
|
42
|
+
},
|
|
43
|
+
async appendRemovalAudit(input) {
|
|
44
|
+
if (input.outcome === 'success') {
|
|
45
|
+
requireLock(input.actorId);
|
|
46
|
+
requireLock(input.targetId);
|
|
47
|
+
}
|
|
48
|
+
await db.insert(auditLog).values({
|
|
49
|
+
id: uuidv7(),
|
|
50
|
+
actor_id: input.actorId,
|
|
51
|
+
actor_realm: 'admin',
|
|
52
|
+
action: 'admin.passkey.remove',
|
|
53
|
+
after: {
|
|
54
|
+
targetUserId: input.targetId,
|
|
55
|
+
credentialRecordId: input.credentialId,
|
|
56
|
+
outcome: input.outcome,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
async getUserHandle(adminUserId) {
|
|
61
|
+
const [row] = await db
|
|
62
|
+
.select({ handle: adminUsers.webauthn_user_handle })
|
|
63
|
+
.from(adminUsers)
|
|
64
|
+
.where(eq(adminUsers.id, adminUserId));
|
|
65
|
+
return row?.handle ?? null;
|
|
66
|
+
},
|
|
67
|
+
async ensureUserHandle(adminUserId, candidate) {
|
|
68
|
+
requireLock(adminUserId);
|
|
69
|
+
assertPasskeyBytes(candidate, 32, 32);
|
|
70
|
+
await db
|
|
71
|
+
.update(adminUsers)
|
|
72
|
+
.set({ webauthn_user_handle: candidate })
|
|
73
|
+
.where(and(eq(adminUsers.id, adminUserId), isNull(adminUsers.webauthn_user_handle)));
|
|
74
|
+
const [row] = await db
|
|
75
|
+
.select({ handle: adminUsers.webauthn_user_handle })
|
|
76
|
+
.from(adminUsers)
|
|
77
|
+
.where(eq(adminUsers.id, adminUserId));
|
|
78
|
+
if (!row?.handle)
|
|
79
|
+
throw new Error('Passkey account not found');
|
|
80
|
+
return row.handle;
|
|
81
|
+
},
|
|
82
|
+
async findCredential(id) {
|
|
83
|
+
const [row] = await db.select().from(credentials).where(eq(credentials.id, id));
|
|
84
|
+
return row ?? null;
|
|
85
|
+
},
|
|
86
|
+
async findCredentialByCredentialId(credentialId) {
|
|
87
|
+
assertPasskeyBytes(credentialId, 1024);
|
|
88
|
+
const [row] = await db
|
|
89
|
+
.select()
|
|
90
|
+
.from(credentials)
|
|
91
|
+
.where(eq(credentials.credential_id, credentialId));
|
|
92
|
+
return row ?? null;
|
|
93
|
+
},
|
|
94
|
+
async listCredentials(adminUserId) {
|
|
95
|
+
return db
|
|
96
|
+
.select()
|
|
97
|
+
.from(credentials)
|
|
98
|
+
.where(and(eq(credentials.admin_user_id, adminUserId), isNull(credentials.revoked_at)));
|
|
99
|
+
},
|
|
100
|
+
async createCredential(input) {
|
|
101
|
+
requireLock(input.admin_user_id);
|
|
102
|
+
assertPasskeyBytes(input.credential_id, 1024);
|
|
103
|
+
assertPasskeyBytes(input.public_key, 8192);
|
|
104
|
+
await db.insert(credentials).values(input);
|
|
105
|
+
},
|
|
106
|
+
async updateCredential(id, patch) {
|
|
107
|
+
await requireCredentialLock(id);
|
|
108
|
+
await db
|
|
109
|
+
.update(credentials)
|
|
110
|
+
.set(patch)
|
|
111
|
+
.where(and(eq(credentials.id, id), isNull(credentials.revoked_at)));
|
|
112
|
+
},
|
|
113
|
+
async revokeCredential(id, at) {
|
|
114
|
+
await requireCredentialLock(id);
|
|
115
|
+
await db
|
|
116
|
+
.update(credentials)
|
|
117
|
+
.set({ revoked_at: at })
|
|
118
|
+
.where(and(eq(credentials.id, id), isNull(credentials.revoked_at)));
|
|
119
|
+
},
|
|
120
|
+
async createAttempt(input) {
|
|
121
|
+
assertPasskeyAttempt(input);
|
|
122
|
+
if (input.purpose !== 'sign-in')
|
|
123
|
+
requireLock(input.actor_user_id);
|
|
124
|
+
await db.insert(attempts).values(input);
|
|
125
|
+
},
|
|
126
|
+
async findAttempt(tokenHash) {
|
|
127
|
+
assertPasskeyTokenHash(tokenHash);
|
|
128
|
+
const [row] = await db.select().from(attempts).where(eq(attempts.token_hash, tokenHash));
|
|
129
|
+
return row ?? null;
|
|
130
|
+
},
|
|
131
|
+
async countPendingForLogin(loginId, now) {
|
|
132
|
+
const [row] = await db
|
|
133
|
+
.select({ total: count() })
|
|
134
|
+
.from(attempts)
|
|
135
|
+
.where(and(eq(attempts.actor_login_id, loginId), isNull(attempts.consumed_at), gt(attempts.expires_at, now)));
|
|
136
|
+
return Number(row?.total ?? 0);
|
|
137
|
+
},
|
|
138
|
+
async claimAttempt(input) {
|
|
139
|
+
if (lockedAccounts !== null)
|
|
140
|
+
throw new Error('Passkey claims require a root store, outside account transactions');
|
|
141
|
+
assertPasskeyClaim(input);
|
|
142
|
+
const binding = input.binding;
|
|
143
|
+
const match = and(eq(attempts.token_hash, input.tokenHash), eq(attempts.purpose, input.purpose), isNull(attempts.consumed_at), gt(attempts.expires_at, input.now), binding.actor_user_id === null
|
|
144
|
+
? isNull(attempts.actor_user_id)
|
|
145
|
+
: eq(attempts.actor_user_id, binding.actor_user_id), binding.actor_login_id === null
|
|
146
|
+
? isNull(attempts.actor_login_id)
|
|
147
|
+
: eq(attempts.actor_login_id, binding.actor_login_id), binding.actor_generation === null
|
|
148
|
+
? isNull(attempts.actor_generation)
|
|
149
|
+
: eq(attempts.actor_generation, binding.actor_generation), input.purpose === 'sign-in' ? isNull(attempts.action) : eq(attempts.action, input.action));
|
|
150
|
+
// The transaction promise resolves only after commit. No retry/savepoint path.
|
|
151
|
+
return db.transaction(async (tx) => {
|
|
152
|
+
const [result] = await tx.update(attempts).set({ consumed_at: input.now }).where(match);
|
|
153
|
+
if (result.affectedRows !== 1)
|
|
154
|
+
return null;
|
|
155
|
+
const [row] = await tx
|
|
156
|
+
.select()
|
|
157
|
+
.from(attempts)
|
|
158
|
+
.where(eq(attempts.token_hash, input.tokenHash));
|
|
159
|
+
if (!row)
|
|
160
|
+
throw new Error('Claimed passkey attempt disappeared');
|
|
161
|
+
return row;
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import { and, eq, isNull
|
|
8
|
+
import { and, eq, isNull } from 'drizzle-orm';
|
|
9
9
|
import { adminRefreshTokens } from '../../database/schema/auth.js';
|
|
10
10
|
import { affectedRowCount } from '../storage/storage-utils.js';
|
|
11
|
+
import { createAuthMaintenanceRepository } from './auth-maintenance-repository.js';
|
|
11
12
|
/**
|
|
12
13
|
* MySQL implementation of `RefreshTokensRepository`, backing the built-in
|
|
13
14
|
* `JwtSessionProvider`. Ported from
|
|
@@ -117,10 +118,8 @@ export function createRefreshTokensRepository(db) {
|
|
|
117
118
|
return affectedRowCount(result);
|
|
118
119
|
},
|
|
119
120
|
async purgeExpired(now = new Date()) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.where(lt(adminRefreshTokens.expires_at, now));
|
|
123
|
-
return affectedRowCount(result);
|
|
121
|
+
// Preserve every member of a native login, including expired predecessors.
|
|
122
|
+
return createAuthMaintenanceRepository(db, null).purgeLegacyRefreshTokens(new Date(now.getTime() - 30 * 86_400_000));
|
|
124
123
|
},
|
|
125
124
|
async listActiveForUser(adminUserId) {
|
|
126
125
|
return db
|
package/package.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"name": "@byline/db-mysql",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "6.
|
|
5
|
+
"version": "6.8.0",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
7
|
+
"node": ">=22.22.2"
|
|
8
8
|
},
|
|
9
9
|
"description": "Byline CMS db mysql package",
|
|
10
10
|
"keywords": [
|
|
@@ -47,26 +47,26 @@
|
|
|
47
47
|
"dist"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
50
|
+
"@byline/admin": "6.8.0",
|
|
51
|
+
"@byline/core": "6.8.0",
|
|
52
|
+
"drizzle-orm": "^0.45.4",
|
|
53
|
+
"mysql2": "^3.24.6",
|
|
54
|
+
"uuid": "^14.0.3"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@biomejs/biome": "2.5.
|
|
58
|
-
"@
|
|
57
|
+
"@biomejs/biome": "2.5.15",
|
|
58
|
+
"@byline/auth": "6.8.0",
|
|
59
|
+
"@byline/client": "6.8.0",
|
|
60
|
+
"@byline/db-conformance": "0.0.10",
|
|
61
|
+
"@types/node": "^26.6.5",
|
|
59
62
|
"chokidar": "^5.0.0",
|
|
60
63
|
"chokidar-cli": "^3.0.0",
|
|
61
|
-
"dotenv": "^18.0.
|
|
62
|
-
"drizzle-kit": "^0.31.
|
|
64
|
+
"dotenv": "^18.0.8",
|
|
65
|
+
"drizzle-kit": "^0.31.11",
|
|
63
66
|
"npm-run-all": "^4.1.5",
|
|
64
|
-
"tsc-alias": "^1.9.
|
|
67
|
+
"tsc-alias": "^1.9.7",
|
|
65
68
|
"typescript": "^7.0.2",
|
|
66
|
-
"vitest": "^5.0.
|
|
67
|
-
"@byline/auth": "6.7.1",
|
|
68
|
-
"@byline/client": "6.7.1",
|
|
69
|
-
"@byline/db-conformance": "0.0.10"
|
|
69
|
+
"vitest": "^5.0.4"
|
|
70
70
|
},
|
|
71
71
|
"publishConfig": {
|
|
72
72
|
"access": "public",
|