@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.
@@ -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 = createAdminStore(tx);
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(createAdminStore(tx));
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, lt } from 'drizzle-orm';
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
- const result = await db
121
- .delete(adminRefreshTokens)
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.7.1",
5
+ "version": "6.8.0",
6
6
  "engines": {
7
- "node": ">=20.9.0"
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
- "drizzle-orm": "^0.45.2",
51
- "mysql2": "^3.24.4",
52
- "uuid": "^14.0.2",
53
- "@byline/admin": "6.7.1",
54
- "@byline/core": "6.7.1"
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.14",
58
- "@types/node": "^26.6.2",
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.1",
62
- "drizzle-kit": "^0.31.10",
64
+ "dotenv": "^18.0.8",
65
+ "drizzle-kit": "^0.31.11",
63
66
  "npm-run-all": "^4.1.5",
64
- "tsc-alias": "^1.9.5",
67
+ "tsc-alias": "^1.9.7",
65
68
  "typescript": "^7.0.2",
66
- "vitest": "^5.0.1",
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",