@byline/db-mysql 6.7.2 → 6.9.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/dist/modules/storage/storage-queries.d.ts +3 -4
- package/dist/modules/storage/storage-queries.js +18 -8
- package/dist/modules/storage/tests/storage-queries.test.js +2 -2
- package/package.json +6 -6
|
@@ -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
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* `implements IDocumentQueries`.
|
|
24
24
|
*/
|
|
25
25
|
import type { CollectionDefinition, DocumentFilter, FieldFilterOperator, FieldSort, FlattenedStore, ICollectionQueries, IDocumentQueries, LocaleVisibility, MissingLocalePolicy, ReadMode } from '@byline/core';
|
|
26
|
+
import { type UnavailableBlock } from '@byline/core';
|
|
26
27
|
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
|
27
28
|
import { DocumentPublishScheduleQueries } from './publish-schedules.js';
|
|
28
29
|
import type * as schema from '../../database/schema/index.js';
|
|
@@ -334,6 +335,7 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
334
335
|
availableLocales: string[];
|
|
335
336
|
_availableVersionLocales: string[];
|
|
336
337
|
_localeAgnostic: boolean;
|
|
338
|
+
unavailableBlocks?: UnavailableBlock[] | undefined;
|
|
337
339
|
restoreWarnings?: string[] | undefined;
|
|
338
340
|
} | {
|
|
339
341
|
document_version_id: string;
|
|
@@ -377,11 +379,8 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
377
379
|
availableLocales: string[];
|
|
378
380
|
_availableVersionLocales: string[];
|
|
379
381
|
_localeAgnostic: boolean;
|
|
382
|
+
unavailableBlocks?: UnavailableBlock[] | undefined;
|
|
380
383
|
} | {
|
|
381
|
-
resolved_locale?: undefined;
|
|
382
|
-
availableLocales?: undefined;
|
|
383
|
-
_availableVersionLocales?: undefined;
|
|
384
|
-
_localeAgnostic?: undefined;
|
|
385
384
|
document_version_id: string;
|
|
386
385
|
document_id: string;
|
|
387
386
|
path: string;
|
|
@@ -512,7 +512,7 @@ export class DocumentQueries {
|
|
|
512
512
|
* _id and _type for blocks and array items inline. Pure JS/core-delegate
|
|
513
513
|
* code — ports unchanged from pg.
|
|
514
514
|
*/
|
|
515
|
-
reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, lenient = false, onMissingLocale, sourceLocale, localeContext) {
|
|
515
|
+
reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, lenient = false, onMissingLocale, sourceLocale, localeContext, identity) {
|
|
516
516
|
const flattenedData = unifiedFieldValues.map((row) => extractFlattenedFieldValue(row));
|
|
517
517
|
if (metaRows) {
|
|
518
518
|
for (const meta of metaRows) {
|
|
@@ -561,14 +561,20 @@ export class DocumentQueries {
|
|
|
561
561
|
rows = flattenedData.filter((row) => row.locale === 'all' || row.field_type === 'meta');
|
|
562
562
|
}
|
|
563
563
|
}
|
|
564
|
-
const { data, warnings } = restoreFieldSetData(definition.fields, rows, resolveLocale);
|
|
564
|
+
const { data, warnings, unavailableBlocks } = restoreFieldSetData(definition.fields, rows, resolveLocale, identity ? { collectionPath: definition.path, ...identity } : undefined);
|
|
565
565
|
if (!lenient && warnings.length > 0) {
|
|
566
566
|
throw ERR_DATABASE({
|
|
567
567
|
message: `document reconstruction failed with ${warnings.length} warnings`,
|
|
568
568
|
details: { warnings },
|
|
569
569
|
}).log(getLogger());
|
|
570
570
|
}
|
|
571
|
-
return {
|
|
571
|
+
return {
|
|
572
|
+
fields: data,
|
|
573
|
+
warnings,
|
|
574
|
+
unavailableBlocks,
|
|
575
|
+
resolvedLocale,
|
|
576
|
+
localeAgnostic: ledger.localeAgnostic,
|
|
577
|
+
};
|
|
572
578
|
}
|
|
573
579
|
/**
|
|
574
580
|
* getCurrentVersionMetadata — narrow metadata fetch for the current version.
|
|
@@ -686,7 +692,7 @@ export class DocumentQueries {
|
|
|
686
692
|
.where(eq(metaStore.document_version_id, document.id));
|
|
687
693
|
const availability = (await this.getAvailableLocalesByVersion([document.id])).get(document.id);
|
|
688
694
|
const advertised = (await this.getAdvertisedLocalesByDocument([document.document_id])).get(document.document_id);
|
|
689
|
-
const { fields, warnings, resolvedLocale, localeAgnostic } = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, lenient, onMissingLocale, document.source_locale, { visibility: localeVisibility, ledger: availability, availableLocales: advertised });
|
|
695
|
+
const { fields, warnings, unavailableBlocks, resolvedLocale, localeAgnostic } = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, lenient, onMissingLocale, document.source_locale, { visibility: localeVisibility, ledger: availability, availableLocales: advertised }, { documentId: document.document_id, versionId: document.id });
|
|
690
696
|
return {
|
|
691
697
|
document_version_id: document.id,
|
|
692
698
|
document_id: document.document_id,
|
|
@@ -702,6 +708,7 @@ export class DocumentQueries {
|
|
|
702
708
|
availableLocales: advertised ?? [],
|
|
703
709
|
_availableVersionLocales: availability?.availableLocales ?? [],
|
|
704
710
|
_localeAgnostic: localeAgnostic,
|
|
711
|
+
...(unavailableBlocks.length > 0 ? { unavailableBlocks } : {}),
|
|
705
712
|
...(lenient && warnings.length > 0 ? { restoreWarnings: warnings } : {}),
|
|
706
713
|
};
|
|
707
714
|
}
|
|
@@ -781,7 +788,7 @@ export class DocumentQueries {
|
|
|
781
788
|
.where(eq(metaStore.document_version_id, document.id));
|
|
782
789
|
const availability = (await this.getAvailableLocalesByVersion([document.id])).get(document.id);
|
|
783
790
|
const advertised = (await this.getAdvertisedLocalesByDocument([document.document_id])).get(document.document_id);
|
|
784
|
-
const { fields, resolvedLocale, localeAgnostic } = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, false, onMissingLocale, document.source_locale, { visibility: localeVisibility, ledger: availability, availableLocales: advertised });
|
|
791
|
+
const { fields, unavailableBlocks, resolvedLocale, localeAgnostic } = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, false, onMissingLocale, document.source_locale, { visibility: localeVisibility, ledger: availability, availableLocales: advertised }, { documentId: document.document_id, versionId: document.id });
|
|
785
792
|
return {
|
|
786
793
|
document_version_id: document.id,
|
|
787
794
|
document_id: document.document_id,
|
|
@@ -797,6 +804,7 @@ export class DocumentQueries {
|
|
|
797
804
|
availableLocales: advertised ?? [],
|
|
798
805
|
_availableVersionLocales: availability?.availableLocales ?? [],
|
|
799
806
|
_localeAgnostic: localeAgnostic,
|
|
807
|
+
...(unavailableBlocks.length > 0 ? { unavailableBlocks } : {}),
|
|
800
808
|
};
|
|
801
809
|
}
|
|
802
810
|
const fieldValues = this.convertUnionRowToFlattenedStores(unifiedFieldValues);
|
|
@@ -853,7 +861,7 @@ export class DocumentQueries {
|
|
|
853
861
|
.from(metaStore)
|
|
854
862
|
.where(eq(metaStore.document_version_id, document.id));
|
|
855
863
|
const ledger = (await this.getAvailableLocalesByVersion([document.id])).get(document.id);
|
|
856
|
-
const { fields, resolvedLocale, localeAgnostic } = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, false, undefined, document.source_locale, { ledger });
|
|
864
|
+
const { fields, unavailableBlocks, resolvedLocale, localeAgnostic } = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, false, undefined, document.source_locale, { ledger }, { documentId: document.document_id, versionId: document.id });
|
|
857
865
|
return {
|
|
858
866
|
document_version_id: document.id,
|
|
859
867
|
document_id: document.document_id,
|
|
@@ -865,6 +873,7 @@ export class DocumentQueries {
|
|
|
865
873
|
fields,
|
|
866
874
|
resolved_locale: resolvedLocale,
|
|
867
875
|
_localeAgnostic: localeAgnostic,
|
|
876
|
+
...(unavailableBlocks.length > 0 ? { unavailableBlocks } : {}),
|
|
868
877
|
};
|
|
869
878
|
}
|
|
870
879
|
/**
|
|
@@ -1429,11 +1438,11 @@ export class DocumentQueries {
|
|
|
1429
1438
|
for (const doc of docs) {
|
|
1430
1439
|
const versionFieldValues = fieldValuesByVersion.get(doc.id) || [];
|
|
1431
1440
|
const docMetaRows = (metaByVersion.get(doc.id) ?? []);
|
|
1432
|
-
const { fields, resolvedLocale, localeAgnostic } = this.reconstructFromUnifiedRows(versionFieldValues, definition, locale, docMetaRows, false, onMissingLocale, doc.source_locale, {
|
|
1441
|
+
const { fields, unavailableBlocks, resolvedLocale, localeAgnostic } = this.reconstructFromUnifiedRows(versionFieldValues, definition, locale, docMetaRows, false, onMissingLocale, doc.source_locale, {
|
|
1433
1442
|
visibility: localeVisibility,
|
|
1434
1443
|
ledger: versionLedgers?.get(doc.id),
|
|
1435
1444
|
availableLocales: documentAdvertised?.get(doc.document_id),
|
|
1436
|
-
});
|
|
1445
|
+
}, { documentId: doc.document_id, versionId: doc.id });
|
|
1437
1446
|
// When specific fields were requested, trim the reconstructed object
|
|
1438
1447
|
// to only those fields. Store-level filtering avoids querying unused
|
|
1439
1448
|
// tables, but fields sharing a store (e.g. price + views in numeric)
|
|
@@ -1454,6 +1463,7 @@ export class DocumentQueries {
|
|
|
1454
1463
|
fields: trimmedFields,
|
|
1455
1464
|
resolved_locale: resolvedLocale,
|
|
1456
1465
|
_localeAgnostic: localeAgnostic,
|
|
1466
|
+
...(unavailableBlocks.length > 0 ? { unavailableBlocks } : {}),
|
|
1457
1467
|
};
|
|
1458
1468
|
result.push(documentWithFields);
|
|
1459
1469
|
}
|
|
@@ -216,7 +216,7 @@ describe('fallback locale selection under a changed default (mysql, live databas
|
|
|
216
216
|
const listed = documents.find((d) => d.document_id === id);
|
|
217
217
|
expect(byPath?.fields.title).toBe(byId?.fields.title);
|
|
218
218
|
expect(listed?.fields.title).toBe(byId?.fields.title);
|
|
219
|
-
expect(byPath
|
|
219
|
+
expect(byPath && 'resolved_locale' in byPath ? byPath.resolved_locale : undefined).toBe(byId?.resolved_locale);
|
|
220
220
|
expect(listed?.resolved_locale).toBe(byId?.resolved_locale);
|
|
221
221
|
}
|
|
222
222
|
});
|
|
@@ -326,7 +326,7 @@ describe('fallback locale selection under a changed default (mysql, live databas
|
|
|
326
326
|
});
|
|
327
327
|
expect(byPath?.document_id).toBe(id);
|
|
328
328
|
expect(byPath?.fields.title, 'the incomplete translation is not delivered').toBe('Slug EN');
|
|
329
|
-
expect(byPath
|
|
329
|
+
expect(byPath && 'resolved_locale' in byPath ? byPath.resolved_locale : undefined).toBe('en');
|
|
330
330
|
});
|
|
331
331
|
it('a projected list falls back to the document source, not the changed default', async () => {
|
|
332
332
|
const richText = (text) => ({
|
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.9.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,16 +47,16 @@
|
|
|
47
47
|
"dist"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@byline/admin": "6.
|
|
51
|
-
"@byline/core": "6.
|
|
50
|
+
"@byline/admin": "6.9.0",
|
|
51
|
+
"@byline/core": "6.9.0",
|
|
52
52
|
"drizzle-orm": "^0.45.4",
|
|
53
53
|
"mysql2": "^3.24.6",
|
|
54
54
|
"uuid": "^14.0.3"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@biomejs/biome": "2.5.15",
|
|
58
|
-
"@byline/auth": "6.
|
|
59
|
-
"@byline/client": "6.
|
|
58
|
+
"@byline/auth": "6.9.0",
|
|
59
|
+
"@byline/client": "6.9.0",
|
|
60
60
|
"@byline/db-conformance": "0.0.10",
|
|
61
61
|
"@types/node": "^26.6.5",
|
|
62
62
|
"chokidar": "^5.0.0",
|