@fonderie/auth 7.3.3 → 7.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -275,7 +275,7 @@ function cookieHeaders(cookies) {
275
275
 
276
276
  // src/controllers/mfa.controller.ts
277
277
  var import_qrcode = __toESM(require("qrcode"), 1);
278
- var import_core4 = require("@fonderie/core");
278
+ var import_core5 = require("@fonderie/core");
279
279
  var import_events = require("@fonderie/events");
280
280
 
281
281
  // src/config.ts
@@ -876,6 +876,7 @@ var BackupCodeModel = class {
876
876
  };
877
877
 
878
878
  // src/models/login-event.model.ts
879
+ var import_core4 = require("@fonderie/core");
879
880
  var LoginEventModel = class {
880
881
  constructor(store) {
881
882
  this.store = store;
@@ -897,12 +898,13 @@ var LoginEventModel = class {
897
898
  ]
898
899
  );
899
900
  }
900
- // Fire-and-forget variant for the login paths: recording history must never
901
- // block or fail a login (same posture as the bus.emit(...).catch(() => {})
902
- // notification sends).
903
- recordSafe(e) {
904
- void this.record(e).catch(() => {
905
- });
901
+ // Never throws — recording history must not fail a login. It IS awaited,
902
+ // though: this is a security audit trail (who signed in, from where), and a
903
+ // detached write is abandoned when a serverless instance freezes after the
904
+ // response, losing the row entirely. background() decides whether waiting is
905
+ // actually necessary for the current runtime.
906
+ async recordSafe(e) {
907
+ await (0, import_core4.background)(this.record(e));
906
908
  }
907
909
  // One page of a user's own login history, newest first. Keyset-paginated on
908
910
  // (created_at, id) — mirrors audit's event list so the two log UIs share a
@@ -974,8 +976,8 @@ function mfaController(store, config, issuer, bus) {
974
976
  users.saveMfaPendingSecret(ctx.user.id, mfaCipher.encrypt(secret)),
975
977
  backupCodes.replace(ctx.user.id, codeHashes)
976
978
  ]);
977
- return (0, import_core4.setApiResponse)(
978
- import_core4.HTTP.OK,
979
+ return (0, import_core5.setApiResponse)(
980
+ import_core5.HTTP.OK,
979
981
  "MFA_SETUP_INITIATED",
980
982
  "Scan the QR code with your authenticator app.",
981
983
  {
@@ -992,7 +994,7 @@ function mfaController(store, config, issuer, bus) {
992
994
  const token = body?.["token"];
993
995
  const isLoginCompletion = ctx.user.mfaPending === true;
994
996
  const meta = requestMeta(ctx);
995
- const failedMfa = () => loginEvents.recordSafe({
997
+ const failedMfa = async () => await loginEvents.recordSafe({
996
998
  userId: ctx.user.id,
997
999
  emailAttempted: ctx.user.email,
998
1000
  method: "mfa",
@@ -1001,35 +1003,34 @@ function mfaController(store, config, issuer, bus) {
1001
1003
  ...meta
1002
1004
  });
1003
1005
  if (typeof token !== "string") {
1004
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "token is required");
1006
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "token is required");
1005
1007
  }
1006
1008
  const pendingCipher = await users.getMfaPendingSecret(ctx.user.id);
1007
1009
  const pendingSecret = pendingCipher ? mfaCipher.decrypt(pendingCipher) : null;
1008
1010
  if (pendingSecret) {
1009
1011
  if (!verifyTotpToken(token, pendingSecret)) {
1010
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid MFA token");
1012
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid MFA token");
1011
1013
  }
1012
1014
  await users.confirmMfaSecret(ctx.user.id);
1013
- bus?.emit(import_events.NOTIFICATION_EVENT, {
1015
+ await (0, import_core5.background)(bus?.emit(import_events.NOTIFICATION_EVENT, {
1014
1016
  type: MESSAGE_KEYS.mfaEnabled,
1015
1017
  locale: ctx.user.locale,
1016
1018
  data: {},
1017
1019
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
1018
- }).catch(() => {
1019
- });
1020
- return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "MFA_VERIFIED", "MFA verified successfully.", {
1020
+ }));
1021
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "MFA_VERIFIED", "MFA verified successfully.", {
1021
1022
  mfaEnabled: true
1022
1023
  });
1023
1024
  } else if (/^[A-Z0-9]{8}$/i.test(token)) {
1024
1025
  if (!ctx.user.mfaPending) {
1025
- return (0, import_core4.setApiResponse)(
1026
- import_core4.HTTP.FORBIDDEN,
1026
+ return (0, import_core5.setApiResponse)(
1027
+ import_core5.HTTP.FORBIDDEN,
1027
1028
  "MFA_NOT_PENDING",
1028
1029
  "Use the mfaToken from the login response"
1029
1030
  );
1030
1031
  }
1031
1032
  if (!ctx.user.mfaEnabled) {
1032
- return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
1033
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
1033
1034
  }
1034
1035
  const unused = await backupCodes.findUnused(ctx.user.id);
1035
1036
  const checks = await Promise.all(
@@ -1040,26 +1041,26 @@ function mfaController(store, config, issuer, bus) {
1040
1041
  );
1041
1042
  const matched = checks.find((r) => r.match);
1042
1043
  if (!matched) {
1043
- if (isLoginCompletion) failedMfa();
1044
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid backup code");
1044
+ if (isLoginCompletion) await failedMfa();
1045
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid backup code");
1045
1046
  }
1046
1047
  await backupCodes.consume(matched.id);
1047
1048
  } else {
1048
1049
  if (!ctx.user.mfaPending) {
1049
- return (0, import_core4.setApiResponse)(
1050
- import_core4.HTTP.FORBIDDEN,
1050
+ return (0, import_core5.setApiResponse)(
1051
+ import_core5.HTTP.FORBIDDEN,
1051
1052
  "MFA_NOT_PENDING",
1052
1053
  "Use the mfaToken from the login response"
1053
1054
  );
1054
1055
  }
1055
1056
  const storedSecret = await users.getMfaSecret(ctx.user.id);
1056
1057
  if (!storedSecret) {
1057
- return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
1058
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_CONFIGURED", "MFA not configured");
1058
1059
  }
1059
1060
  const secret = mfaCipher.decrypt(storedSecret);
1060
1061
  if (!verifyTotpToken(token, secret)) {
1061
- if (isLoginCompletion) failedMfa();
1062
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid MFA token");
1062
+ if (isLoginCompletion) await failedMfa();
1063
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid MFA token");
1063
1064
  }
1064
1065
  if (!ctx.user.mfaEnabled) {
1065
1066
  await users.enableMfa(ctx.user.id);
@@ -1071,7 +1072,7 @@ function mfaController(store, config, issuer, bus) {
1071
1072
  });
1072
1073
  await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
1073
1074
  if (isLoginCompletion) {
1074
- loginEvents.recordSafe({
1075
+ await loginEvents.recordSafe({
1075
1076
  userId: ctx.user.id,
1076
1077
  emailAttempted: ctx.user.email,
1077
1078
  method: "mfa",
@@ -1081,7 +1082,7 @@ function mfaController(store, config, issuer, bus) {
1081
1082
  }
1082
1083
  const fullUser = await users.findById(ctx.user.id);
1083
1084
  if (!fullUser) {
1084
- return (0, import_core4.setApiResponse)(import_core4.HTTP.SERVER_ERROR, "SERVER_ERROR", "User not found after MFA verify");
1085
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "User not found after MFA verify");
1085
1086
  }
1086
1087
  return Response.json(
1087
1088
  {
@@ -1103,27 +1104,26 @@ function mfaController(store, config, issuer, bus) {
1103
1104
  const body = ctx.meta["body"];
1104
1105
  const token = body?.["token"];
1105
1106
  if (typeof token !== "string") {
1106
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
1107
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
1107
1108
  }
1108
1109
  if (!ctx.user.mfaEnabled) {
1109
- return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
1110
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
1110
1111
  }
1111
1112
  const storedSecret = await users.getMfaSecret(ctx.user.id);
1112
1113
  const secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;
1113
1114
  if (!secret || !verifyTotpToken(token, secret)) {
1114
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
1115
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
1115
1116
  }
1116
1117
  const plainCodes = generateBackupCodes();
1117
1118
  const codeHashes = await Promise.all(plainCodes.map((c) => hashPassword(c)));
1118
1119
  await backupCodes.replace(ctx.user.id, codeHashes);
1119
- bus?.emit(import_events.NOTIFICATION_EVENT, {
1120
+ await (0, import_core5.background)(bus?.emit(import_events.NOTIFICATION_EVENT, {
1120
1121
  type: MESSAGE_KEYS.mfaBackupCodesRegenerated,
1121
1122
  locale: ctx.user.locale,
1122
1123
  data: {},
1123
1124
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
1124
- }).catch(() => {
1125
- });
1126
- return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "BACKUP_CODES_REGENERATED", "Backup codes regenerated.", {
1125
+ }));
1126
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "BACKUP_CODES_REGENERATED", "Backup codes regenerated.", {
1127
1127
  backupCodes: plainCodes
1128
1128
  });
1129
1129
  },
@@ -1132,26 +1132,25 @@ function mfaController(store, config, issuer, bus) {
1132
1132
  const body = ctx.meta["body"];
1133
1133
  const token = body?.["token"];
1134
1134
  if (typeof token !== "string") {
1135
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
1135
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "TOTP token is required");
1136
1136
  }
1137
1137
  const user = await users.findById(ctx.user.id);
1138
1138
  if (!user || !user.mfaEnabled) {
1139
- return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
1139
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "MFA_NOT_ENABLED", "MFA is not enabled");
1140
1140
  }
1141
1141
  const storedSecret = await users.getMfaSecret(ctx.user.id);
1142
1142
  const secret = storedSecret ? mfaCipher.decrypt(storedSecret) : null;
1143
1143
  if (!secret || !verifyTotpToken(token, secret)) {
1144
- return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
1144
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CODE", "Invalid TOTP code");
1145
1145
  }
1146
1146
  await Promise.all([users.disableMfa(ctx.user.id), backupCodes.deleteByUser(ctx.user.id)]);
1147
- bus?.emit(import_events.NOTIFICATION_EVENT, {
1147
+ await (0, import_core5.background)(bus?.emit(import_events.NOTIFICATION_EVENT, {
1148
1148
  type: MESSAGE_KEYS.mfaDisabled,
1149
1149
  locale: user.locale,
1150
1150
  data: {},
1151
1151
  recipient: { email: user.email, phone: null, deviceToken: null }
1152
- }).catch(() => {
1153
- });
1154
- return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "MFA_DISABLED", "MFA disabled successfully.");
1152
+ }));
1153
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "MFA_DISABLED", "MFA disabled successfully.");
1155
1154
  }
1156
1155
  };
1157
1156
  }
@@ -1159,7 +1158,7 @@ function mfaController(store, config, issuer, bus) {
1159
1158
  // src/controllers/auth.controller.ts
1160
1159
  var import_node_crypto5 = require("crypto");
1161
1160
  var import_events2 = require("@fonderie/events");
1162
- var import_core5 = require("@fonderie/core");
1161
+ var import_core6 = require("@fonderie/core");
1163
1162
 
1164
1163
  // src/services/cooldown.ts
1165
1164
  function checkCooldown(lastSentAt, cooldownMs) {
@@ -1374,18 +1373,18 @@ function authController(store, config, bus) {
1374
1373
  if (typeof email2 === "string" && typeof password2 === "string") {
1375
1374
  const normalizedEmail = normalizeEmailSafe(email2);
1376
1375
  if (!normalizedEmail) {
1377
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1376
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1378
1377
  }
1379
1378
  if (password2.length < 8) {
1380
- return (0, import_core5.setApiResponse)(
1381
- import_core5.HTTP.UNPROCESSABLE,
1379
+ return (0, import_core6.setApiResponse)(
1380
+ import_core6.HTTP.UNPROCESSABLE,
1382
1381
  "INVALID_PARAMETER",
1383
1382
  "password must be at least 8 characters"
1384
1383
  );
1385
1384
  }
1386
1385
  const existing = await users.findByEmail(normalizedEmail);
1387
1386
  if (existing) {
1388
- return (0, import_core5.setApiResponse)(import_core5.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
1387
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
1389
1388
  }
1390
1389
  const passwordHash = await hashPassword(password2);
1391
1390
  const row = await users.create(
@@ -1395,18 +1394,18 @@ function authController(store, config, bus) {
1395
1394
  lastName
1396
1395
  );
1397
1396
  if (!row) {
1398
- return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1397
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1399
1398
  }
1400
1399
  const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1401
1400
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
1402
1401
  await emailVerif.create(row.id, pin, expiresAt);
1403
1402
  const user = await users.findById(row.id);
1404
1403
  if (!user) {
1405
- return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1404
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1406
1405
  }
1407
1406
  const reqId = ctx.meta["requestId"];
1408
1407
  const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
1409
- bus?.emit(
1408
+ await (0, import_core6.background)(bus?.emit(
1410
1409
  import_events2.NOTIFICATION_EVENT,
1411
1410
  {
1412
1411
  type: MESSAGE_KEYS.emailRegistration,
@@ -1415,9 +1414,8 @@ function authController(store, config, bus) {
1415
1414
  recipient: { email: normalizedEmail, phone: null, deviceToken: null }
1416
1415
  },
1417
1416
  reqOpts
1418
- ).catch(() => {
1419
- });
1420
- bus?.emit(
1417
+ ));
1418
+ await (0, import_core6.background)(bus?.emit(
1421
1419
  EVENT_KEYS.userRegistered,
1422
1420
  {
1423
1421
  userId: user.id,
@@ -1427,8 +1425,7 @@ function authController(store, config, bus) {
1427
1425
  loginMethod: "email"
1428
1426
  },
1429
1427
  reqOpts
1430
- ).catch(() => {
1431
- });
1428
+ ));
1432
1429
  const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
1433
1430
  loginMethod: "email"
1434
1431
  });
@@ -1454,7 +1451,7 @@ function authController(store, config, bus) {
1454
1451
  if (isValidPhone(phone2)) {
1455
1452
  const existing = await users.findByPhone(normalizePhone(phone2));
1456
1453
  if (existing) {
1457
- return (0, import_core5.setApiResponse)(import_core5.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
1454
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
1458
1455
  }
1459
1456
  const { id } = await users.findOrCreateByPhone(
1460
1457
  normalizePhone(phone2),
@@ -1466,11 +1463,11 @@ function authController(store, config, bus) {
1466
1463
  await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
1467
1464
  const user = await users.findById(id);
1468
1465
  if (!user) {
1469
- return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1466
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1470
1467
  }
1471
1468
  const reqId2 = ctx.meta["requestId"];
1472
1469
  const reqOpts2 = reqId2 !== void 0 ? { requestId: reqId2 } : void 0;
1473
- bus?.emit(
1470
+ await (0, import_core6.background)(bus?.emit(
1474
1471
  import_events2.NOTIFICATION_EVENT,
1475
1472
  {
1476
1473
  type: MESSAGE_KEYS.phoneOtp,
@@ -1479,9 +1476,8 @@ function authController(store, config, bus) {
1479
1476
  recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
1480
1477
  },
1481
1478
  reqOpts2
1482
- ).catch(() => {
1483
- });
1484
- bus?.emit(
1479
+ ));
1480
+ await (0, import_core6.background)(bus?.emit(
1485
1481
  EVENT_KEYS.userRegistered,
1486
1482
  {
1487
1483
  userId: user.id,
@@ -1491,8 +1487,7 @@ function authController(store, config, bus) {
1491
1487
  loginMethod: "phone"
1492
1488
  },
1493
1489
  reqOpts2
1494
- ).catch(() => {
1495
- });
1490
+ ));
1496
1491
  const otpToken = issueMfaPendingToken(user.id, config, "phone");
1497
1492
  return Response.json(
1498
1493
  {
@@ -1506,8 +1501,8 @@ function authController(store, config, bus) {
1506
1501
  { status: 202 }
1507
1502
  );
1508
1503
  }
1509
- return (0, import_core5.setApiResponse)(
1510
- import_core5.HTTP.UNPROCESSABLE,
1504
+ return (0, import_core6.setApiResponse)(
1505
+ import_core6.HTTP.UNPROCESSABLE,
1511
1506
  "INVALID_PARAMETER",
1512
1507
  "Provide email + password or a valid phone number"
1513
1508
  );
@@ -1519,11 +1514,11 @@ function authController(store, config, bus) {
1519
1514
  const { email: rawEmail, password: password2 } = body;
1520
1515
  const email2 = normalizeEmailSafe(rawEmail);
1521
1516
  if (!email2) {
1522
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1517
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1523
1518
  }
1524
1519
  const user = await users.findByEmail(email2);
1525
1520
  if (!user || !user.passwordHash) {
1526
- loginEvents.recordSafe({
1521
+ await loginEvents.recordSafe({
1527
1522
  userId: null,
1528
1523
  emailAttempted: email2,
1529
1524
  method: "password",
@@ -1531,7 +1526,7 @@ function authController(store, config, bus) {
1531
1526
  failureReason: "unknown_email",
1532
1527
  ...meta
1533
1528
  });
1534
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1529
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1535
1530
  }
1536
1531
  const { valid, needsRehash } = await verifyPasswordForLogin(
1537
1532
  password2,
@@ -1539,7 +1534,7 @@ function authController(store, config, bus) {
1539
1534
  config.legacyVerify
1540
1535
  );
1541
1536
  if (!valid) {
1542
- loginEvents.recordSafe({
1537
+ await loginEvents.recordSafe({
1543
1538
  userId: user.id,
1544
1539
  emailAttempted: email2,
1545
1540
  method: "password",
@@ -1547,21 +1542,21 @@ function authController(store, config, bus) {
1547
1542
  failureReason: "bad_password",
1548
1543
  ...meta
1549
1544
  });
1550
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1545
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1551
1546
  }
1552
1547
  if (needsRehash) {
1553
1548
  await users.updatePassword(user.id, await hashPassword(password2));
1554
1549
  }
1555
1550
  if (user.suspended) {
1556
- return (0, import_core5.setApiResponse)(
1557
- import_core5.HTTP.FORBIDDEN,
1551
+ return (0, import_core6.setApiResponse)(
1552
+ import_core6.HTTP.FORBIDDEN,
1558
1553
  "ACCOUNT_SUSPENDED",
1559
1554
  "Account suspended. Please contact support."
1560
1555
  );
1561
1556
  }
1562
1557
  if (user.mfaEnabled) {
1563
1558
  const mfaToken = issueMfaPendingToken(user.id, config, "email");
1564
- return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
1559
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
1565
1560
  mfaToken
1566
1561
  });
1567
1562
  }
@@ -1569,7 +1564,7 @@ function authController(store, config, bus) {
1569
1564
  loginMethod: "email"
1570
1565
  });
1571
1566
  await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
1572
- loginEvents.recordSafe({
1567
+ await loginEvents.recordSafe({
1573
1568
  userId: user.id,
1574
1569
  emailAttempted: email2,
1575
1570
  method: "password",
@@ -1598,11 +1593,11 @@ function authController(store, config, bus) {
1598
1593
  if (isValidPhone(phone2)) {
1599
1594
  const user = await users.findByPhone(normalizePhone(phone2));
1600
1595
  if (!user) {
1601
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1596
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1602
1597
  }
1603
1598
  if (user.suspended) {
1604
- return (0, import_core5.setApiResponse)(
1605
- import_core5.HTTP.FORBIDDEN,
1599
+ return (0, import_core6.setApiResponse)(
1600
+ import_core6.HTTP.FORBIDDEN,
1606
1601
  "ACCOUNT_SUSPENDED",
1607
1602
  "Account suspended. Please contact support."
1608
1603
  );
@@ -1610,13 +1605,12 @@ function authController(store, config, bus) {
1610
1605
  const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1611
1606
  const expiresAt = new Date(Date.now() + OTP_TTL_MS);
1612
1607
  await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
1613
- bus?.emit(import_events2.NOTIFICATION_EVENT, {
1608
+ await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
1614
1609
  type: MESSAGE_KEYS.phoneOtp,
1615
1610
  locale: user.locale,
1616
1611
  data: { otp },
1617
1612
  recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
1618
- }).catch(() => {
1619
- });
1613
+ }));
1620
1614
  const otpToken = issueMfaPendingToken(user.id, config, "phone");
1621
1615
  return Response.json(
1622
1616
  {
@@ -1627,8 +1621,8 @@ function authController(store, config, bus) {
1627
1621
  { status: 202 }
1628
1622
  );
1629
1623
  }
1630
- return (0, import_core5.setApiResponse)(
1631
- import_core5.HTTP.UNPROCESSABLE,
1624
+ return (0, import_core6.setApiResponse)(
1625
+ import_core6.HTTP.UNPROCESSABLE,
1632
1626
  "INVALID_PARAMETER",
1633
1627
  "Provide email + password or a valid phone number"
1634
1628
  );
@@ -1653,23 +1647,23 @@ function authController(store, config, bus) {
1653
1647
  refresh: async (ctx) => {
1654
1648
  const token = extractRefreshToken(ctx);
1655
1649
  if (!token) {
1656
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
1650
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
1657
1651
  }
1658
1652
  const payload = verifyToken(token, config);
1659
1653
  if (!payload || payload.type !== "refresh") {
1660
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
1654
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
1661
1655
  }
1662
1656
  const valid = await sessions.exists(token);
1663
1657
  if (!valid) {
1664
- return (0, import_core5.setApiResponse)(
1665
- import_core5.HTTP.UNAUTHORIZED,
1658
+ return (0, import_core6.setApiResponse)(
1659
+ import_core6.HTTP.UNAUTHORIZED,
1666
1660
  "TOKEN_REFRESH_FAILED",
1667
1661
  "Session expired or already revoked"
1668
1662
  );
1669
1663
  }
1670
1664
  const user = await users.findById(payload.sub);
1671
1665
  if (!user || user.suspended || user.deletedAt) {
1672
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
1666
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
1673
1667
  }
1674
1668
  await sessions.delete(token);
1675
1669
  const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
@@ -1693,16 +1687,16 @@ function authController(store, config, bus) {
1693
1687
  const body = ctx.meta["body"];
1694
1688
  const rawEmail = body?.["email"];
1695
1689
  if (typeof rawEmail !== "string") {
1696
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
1690
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
1697
1691
  }
1698
1692
  const email2 = normalizeEmailSafe(rawEmail);
1699
1693
  if (!email2) {
1700
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1694
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1701
1695
  }
1702
1696
  const user = await users.findByEmail(email2);
1703
1697
  if (!user) {
1704
- return (0, import_core5.setApiResponse)(
1705
- import_core5.HTTP.OK,
1698
+ return (0, import_core6.setApiResponse)(
1699
+ import_core6.HTTP.OK,
1706
1700
  "PASSWORD_RESET_EMAIL_SENT",
1707
1701
  "Password reset email sent (if account exists)."
1708
1702
  );
@@ -1711,8 +1705,8 @@ function authController(store, config, bus) {
1711
1705
  const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
1712
1706
  const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
1713
1707
  if (remaining > 0) {
1714
- return (0, import_core5.setApiResponse)(
1715
- import_core5.HTTP.OK,
1708
+ return (0, import_core6.setApiResponse)(
1709
+ import_core6.HTTP.OK,
1716
1710
  "PASSWORD_RESET_EMAIL_SENT",
1717
1711
  "Password reset email sent (if account exists)."
1718
1712
  );
@@ -1723,15 +1717,14 @@ function authController(store, config, bus) {
1723
1717
  await passwordReset.create(user.id, pin, token, expiresAt);
1724
1718
  const base = resolved.passwordResetUrl;
1725
1719
  const resetUrl = base ? `${base}${base.includes("?") ? "&" : "?"}token=${token}` : "";
1726
- bus?.emit(import_events2.NOTIFICATION_EVENT, {
1720
+ await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
1727
1721
  type: MESSAGE_KEYS.passwordReset,
1728
1722
  locale: user.locale,
1729
1723
  recipient: { email: email2, phone: null, deviceToken: null },
1730
1724
  data: { pin, token, resetUrl }
1731
- }).catch(() => {
1732
- });
1733
- return (0, import_core5.setApiResponse)(
1734
- import_core5.HTTP.OK,
1725
+ }));
1726
+ return (0, import_core6.setApiResponse)(
1727
+ import_core6.HTTP.OK,
1735
1728
  "PASSWORD_RESET_EMAIL_SENT",
1736
1729
  "Password reset email sent (if account exists)."
1737
1730
  );
@@ -1742,11 +1735,11 @@ function authController(store, config, bus) {
1742
1735
  const password2 = body?.["password"];
1743
1736
  const rawToken = body?.["token"];
1744
1737
  if (typeof password2 !== "string") {
1745
- return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
1738
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
1746
1739
  }
1747
1740
  if (password2.length < 8) {
1748
- return (0, import_core5.setApiResponse)(
1749
- import_core5.HTTP.UNPROCESSABLE,
1741
+ return (0, import_core6.setApiResponse)(
1742
+ import_core6.HTTP.UNPROCESSABLE,
1750
1743
  "INVALID_PARAMETER",
1751
1744
  "password must be at least 8 characters"
1752
1745
  );
@@ -1756,8 +1749,8 @@ function authController(store, config, bus) {
1756
1749
  row = await passwordReset.findByToken(rawToken.trim());
1757
1750
  } else {
1758
1751
  if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
1759
- return (0, import_core5.setApiResponse)(
1760
- import_core5.HTTP.UNPROCESSABLE,
1752
+ return (0, import_core6.setApiResponse)(
1753
+ import_core6.HTTP.UNPROCESSABLE,
1761
1754
  "INVALID_PARAMETER",
1762
1755
  "a 6-digit pin or a reset token is required"
1763
1756
  );
@@ -1765,8 +1758,8 @@ function authController(store, config, bus) {
1765
1758
  row = await passwordReset.findByPin(raw.trim());
1766
1759
  }
1767
1760
  if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
1768
- return (0, import_core5.setApiResponse)(
1769
- import_core5.HTTP.BAD_REQUEST,
1761
+ return (0, import_core6.setApiResponse)(
1762
+ import_core6.HTTP.BAD_REQUEST,
1770
1763
  "PASSWORD_RESET_FAILED",
1771
1764
  "Invalid or expired reset credentials"
1772
1765
  );
@@ -1784,14 +1777,14 @@ function authController(store, config, bus) {
1784
1777
  tx.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [row.userId])
1785
1778
  ]);
1786
1779
  });
1787
- return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
1780
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
1788
1781
  },
1789
1782
  verify: async (ctx) => {
1790
1783
  if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
1791
- return (0, import_core5.setApiResponse)(import_core5.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
1784
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
1792
1785
  }
1793
1786
  if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
1794
- return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "VERIFIED", "Email verified successfully.", {
1787
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFIED", "Email verified successfully.", {
1795
1788
  verified: true,
1796
1789
  email: ctx.user.email
1797
1790
  });
@@ -1799,8 +1792,8 @@ function authController(store, config, bus) {
1799
1792
  const body = ctx.meta["body"];
1800
1793
  const raw = body?.["token"];
1801
1794
  if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
1802
- return (0, import_core5.setApiResponse)(
1803
- import_core5.HTTP.UNPROCESSABLE,
1795
+ return (0, import_core6.setApiResponse)(
1796
+ import_core6.HTTP.UNPROCESSABLE,
1804
1797
  "INVALID_PARAMETER",
1805
1798
  "token must be a 6-digit code"
1806
1799
  );
@@ -1809,12 +1802,12 @@ function authController(store, config, bus) {
1809
1802
  if (ctx.user.loginMethod === "phone") {
1810
1803
  const record = await phoneVerif.findByUser(ctx.user.id, pin);
1811
1804
  if (!record) {
1812
- return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
1805
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
1813
1806
  }
1814
1807
  if (/* @__PURE__ */ new Date() > record.expiresAt) {
1815
1808
  await phoneVerif.deleteByUser(ctx.user.id);
1816
- return (0, import_core5.setApiResponse)(
1817
- import_core5.HTTP.BAD_REQUEST,
1809
+ return (0, import_core6.setApiResponse)(
1810
+ import_core6.HTTP.BAD_REQUEST,
1818
1811
  "VERIFICATION_FAILED",
1819
1812
  "Verification code expired"
1820
1813
  );
@@ -1827,11 +1820,11 @@ function authController(store, config, bus) {
1827
1820
  await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));
1828
1821
  const verifiedUser = await users.findById(ctx.user.id);
1829
1822
  if (!verifiedUser) {
1830
- return (0, import_core5.setApiResponse)(import_core5.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
1823
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
1831
1824
  }
1832
1825
  if (verifiedUser.suspended) {
1833
- return (0, import_core5.setApiResponse)(
1834
- import_core5.HTTP.FORBIDDEN,
1826
+ return (0, import_core6.setApiResponse)(
1827
+ import_core6.HTTP.FORBIDDEN,
1835
1828
  "ACCOUNT_SUSPENDED",
1836
1829
  "Account suspended. Please contact support."
1837
1830
  );
@@ -1853,10 +1846,10 @@ function authController(store, config, bus) {
1853
1846
  }
1854
1847
  const row = await emailVerif.findByUser(ctx.user.id, pin);
1855
1848
  if (!row) {
1856
- return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
1849
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
1857
1850
  }
1858
1851
  if (/* @__PURE__ */ new Date() > row.expiresAt) {
1859
- return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
1852
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
1860
1853
  }
1861
1854
  await store.transaction(async (tx) => {
1862
1855
  await Promise.all([
@@ -1870,30 +1863,30 @@ function authController(store, config, bus) {
1870
1863
  ])
1871
1864
  ]);
1872
1865
  });
1873
- return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "VERIFIED", "Email verified successfully.", {
1866
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFIED", "Email verified successfully.", {
1874
1867
  verified: true,
1875
1868
  email: ctx.user.email
1876
1869
  });
1877
1870
  },
1878
1871
  sendVerification: async (ctx) => {
1879
1872
  if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
1880
- return (0, import_core5.setApiResponse)(import_core5.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
1873
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
1881
1874
  }
1882
1875
  const resolved = { ...config, ...config.resolve?.(ctx) };
1883
1876
  const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
1884
1877
  if (ctx.user.loginMethod === "phone") {
1885
1878
  const phone2 = ctx.user.phone;
1886
1879
  if (!phone2) {
1887
- return (0, import_core5.setApiResponse)(
1888
- import_core5.HTTP.BAD_REQUEST,
1880
+ return (0, import_core6.setApiResponse)(
1881
+ import_core6.HTTP.BAD_REQUEST,
1889
1882
  "NO_PHONE_ON_ACCOUNT",
1890
1883
  "No phone number associated with this account"
1891
1884
  );
1892
1885
  }
1893
1886
  const remaining2 = checkCooldown(await phoneVerif.findLastSentAt(ctx.user.id), cooldown);
1894
1887
  if (remaining2 > 0) {
1895
- return (0, import_core5.setApiResponse)(
1896
- import_core5.HTTP.TOO_MANY_REQUESTS,
1888
+ return (0, import_core6.setApiResponse)(
1889
+ import_core6.HTTP.TOO_MANY_REQUESTS,
1897
1890
  "VERIFICATION_COOLDOWN",
1898
1891
  "Please wait before requesting a new code.",
1899
1892
  {
@@ -1904,36 +1897,35 @@ function authController(store, config, bus) {
1904
1897
  const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1905
1898
  const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
1906
1899
  await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
1907
- bus?.emit(import_events2.NOTIFICATION_EVENT, {
1900
+ await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
1908
1901
  type: MESSAGE_KEYS.phoneOtp,
1909
1902
  locale: ctx.user.locale,
1910
1903
  data: { otp },
1911
1904
  recipient: { email: null, phone: phone2, deviceToken: null }
1912
- }).catch(() => {
1913
- });
1914
- return (0, import_core5.setApiResponse)(
1915
- import_core5.HTTP.OK,
1905
+ }));
1906
+ return (0, import_core6.setApiResponse)(
1907
+ import_core6.HTTP.OK,
1916
1908
  "VERIFICATION_SENT",
1917
1909
  "A verification code has been sent to your phone."
1918
1910
  );
1919
1911
  }
1920
1912
  if (!ctx.user.email) {
1921
- return (0, import_core5.setApiResponse)(
1922
- import_core5.HTTP.BAD_REQUEST,
1913
+ return (0, import_core6.setApiResponse)(
1914
+ import_core6.HTTP.BAD_REQUEST,
1923
1915
  "NO_EMAIL_ON_ACCOUNT",
1924
1916
  "No email address associated with this account"
1925
1917
  );
1926
1918
  }
1927
1919
  if (ctx.user.emailVerifiedAt) {
1928
- return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "EMAIL_VERIFIED", "Email already verified.", {
1920
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "EMAIL_VERIFIED", "Email already verified.", {
1929
1921
  verified: true,
1930
1922
  email: ctx.user.email
1931
1923
  });
1932
1924
  }
1933
1925
  const remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user.id), cooldown);
1934
1926
  if (remaining > 0) {
1935
- return (0, import_core5.setApiResponse)(
1936
- import_core5.HTTP.TOO_MANY_REQUESTS,
1927
+ return (0, import_core6.setApiResponse)(
1928
+ import_core6.HTTP.TOO_MANY_REQUESTS,
1937
1929
  "VERIFICATION_COOLDOWN",
1938
1930
  "Please wait before requesting a new code.",
1939
1931
  {
@@ -1944,14 +1936,13 @@ function authController(store, config, bus) {
1944
1936
  const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1945
1937
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
1946
1938
  await emailVerif.replace(ctx.user.id, pin, expiresAt);
1947
- bus?.emit(import_events2.NOTIFICATION_EVENT, {
1939
+ await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
1948
1940
  type: MESSAGE_KEYS.emailVerification,
1949
1941
  locale: ctx.user.locale,
1950
1942
  recipient: { email: ctx.user.email, phone: null, deviceToken: null },
1951
1943
  data: { pin }
1952
- }).catch(() => {
1953
- });
1954
- return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
1944
+ }));
1945
+ return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
1955
1946
  email: ctx.user.email
1956
1947
  });
1957
1948
  }
@@ -1960,11 +1951,11 @@ function authController(store, config, bus) {
1960
1951
 
1961
1952
  // src/controllers/user.controller.ts
1962
1953
  var import_node_crypto6 = require("crypto");
1963
- var import_core7 = require("@fonderie/core");
1954
+ var import_core8 = require("@fonderie/core");
1964
1955
  var import_events3 = require("@fonderie/events");
1965
1956
 
1966
1957
  // src/dtos/login-activity.ts
1967
- var import_core6 = require("@fonderie/core");
1958
+ var import_core7 = require("@fonderie/core");
1968
1959
  function toLoginEventDTO(row) {
1969
1960
  return {
1970
1961
  id: row.id,
@@ -1978,10 +1969,10 @@ function toLoginEventDTO(row) {
1978
1969
  }
1979
1970
  function toLoginHistoryPageDTO(page) {
1980
1971
  const last = page.events[page.events.length - 1];
1981
- const nextCursor = page.hasMore && last ? (0, import_core6.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
1972
+ const nextCursor = page.hasMore && last ? (0, import_core7.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
1982
1973
  return { events: page.events.map(toLoginEventDTO), nextCursor };
1983
1974
  }
1984
- var decodeLoginCursor = import_core6.decodeKeysetCursor;
1975
+ var decodeLoginCursor = import_core7.decodeKeysetCursor;
1985
1976
  function toSessionDTO(row, currentSid) {
1986
1977
  return {
1987
1978
  id: row.id,
@@ -2010,9 +2001,9 @@ function userController(store, config, bus) {
2010
2001
  me: async (ctx) => {
2011
2002
  const user = await users.findById(ctx.user.id);
2012
2003
  if (!user) {
2013
- return (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2004
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2014
2005
  }
2015
- return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
2006
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
2016
2007
  user: toUserDTO(user, ctx.user.phoneVerified)
2017
2008
  });
2018
2009
  },
@@ -2023,14 +2014,14 @@ function userController(store, config, bus) {
2023
2014
  const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
2024
2015
  const outcomeParam = params.get("outcome");
2025
2016
  if (outcomeParam !== null && outcomeParam !== "success" && outcomeParam !== "failed") {
2026
- return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
2017
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
2027
2018
  }
2028
2019
  const cursorParam = params.get("cursor");
2029
2020
  let cursor = null;
2030
2021
  if (cursorParam) {
2031
2022
  cursor = decodeLoginCursor(cursorParam);
2032
2023
  if (!cursor) {
2033
- return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
2024
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
2034
2025
  }
2035
2026
  }
2036
2027
  const query = { userId: ctx.user.id, limit };
@@ -2041,8 +2032,8 @@ function userController(store, config, bus) {
2041
2032
  if (from) query.from = new Date(from);
2042
2033
  if (to) query.to = new Date(to);
2043
2034
  const page = await loginEvents.listByUser(query);
2044
- return (0, import_core7.setApiResponse)(
2045
- import_core7.HTTP.OK,
2035
+ return (0, import_core8.setApiResponse)(
2036
+ import_core8.HTTP.OK,
2046
2037
  "LOGIN_HISTORY_FETCHED",
2047
2038
  "Login history retrieved.",
2048
2039
  toLoginHistoryPageDTO(page)
@@ -2052,32 +2043,32 @@ function userController(store, config, bus) {
2052
2043
  listSessions: async (ctx) => {
2053
2044
  const rows = await sessions.listLiveByUser(ctx.user.id);
2054
2045
  const currentSid = ctx.user.sid ?? null;
2055
- return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
2046
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
2056
2047
  sessions: rows.map((r) => toSessionDTO(r, currentSid))
2057
2048
  });
2058
2049
  },
2059
2050
  terminateSession: async (ctx) => {
2060
2051
  const id = ctx.meta["params"]?.["id"];
2061
2052
  if (!id) {
2062
- return (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
2053
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
2063
2054
  }
2064
2055
  const removed = await sessions.terminateById(ctx.user.id, id);
2065
2056
  if (!removed) {
2066
- return (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
2057
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
2067
2058
  }
2068
- return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "SESSION_TERMINATED", "Session terminated.", { id });
2059
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSION_TERMINATED", "Session terminated.", { id });
2069
2060
  },
2070
2061
  terminateOtherSessions: async (ctx) => {
2071
2062
  const currentSid = ctx.user.sid ?? null;
2072
2063
  if (!currentSid) {
2073
- return (0, import_core7.setApiResponse)(
2074
- import_core7.HTTP.UNPROCESSABLE,
2064
+ return (0, import_core8.setApiResponse)(
2065
+ import_core8.HTTP.UNPROCESSABLE,
2075
2066
  "NO_CURRENT_SESSION",
2076
2067
  "Cannot terminate others without a current session context"
2077
2068
  );
2078
2069
  }
2079
2070
  const count = await sessions.terminateOthers(ctx.user.id, currentSid);
2080
- return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
2071
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
2081
2072
  },
2082
2073
  updateProfile: async (ctx) => {
2083
2074
  const body = ctx.meta["body"];
@@ -2087,18 +2078,18 @@ function userController(store, config, bus) {
2087
2078
  if (body?.[key] !== void 0) fields[key] = body[key];
2088
2079
  }
2089
2080
  if (Object.keys(fields).length === 0) {
2090
- return (0, import_core7.setApiResponse)(
2091
- import_core7.HTTP.UNPROCESSABLE,
2081
+ return (0, import_core8.setApiResponse)(
2082
+ import_core8.HTTP.UNPROCESSABLE,
2092
2083
  "INVALID_PARAMETER",
2093
2084
  "Provide at least one of: firstName, lastName, avatarUrl"
2094
2085
  );
2095
2086
  }
2096
2087
  const row = await users.update(ctx.user.id, fields);
2097
2088
  if (!row) {
2098
- return (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2089
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2099
2090
  }
2100
2091
  const updated = await users.findById(ctx.user.id);
2101
- return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "PROFILE_UPDATED", "Profile updated.", {
2092
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PROFILE_UPDATED", "Profile updated.", {
2102
2093
  user: toUserDTO(updated, ctx.user.phoneVerified)
2103
2094
  });
2104
2095
  },
@@ -2115,14 +2106,14 @@ function userController(store, config, bus) {
2115
2106
  if (Object.keys(patch).length > 0) fields.patch = patch;
2116
2107
  const row = await users.updatePreferences(ctx.user.id, fields);
2117
2108
  if (!row) {
2118
- return (0, import_core7.setApiResponse)(
2119
- import_core7.HTTP.UNPROCESSABLE,
2109
+ return (0, import_core8.setApiResponse)(
2110
+ import_core8.HTTP.UNPROCESSABLE,
2120
2111
  "INVALID_PARAMETER",
2121
2112
  "Provide at least one preference field"
2122
2113
  );
2123
2114
  }
2124
2115
  const updated = await users.findById(ctx.user.id);
2125
- return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
2116
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
2126
2117
  user: toUserDTO(updated, ctx.user.phoneVerified)
2127
2118
  });
2128
2119
  },
@@ -2131,46 +2122,44 @@ function userController(store, config, bus) {
2131
2122
  const newEmail = body?.["email"];
2132
2123
  const normalised = typeof newEmail === "string" ? normalizeEmailSafe(newEmail) : null;
2133
2124
  if (!normalised) {
2134
- return (0, import_core7.setApiResponse)(
2135
- import_core7.HTTP.UNPROCESSABLE,
2125
+ return (0, import_core8.setApiResponse)(
2126
+ import_core8.HTTP.UNPROCESSABLE,
2136
2127
  "INVALID_PARAMETER",
2137
2128
  "A valid email address is required"
2138
2129
  );
2139
2130
  }
2140
2131
  const oldEmail = ctx.user.email;
2141
2132
  if (normalised === oldEmail) {
2142
- return (0, import_core7.setApiResponse)(
2143
- import_core7.HTTP.UNPROCESSABLE,
2133
+ return (0, import_core8.setApiResponse)(
2134
+ import_core8.HTTP.UNPROCESSABLE,
2144
2135
  "INVALID_PARAMETER",
2145
2136
  "New email must differ from current email"
2146
2137
  );
2147
2138
  }
2148
2139
  const existing = await users.findByEmail(normalised);
2149
2140
  if (existing) {
2150
- return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
2141
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
2151
2142
  }
2152
2143
  const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
2153
2144
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
2154
2145
  await emailVerif.replace(ctx.user.id, pin, expiresAt);
2155
2146
  await users.updateEmail(ctx.user.id, normalised);
2156
- bus?.emit(import_events3.NOTIFICATION_EVENT, {
2147
+ await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2157
2148
  type: MESSAGE_KEYS.emailVerification,
2158
2149
  locale: ctx.user.locale,
2159
2150
  data: { pin },
2160
2151
  recipient: { email: normalised, phone: null, deviceToken: null }
2161
- }).catch(() => {
2162
- });
2152
+ }));
2163
2153
  if (oldEmail) {
2164
- bus?.emit(import_events3.NOTIFICATION_EVENT, {
2154
+ await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2165
2155
  type: MESSAGE_KEYS.emailChanged,
2166
2156
  locale: ctx.user.locale,
2167
2157
  data: { newEmail: normalised },
2168
2158
  recipient: { email: oldEmail, phone: null, deviceToken: null }
2169
- }).catch(() => {
2170
- });
2159
+ }));
2171
2160
  }
2172
- return (0, import_core7.setApiResponse)(
2173
- import_core7.HTTP.OK,
2161
+ return (0, import_core8.setApiResponse)(
2162
+ import_core8.HTTP.OK,
2174
2163
  "EMAIL_UPDATED",
2175
2164
  "Email updated. A verification code has been sent to your new address.",
2176
2165
  {
@@ -2182,8 +2171,8 @@ function userController(store, config, bus) {
2182
2171
  const body = ctx.meta["body"];
2183
2172
  const newPhone = body?.["phone"];
2184
2173
  if (!isValidPhone2(newPhone)) {
2185
- return (0, import_core7.setApiResponse)(
2186
- import_core7.HTTP.UNPROCESSABLE,
2174
+ return (0, import_core8.setApiResponse)(
2175
+ import_core8.HTTP.UNPROCESSABLE,
2187
2176
  "INVALID_PARAMETER",
2188
2177
  "A valid phone number is required"
2189
2178
  );
@@ -2191,30 +2180,28 @@ function userController(store, config, bus) {
2191
2180
  const normalised = normalizePhone2(newPhone);
2192
2181
  const existing = await users.findByPhone(normalised);
2193
2182
  if (existing) {
2194
- return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
2183
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
2195
2184
  }
2196
2185
  const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
2197
2186
  const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
2198
2187
  await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
2199
2188
  await users.updatePhone(ctx.user.id, normalised);
2200
- bus?.emit(import_events3.NOTIFICATION_EVENT, {
2189
+ await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2201
2190
  type: MESSAGE_KEYS.phoneOtp,
2202
2191
  locale: ctx.user.locale,
2203
2192
  data: { otp },
2204
2193
  recipient: { email: null, phone: normalised, deviceToken: null }
2205
- }).catch(() => {
2206
- });
2194
+ }));
2207
2195
  if (ctx.user.email) {
2208
- bus?.emit(import_events3.NOTIFICATION_EVENT, {
2196
+ await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2209
2197
  type: MESSAGE_KEYS.phoneChanged,
2210
2198
  locale: ctx.user.locale,
2211
2199
  data: {},
2212
2200
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
2213
- }).catch(() => {
2214
- });
2201
+ }));
2215
2202
  }
2216
- return (0, import_core7.setApiResponse)(
2217
- import_core7.HTTP.OK,
2203
+ return (0, import_core8.setApiResponse)(
2204
+ import_core8.HTTP.OK,
2218
2205
  "PHONE_UPDATED",
2219
2206
  "Phone number updated. A verification code has been sent to your new number.",
2220
2207
  {
@@ -2227,33 +2214,32 @@ function userController(store, config, bus) {
2227
2214
  const currentPassword = body?.["currentPassword"];
2228
2215
  const newPassword = body?.["newPassword"];
2229
2216
  if (typeof currentPassword !== "string" || !currentPassword) {
2230
- return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
2217
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
2231
2218
  }
2232
2219
  if (typeof newPassword !== "string" || newPassword.length < 8) {
2233
- return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
2220
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
2234
2221
  }
2235
2222
  const user = await users.findById(ctx.user.id);
2236
- if (!user) return (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2223
+ if (!user) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2237
2224
  const { hashPassword: hashPassword2, verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password(), password_exports));
2238
2225
  const valid = await verifyPassword2(currentPassword, user.passwordHash ?? "");
2239
2226
  if (!valid) {
2240
- return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
2227
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
2241
2228
  }
2242
2229
  const hash = await hashPassword2(newPassword);
2243
2230
  await users.updatePassword(ctx.user.id, hash);
2244
2231
  await sessions.deleteByUser(ctx.user.id);
2245
- return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "PASSWORD_CHANGED", "Password updated successfully.");
2232
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PASSWORD_CHANGED", "Password updated successfully.");
2246
2233
  },
2247
2234
  deleteMe: async (ctx) => {
2248
2235
  const userId = ctx.user.id;
2249
2236
  await users.softDelete(userId);
2250
2237
  const reqId = ctx.meta["requestId"];
2251
- bus?.emit(
2238
+ await (0, import_core8.background)(bus?.emit(
2252
2239
  EVENT_KEYS.userDeleted,
2253
2240
  { userId },
2254
2241
  reqId !== void 0 ? { requestId: reqId } : void 0
2255
- ).catch(() => {
2256
- });
2242
+ ));
2257
2243
  return Response.json(
2258
2244
  { reason: "ACCOUNT_DELETED", explanation: "Account successfully deleted." },
2259
2245
  {
@@ -2267,7 +2253,7 @@ function userController(store, config, bus) {
2267
2253
  // no MFA secret, no session tokens (session metadata only).
2268
2254
  exportMe: async (ctx) => {
2269
2255
  const user = await users.findById(ctx.user.id);
2270
- if (!user) return (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2256
+ if (!user) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2271
2257
  const sessionRows = await sessions.listByUser(ctx.user.id);
2272
2258
  const bundle = {
2273
2259
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2276,8 +2262,8 @@ function userController(store, config, bus) {
2276
2262
  id: s.id,
2277
2263
  userAgent: s.userAgent,
2278
2264
  ipAddress: s.ipAddress,
2279
- createdAt: (0, import_core7.dateOrEmpty)(s.createdAt),
2280
- expiresAt: (0, import_core7.dateOrEmpty)(s.expiresAt)
2265
+ createdAt: (0, import_core8.dateOrEmpty)(s.createdAt),
2266
+ expiresAt: (0, import_core8.dateOrEmpty)(s.expiresAt)
2281
2267
  })),
2282
2268
  security: {
2283
2269
  mfaEnabled: user.mfaEnabled === true,
@@ -2296,7 +2282,7 @@ function userController(store, config, bus) {
2296
2282
  }
2297
2283
  bundle["modules"] = modules;
2298
2284
  }
2299
- return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "DATA_EXPORT", "Your account data.", bundle);
2285
+ return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "DATA_EXPORT", "Your account data.", bundle);
2300
2286
  }
2301
2287
  };
2302
2288
  }
@@ -2304,7 +2290,7 @@ function userController(store, config, bus) {
2304
2290
  // src/controllers/oauth.controller.ts
2305
2291
  var import_node_crypto7 = require("crypto");
2306
2292
  var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
2307
- var import_core8 = require("@fonderie/core");
2293
+ var import_core9 = require("@fonderie/core");
2308
2294
 
2309
2295
  // src/models/consumed-token.model.ts
2310
2296
  var ConsumedTokenModel = class {
@@ -2413,18 +2399,18 @@ function oauthController(store, config) {
2413
2399
  const completeAppleLogin = async (ctx, claims) => {
2414
2400
  const meta = requestMeta(ctx);
2415
2401
  if (!claims.email) {
2416
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "No email in Apple identity token");
2402
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "No email in Apple identity token");
2417
2403
  }
2418
2404
  if (!claims.emailVerified) {
2419
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Apple account email is not verified");
2405
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Apple account email is not verified");
2420
2406
  }
2421
2407
  const normalizedEmail = normalizeEmailSafe(claims.email);
2422
2408
  if (!normalizedEmail) {
2423
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid email in Apple identity token");
2409
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid email in Apple identity token");
2424
2410
  }
2425
2411
  const upserted = await users.upsertByProvider(normalizedEmail, "apple", claims.sub ?? "");
2426
2412
  if (!upserted) {
2427
- loginEvents.recordSafe({
2413
+ await loginEvents.recordSafe({
2428
2414
  userId: null,
2429
2415
  emailAttempted: normalizedEmail,
2430
2416
  method: "oauth-apple",
@@ -2432,15 +2418,15 @@ function oauthController(store, config) {
2432
2418
  failureReason: "provider_upsert_failed",
2433
2419
  ...meta
2434
2420
  });
2435
- return (0, import_core8.setApiResponse)(import_core8.HTTP.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
2421
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
2436
2422
  }
2437
2423
  const fullUser = await users.findById(upserted.id);
2438
2424
  if (!fullUser) {
2439
- return (0, import_core8.setApiResponse)(import_core8.HTTP.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
2425
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
2440
2426
  }
2441
2427
  const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, { loginMethod: "apple" });
2442
2428
  await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
2443
- loginEvents.recordSafe({
2429
+ await loginEvents.recordSafe({
2444
2430
  userId: upserted.id,
2445
2431
  emailAttempted: normalizedEmail,
2446
2432
  method: "oauth-apple",
@@ -2470,8 +2456,8 @@ function oauthController(store, config) {
2470
2456
  googleInit: async (_ctx) => {
2471
2457
  const google = config.google;
2472
2458
  if (!google) {
2473
- return (0, import_core8.setApiResponse)(
2474
- import_core8.HTTP.NOT_IMPLEMENTED,
2459
+ return (0, import_core9.setApiResponse)(
2460
+ import_core9.HTTP.NOT_IMPLEMENTED,
2475
2461
  "NOT_CONFIGURED",
2476
2462
  "Google OAuth not configured"
2477
2463
  );
@@ -2505,8 +2491,8 @@ function oauthController(store, config) {
2505
2491
  googleCallback: async (ctx) => {
2506
2492
  const google = config.google;
2507
2493
  if (!google) {
2508
- return (0, import_core8.setApiResponse)(
2509
- import_core8.HTTP.NOT_IMPLEMENTED,
2494
+ return (0, import_core9.setApiResponse)(
2495
+ import_core9.HTTP.NOT_IMPLEMENTED,
2510
2496
  "NOT_CONFIGURED",
2511
2497
  "Google OAuth not configured"
2512
2498
  );
@@ -2514,14 +2500,14 @@ function oauthController(store, config) {
2514
2500
  const url = new URL(ctx.request.url);
2515
2501
  const code = url.searchParams.get("code");
2516
2502
  if (!code) {
2517
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
2503
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
2518
2504
  }
2519
2505
  const returnedState = url.searchParams.get("state") ?? "";
2520
2506
  const cookieHeader = ctx.request.headers.get("cookie") ?? "";
2521
2507
  const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
2522
- if (!returnedState || !expectedState || !(0, import_core8.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
2523
- return (0, import_core8.setApiResponse)(
2524
- import_core8.HTTP.BAD_REQUEST,
2508
+ if (!returnedState || !expectedState || !(0, import_core9.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
2509
+ return (0, import_core9.setApiResponse)(
2510
+ import_core9.HTTP.BAD_REQUEST,
2525
2511
  "GOOGLE_AUTH_FAILED",
2526
2512
  "OAuth state mismatch \u2014 restart the sign-in flow"
2527
2513
  );
@@ -2539,8 +2525,8 @@ function oauthController(store, config) {
2539
2525
  });
2540
2526
  const tokenData = await tokenRes.json();
2541
2527
  if (!tokenData.id_token) {
2542
- return (0, import_core8.setApiResponse)(
2543
- import_core8.HTTP.BAD_REQUEST,
2528
+ return (0, import_core9.setApiResponse)(
2529
+ import_core9.HTTP.BAD_REQUEST,
2544
2530
  "GOOGLE_AUTH_FAILED",
2545
2531
  "OAuth token exchange failed"
2546
2532
  );
@@ -2549,32 +2535,32 @@ function oauthController(store, config) {
2549
2535
  Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
2550
2536
  );
2551
2537
  if (payload.aud !== google.clientId) {
2552
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
2538
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
2553
2539
  }
2554
2540
  if (payload.iss !== "https://accounts.google.com" && payload.iss !== "accounts.google.com") {
2555
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
2541
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
2556
2542
  }
2557
2543
  if (typeof payload.exp !== "number" || payload.exp * 1e3 < Date.now()) {
2558
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
2544
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
2559
2545
  }
2560
2546
  if (!payload.email) {
2561
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
2547
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
2562
2548
  }
2563
2549
  if (payload.email_verified !== true) {
2564
- return (0, import_core8.setApiResponse)(
2565
- import_core8.HTTP.BAD_REQUEST,
2550
+ return (0, import_core9.setApiResponse)(
2551
+ import_core9.HTTP.BAD_REQUEST,
2566
2552
  "GOOGLE_AUTH_FAILED",
2567
2553
  "Google account email is not verified"
2568
2554
  );
2569
2555
  }
2570
2556
  const normalizedEmail = normalizeEmailSafe(payload.email);
2571
2557
  if (!normalizedEmail) {
2572
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "Invalid email in OAuth response");
2558
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "Invalid email in OAuth response");
2573
2559
  }
2574
2560
  const meta = requestMeta(ctx);
2575
2561
  const upserted = await users.upsertByProvider(normalizedEmail, "google", payload.sub ?? "");
2576
2562
  if (!upserted) {
2577
- loginEvents.recordSafe({
2563
+ await loginEvents.recordSafe({
2578
2564
  userId: null,
2579
2565
  emailAttempted: normalizedEmail,
2580
2566
  method: "oauth-google",
@@ -2582,17 +2568,17 @@ function oauthController(store, config) {
2582
2568
  failureReason: "provider_upsert_failed",
2583
2569
  ...meta
2584
2570
  });
2585
- return (0, import_core8.setApiResponse)(import_core8.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
2571
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
2586
2572
  }
2587
2573
  const fullUser = await users.findById(upserted.id);
2588
2574
  if (!fullUser) {
2589
- return (0, import_core8.setApiResponse)(import_core8.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
2575
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
2590
2576
  }
2591
2577
  const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {
2592
2578
  loginMethod: "google"
2593
2579
  });
2594
2580
  await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
2595
- loginEvents.recordSafe({
2581
+ await loginEvents.recordSafe({
2596
2582
  userId: upserted.id,
2597
2583
  emailAttempted: normalizedEmail,
2598
2584
  method: "oauth-google",
@@ -2621,7 +2607,7 @@ function oauthController(store, config) {
2621
2607
  appleInit: async (_ctx) => {
2622
2608
  const apple = config.apple;
2623
2609
  if (!apple) {
2624
- return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
2610
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
2625
2611
  }
2626
2612
  const state = (0, import_node_crypto7.randomBytes)(16).toString("hex");
2627
2613
  const params = new URLSearchParams({
@@ -2652,20 +2638,20 @@ function oauthController(store, config) {
2652
2638
  appleCallback: async (ctx) => {
2653
2639
  const apple = config.apple;
2654
2640
  if (!apple) {
2655
- return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
2641
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
2656
2642
  }
2657
2643
  const pre = ctx.meta["body"];
2658
2644
  const form = pre && typeof pre === "object" ? new URLSearchParams(pre) : new URLSearchParams(await ctx.request.text());
2659
2645
  const code = form.get("code");
2660
2646
  if (!code) {
2661
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
2647
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
2662
2648
  }
2663
2649
  const returnedState = form.get("state") ?? "";
2664
2650
  const cookieHeader = ctx.request.headers.get("cookie") ?? "";
2665
2651
  const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
2666
- if (!returnedState || !expectedState || !(0, import_core8.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
2667
- return (0, import_core8.setApiResponse)(
2668
- import_core8.HTTP.BAD_REQUEST,
2652
+ if (!returnedState || !expectedState || !(0, import_core9.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
2653
+ return (0, import_core9.setApiResponse)(
2654
+ import_core9.HTTP.BAD_REQUEST,
2669
2655
  "APPLE_AUTH_FAILED",
2670
2656
  "OAuth state mismatch \u2014 restart the sign-in flow"
2671
2657
  );
@@ -2683,11 +2669,11 @@ function oauthController(store, config) {
2683
2669
  });
2684
2670
  const tokenData = await tokenRes.json();
2685
2671
  if (!tokenData.id_token) {
2686
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
2672
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
2687
2673
  }
2688
2674
  const claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });
2689
2675
  if (!claims) {
2690
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
2676
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
2691
2677
  }
2692
2678
  return completeAppleLogin(ctx, {
2693
2679
  email: claims.email,
@@ -2698,18 +2684,18 @@ function oauthController(store, config) {
2698
2684
  appleNative: async (ctx) => {
2699
2685
  const apple = config.apple;
2700
2686
  if (!apple) {
2701
- return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
2687
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
2702
2688
  }
2703
2689
  const body = ctx.meta["body"];
2704
2690
  const identityToken = body?.identityToken;
2705
2691
  if (!identityToken) {
2706
- return (0, import_core8.setApiResponse)(import_core8.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
2692
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
2707
2693
  }
2708
2694
  const audiences = apple.nativeClientIds && apple.nativeClientIds.length ? apple.nativeClientIds : [apple.clientId];
2709
2695
  const claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });
2710
2696
  if (!claims) {
2711
2697
  const meta = requestMeta(ctx);
2712
- loginEvents.recordSafe({
2698
+ await loginEvents.recordSafe({
2713
2699
  userId: null,
2714
2700
  emailAttempted: null,
2715
2701
  method: "oauth-apple",
@@ -2717,14 +2703,14 @@ function oauthController(store, config) {
2717
2703
  failureReason: "invalid_identity_token",
2718
2704
  ...meta
2719
2705
  });
2720
- return (0, import_core8.setApiResponse)(import_core8.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
2706
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
2721
2707
  }
2722
2708
  const tokenHash = (0, import_node_crypto7.createHash)("sha256").update(identityToken).digest("hex");
2723
2709
  const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
2724
2710
  const firstUse = await consumedTokens.consumeOnce(tokenHash, new Date(expMs));
2725
2711
  if (!firstUse) {
2726
2712
  const meta = requestMeta(ctx);
2727
- loginEvents.recordSafe({
2713
+ await loginEvents.recordSafe({
2728
2714
  userId: null,
2729
2715
  emailAttempted: claims.email ?? null,
2730
2716
  method: "oauth-apple",
@@ -2732,8 +2718,8 @@ function oauthController(store, config) {
2732
2718
  failureReason: "token_replayed",
2733
2719
  ...meta
2734
2720
  });
2735
- return (0, import_core8.setApiResponse)(
2736
- import_core8.HTTP.UNAUTHORIZED,
2721
+ return (0, import_core9.setApiResponse)(
2722
+ import_core9.HTTP.UNAUTHORIZED,
2737
2723
  "APPLE_AUTH_FAILED",
2738
2724
  "This Apple identity token has already been used"
2739
2725
  );
@@ -2831,17 +2817,17 @@ function buildAuthRoutes(store, config, bus) {
2831
2817
  }
2832
2818
 
2833
2819
  // src/services/config-guard.ts
2834
- var import_core9 = require("@fonderie/core");
2820
+ var import_core10 = require("@fonderie/core");
2835
2821
  var MODULE = "@fonderie/auth";
2836
2822
  function collectAuthConfigProblems(config) {
2837
2823
  const problems = [];
2838
2824
  const secret = config.jwtSecret ?? "";
2839
- const secretProblem = (0, import_core9.secretStrengthProblem)(secret);
2825
+ const secretProblem = (0, import_core10.secretStrengthProblem)(secret);
2840
2826
  if (secretProblem === "too-short") {
2841
2827
  problems.push({
2842
2828
  module: MODULE,
2843
2829
  severity: "error",
2844
- message: `jwtSecret must be at least ${import_core9.MIN_SECRET_LENGTH} characters (got ${secret.length})`
2830
+ message: `jwtSecret must be at least ${import_core10.MIN_SECRET_LENGTH} characters (got ${secret.length})`
2845
2831
  });
2846
2832
  } else if (secretProblem === "placeholder") {
2847
2833
  problems.push({
@@ -2865,7 +2851,7 @@ function collectAuthConfigProblems(config) {
2865
2851
  severity: "error",
2866
2852
  message: "google OAuth is configured but clientId, clientSecret, or redirectUri is missing"
2867
2853
  });
2868
- } else if (import_core9.PLACEHOLDER_SECRET.test(clientSecret)) {
2854
+ } else if (import_core10.PLACEHOLDER_SECRET.test(clientSecret)) {
2869
2855
  problems.push({
2870
2856
  module: MODULE,
2871
2857
  severity: "error",