@fonderie/auth 7.22.1 → 7.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -100,7 +100,7 @@ __export(index_exports, {
100
100
  module.exports = __toCommonJS(index_exports);
101
101
 
102
102
  // src/routes.ts
103
- var import_core10 = require("@fonderie/core");
103
+ var import_core11 = require("@fonderie/core");
104
104
  var import_middlewares2 = require("@fonderie/core/middlewares");
105
105
 
106
106
  // src/middlewares/require-email-login.ts
@@ -389,6 +389,10 @@ var EVENT_KEYS = {
389
389
  // 'password-changed', 'admin', 'refresh-reuse' (theft signal).
390
390
  sessionRevoked: "fonderie.session.revoked"
391
391
  };
392
+ var deletionGracePeriodDays = (config) => {
393
+ const days = config.accountDeletion?.gracePeriodDays;
394
+ return typeof days === "number" && Number.isFinite(days) && days >= 0 ? days : 30;
395
+ };
392
396
 
393
397
  // src/services/jwt.ts
394
398
  var import_node_crypto = require("crypto");
@@ -805,6 +809,27 @@ var UserModel = class {
805
809
  );
806
810
  return row ?? null;
807
811
  }
812
+ /**
813
+ * An ARCHIVED (soft-deleted, not yet purged) account holding this address.
814
+ * Only for telling its owner — after they prove it is theirs — that it is
815
+ * scheduled for deletion, and for refusing to create a second account on
816
+ * the address meanwhile. Never for signing in.
817
+ */
818
+ async findArchivedByEmail(email2) {
819
+ const [row] = await this.store.query(
820
+ `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NOT NULL`,
821
+ [email2]
822
+ );
823
+ return row ?? null;
824
+ }
825
+ /** As findArchivedByEmail, for a phone number. */
826
+ async findArchivedByPhone(phone2) {
827
+ const [row] = await this.store.query(
828
+ `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NOT NULL`,
829
+ [phone2]
830
+ );
831
+ return row ?? null;
832
+ }
808
833
  async findByPhone(phone2) {
809
834
  const [row] = await this.store.query(
810
835
  `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
@@ -1659,7 +1684,7 @@ function mfaController(store, config, issuer, bus) {
1659
1684
  // src/controllers/auth.controller.ts
1660
1685
  var import_node_crypto6 = require("crypto");
1661
1686
  var import_events2 = require("@fonderie/events");
1662
- var import_core6 = require("@fonderie/core");
1687
+ var import_core7 = require("@fonderie/core");
1663
1688
 
1664
1689
  // src/services/cooldown.ts
1665
1690
  function checkCooldown(lastSentAt, cooldownMs) {
@@ -1670,6 +1695,28 @@ function checkCooldown(lastSentAt, cooldownMs) {
1670
1695
  // src/controllers/auth.controller.ts
1671
1696
  init_password();
1672
1697
 
1698
+ // src/services/pending-deletion.ts
1699
+ var import_core6 = require("@fonderie/core");
1700
+ function deletionDate(deletedAt, config) {
1701
+ return new Date(deletedAt.getTime() + deletionGracePeriodDays(config) * 24 * 60 * 60 * 1e3);
1702
+ }
1703
+ function pendingDeletionResponse(deletedAt, config) {
1704
+ const deleteOn = deletionDate(deletedAt, config);
1705
+ return (0, import_core6.setApiResponse)(
1706
+ import_core6.HTTP.FORBIDDEN,
1707
+ "ACCOUNT_PENDING_DELETION",
1708
+ `This account is scheduled for deletion on ${deleteOn.toISOString().slice(0, 10)}.`,
1709
+ { requestedAt: deletedAt.toISOString(), deleteOn: deleteOn.toISOString() }
1710
+ );
1711
+ }
1712
+ function archivedAddressResponse() {
1713
+ return (0, import_core6.setApiResponse)(
1714
+ import_core6.HTTP.CONFLICT,
1715
+ "ACCOUNT_PENDING_DELETION",
1716
+ "An account using this address is scheduled for deletion. Sign in to keep it, or wait until it is deleted."
1717
+ );
1718
+ }
1719
+
1673
1720
  // src/models/password-reset.model.ts
1674
1721
  var import_node_crypto5 = require("crypto");
1675
1722
  var hashSecret = (value) => (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
@@ -1826,7 +1873,7 @@ function extractRefreshToken(ctx) {
1826
1873
  const match = cookie.match(/(?:^|;\s*)refresh_token=([^;]+)/);
1827
1874
  return match?.[1] ?? null;
1828
1875
  }
1829
- function authController(store, config, bus, locales = (0, import_core6.defineLocales)()) {
1876
+ function authController(store, config, bus, locales = (0, import_core7.defineLocales)()) {
1830
1877
  const users = new UserModel(store);
1831
1878
  const sessions = new SessionModel(store, config.location);
1832
1879
  const loginEvents = new LoginEventModel(store, config.location);
@@ -1838,23 +1885,24 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
1838
1885
  register: async (ctx) => {
1839
1886
  const body = ctx.meta["body"];
1840
1887
  const { email: email2, password: password2, phone: phone2, firstName = null, lastName = null } = body ?? {};
1841
- const locale = (0, import_core6.canonicalLocale)(body?.["locale"]) ?? locales.default;
1888
+ const locale = (0, import_core7.canonicalLocale)(body?.["locale"]) ?? locales.default;
1842
1889
  if (typeof email2 === "string" && typeof password2 === "string") {
1843
1890
  const normalizedEmail = normalizeEmailSafe(email2);
1844
1891
  if (!normalizedEmail) {
1845
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1892
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1846
1893
  }
1847
1894
  if (password2.length < 8) {
1848
- return (0, import_core6.setApiResponse)(
1849
- import_core6.HTTP.UNPROCESSABLE,
1895
+ return (0, import_core7.setApiResponse)(
1896
+ import_core7.HTTP.UNPROCESSABLE,
1850
1897
  "INVALID_PARAMETER",
1851
1898
  "password must be at least 8 characters"
1852
1899
  );
1853
1900
  }
1854
1901
  const existing = await users.findByEmail(normalizedEmail);
1855
1902
  if (existing) {
1856
- return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
1903
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
1857
1904
  }
1905
+ if (await users.findArchivedByEmail(normalizedEmail)) return archivedAddressResponse();
1858
1906
  const passwordHash = await hashPassword(password2);
1859
1907
  const row = await users.create(
1860
1908
  normalizedEmail,
@@ -1864,18 +1912,18 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
1864
1912
  locale
1865
1913
  );
1866
1914
  if (!row) {
1867
- return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1915
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1868
1916
  }
1869
1917
  const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
1870
1918
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
1871
1919
  await emailVerif.create(row.id, pin, expiresAt);
1872
1920
  const user = await users.findById(row.id);
1873
1921
  if (!user) {
1874
- return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1922
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1875
1923
  }
1876
1924
  const reqId = ctx.meta["requestId"];
1877
1925
  const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
1878
- await (0, import_core6.background)(bus?.emit(
1926
+ await (0, import_core7.background)(bus?.emit(
1879
1927
  import_events2.NOTIFICATION_EVENT,
1880
1928
  {
1881
1929
  type: MESSAGE_KEYS.emailRegistration,
@@ -1885,7 +1933,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
1885
1933
  },
1886
1934
  reqOpts
1887
1935
  ));
1888
- await (0, import_core6.background)(bus?.emit(
1936
+ await (0, import_core7.background)(bus?.emit(
1889
1937
  EVENT_KEYS.userRegistered,
1890
1938
  {
1891
1939
  userId: user.id,
@@ -1929,8 +1977,9 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
1929
1977
  if (isValidPhone(phone2)) {
1930
1978
  const existing = await users.findByPhone(normalizePhone(phone2));
1931
1979
  if (existing) {
1932
- return (0, import_core6.setApiResponse)(import_core6.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
1980
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
1933
1981
  }
1982
+ if (await users.findArchivedByPhone(normalizePhone(phone2))) return archivedAddressResponse();
1934
1983
  const { id } = await users.findOrCreateByPhone(
1935
1984
  normalizePhone(phone2),
1936
1985
  firstName ?? null,
@@ -1942,11 +1991,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
1942
1991
  await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
1943
1992
  const user = await users.findById(id);
1944
1993
  if (!user) {
1945
- return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1994
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1946
1995
  }
1947
1996
  const reqId2 = ctx.meta["requestId"];
1948
1997
  const reqOpts2 = reqId2 !== void 0 ? { requestId: reqId2 } : void 0;
1949
- await (0, import_core6.background)(bus?.emit(
1998
+ await (0, import_core7.background)(bus?.emit(
1950
1999
  import_events2.NOTIFICATION_EVENT,
1951
2000
  {
1952
2001
  type: MESSAGE_KEYS.phoneOtp,
@@ -1956,7 +2005,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
1956
2005
  },
1957
2006
  reqOpts2
1958
2007
  ));
1959
- await (0, import_core6.background)(bus?.emit(
2008
+ await (0, import_core7.background)(bus?.emit(
1960
2009
  EVENT_KEYS.userRegistered,
1961
2010
  {
1962
2011
  userId: user.id,
@@ -1980,8 +2029,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
1980
2029
  { status: 202 }
1981
2030
  );
1982
2031
  }
1983
- return (0, import_core6.setApiResponse)(
1984
- import_core6.HTTP.UNPROCESSABLE,
2032
+ return (0, import_core7.setApiResponse)(
2033
+ import_core7.HTTP.UNPROCESSABLE,
1985
2034
  "INVALID_PARAMETER",
1986
2035
  "Provide email + password or a valid phone number"
1987
2036
  );
@@ -1993,9 +2042,25 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
1993
2042
  const { email: rawEmail, password: password2 } = body;
1994
2043
  const email2 = normalizeEmailSafe(rawEmail);
1995
2044
  if (!email2) {
1996
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
2045
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1997
2046
  }
1998
2047
  const user = await users.findByEmail(email2);
2048
+ if (!user) {
2049
+ const archived = await users.findArchivedByEmail(email2);
2050
+ if (archived?.passwordHash && archived.deletedAt) {
2051
+ const { valid: valid2 } = await verifyPasswordForLogin(password2, archived.passwordHash, config.legacyVerify);
2052
+ await loginEvents.recordSafe({
2053
+ userId: archived.id,
2054
+ emailAttempted: email2,
2055
+ method: "password",
2056
+ outcome: "failed",
2057
+ failureReason: valid2 ? "pending_deletion" : "bad_password",
2058
+ ...meta
2059
+ });
2060
+ if (valid2) return pendingDeletionResponse(archived.deletedAt, config);
2061
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2062
+ }
2063
+ }
1999
2064
  if (!user || !user.passwordHash) {
2000
2065
  await loginEvents.recordSafe({
2001
2066
  userId: null,
@@ -2005,7 +2070,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2005
2070
  failureReason: "unknown_email",
2006
2071
  ...meta
2007
2072
  });
2008
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2073
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2009
2074
  }
2010
2075
  const { valid, needsRehash } = await verifyPasswordForLogin(
2011
2076
  password2,
@@ -2021,21 +2086,21 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2021
2086
  failureReason: "bad_password",
2022
2087
  ...meta
2023
2088
  });
2024
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2089
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2025
2090
  }
2026
2091
  if (needsRehash) {
2027
2092
  await users.updatePassword(user.id, await hashPassword(password2));
2028
2093
  }
2029
2094
  if (user.suspended) {
2030
- return (0, import_core6.setApiResponse)(
2031
- import_core6.HTTP.FORBIDDEN,
2095
+ return (0, import_core7.setApiResponse)(
2096
+ import_core7.HTTP.FORBIDDEN,
2032
2097
  "ACCOUNT_SUSPENDED",
2033
2098
  "Account suspended. Please contact support."
2034
2099
  );
2035
2100
  }
2036
2101
  if (user.mfaEnabled) {
2037
2102
  const mfaToken = issueMfaPendingToken(user.id, config, "email");
2038
- return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
2103
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
2039
2104
  mfaToken
2040
2105
  });
2041
2106
  }
@@ -2072,11 +2137,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2072
2137
  if (isValidPhone(phone2)) {
2073
2138
  const user = await users.findByPhone(normalizePhone(phone2));
2074
2139
  if (!user) {
2075
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2140
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2076
2141
  }
2077
2142
  if (user.suspended) {
2078
- return (0, import_core6.setApiResponse)(
2079
- import_core6.HTTP.FORBIDDEN,
2143
+ return (0, import_core7.setApiResponse)(
2144
+ import_core7.HTTP.FORBIDDEN,
2080
2145
  "ACCOUNT_SUSPENDED",
2081
2146
  "Account suspended. Please contact support."
2082
2147
  );
@@ -2084,7 +2149,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2084
2149
  const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
2085
2150
  const expiresAt = new Date(Date.now() + OTP_TTL_MS);
2086
2151
  await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
2087
- await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
2152
+ await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
2088
2153
  type: MESSAGE_KEYS.phoneOtp,
2089
2154
  locale: user.locale,
2090
2155
  data: { otp },
@@ -2100,8 +2165,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2100
2165
  { status: 202 }
2101
2166
  );
2102
2167
  }
2103
- return (0, import_core6.setApiResponse)(
2104
- import_core6.HTTP.UNPROCESSABLE,
2168
+ return (0, import_core7.setApiResponse)(
2169
+ import_core7.HTTP.UNPROCESSABLE,
2105
2170
  "INVALID_PARAMETER",
2106
2171
  "Provide email + password or a valid phone number"
2107
2172
  );
@@ -2126,35 +2191,35 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2126
2191
  refresh: async (ctx) => {
2127
2192
  const token = extractRefreshToken(ctx);
2128
2193
  if (!token) {
2129
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
2194
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
2130
2195
  }
2131
2196
  const payload = verifyToken(token, config);
2132
2197
  if (!payload || payload.type !== "refresh") {
2133
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
2198
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
2134
2199
  }
2135
2200
  const runtime = config.resolve?.(ctx);
2136
2201
  let issued = null;
2137
2202
  for (let attempt = 0; attempt < 3 && !issued; attempt++) {
2138
2203
  const found = await sessions.match(token);
2139
2204
  if (found.kind === "unknown") {
2140
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2205
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2141
2206
  }
2142
2207
  if (found.kind === "reused") {
2143
2208
  await sessions.revokeById(found.row.id);
2144
- await (0, import_core6.background)(
2209
+ await (0, import_core7.background)(
2145
2210
  bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sids: [found.row.sid], reason: "refresh-reuse" })
2146
2211
  );
2147
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2212
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2148
2213
  }
2149
2214
  const resolvedRefresh = configForClient(config, runtime, found.row.clientKind);
2150
2215
  const maxAgeMs = resolvedRefresh.sessionMaxAge ? durationMs(String(resolvedRefresh.sessionMaxAge)) : null;
2151
2216
  if (maxAgeMs !== null && Date.now() - found.row.createdAt.getTime() > maxAgeMs) {
2152
2217
  await sessions.revokeById(found.row.id);
2153
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
2218
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
2154
2219
  }
2155
2220
  const user = await users.findById(found.row.userId);
2156
2221
  if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
2157
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
2222
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
2158
2223
  }
2159
2224
  const pair = issueTokenPair(user.id, resolvedRefresh, {
2160
2225
  loginMethod: payload.loginMethod ?? "email",
@@ -2167,7 +2232,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2167
2232
  if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
2168
2233
  }
2169
2234
  if (!issued) {
2170
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2235
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2171
2236
  }
2172
2237
  const { accessToken, refreshToken } = issued;
2173
2238
  return Response.json(
@@ -2186,16 +2251,16 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2186
2251
  const body = ctx.meta["body"];
2187
2252
  const rawEmail = body?.["email"];
2188
2253
  if (typeof rawEmail !== "string") {
2189
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
2254
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
2190
2255
  }
2191
2256
  const email2 = normalizeEmailSafe(rawEmail);
2192
2257
  if (!email2) {
2193
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
2258
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
2194
2259
  }
2195
2260
  const user = await users.findByEmail(email2);
2196
2261
  if (!user) {
2197
- return (0, import_core6.setApiResponse)(
2198
- import_core6.HTTP.OK,
2262
+ return (0, import_core7.setApiResponse)(
2263
+ import_core7.HTTP.OK,
2199
2264
  "PASSWORD_RESET_EMAIL_SENT",
2200
2265
  "Password reset email sent (if account exists)."
2201
2266
  );
@@ -2204,8 +2269,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2204
2269
  const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
2205
2270
  const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
2206
2271
  if (remaining > 0) {
2207
- return (0, import_core6.setApiResponse)(
2208
- import_core6.HTTP.OK,
2272
+ return (0, import_core7.setApiResponse)(
2273
+ import_core7.HTTP.OK,
2209
2274
  "PASSWORD_RESET_EMAIL_SENT",
2210
2275
  "Password reset email sent (if account exists)."
2211
2276
  );
@@ -2216,14 +2281,14 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2216
2281
  await passwordReset.create(user.id, pin, token, expiresAt);
2217
2282
  const base = resolved.passwordResetUrl;
2218
2283
  const resetUrl = base ? `${base}${base.includes("?") ? "&" : "?"}token=${token}` : "";
2219
- await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
2284
+ await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
2220
2285
  type: MESSAGE_KEYS.passwordReset,
2221
2286
  locale: user.locale,
2222
2287
  recipient: { email: email2, phone: null, deviceToken: null },
2223
2288
  data: { pin, token, resetUrl }
2224
2289
  }));
2225
- return (0, import_core6.setApiResponse)(
2226
- import_core6.HTTP.OK,
2290
+ return (0, import_core7.setApiResponse)(
2291
+ import_core7.HTTP.OK,
2227
2292
  "PASSWORD_RESET_EMAIL_SENT",
2228
2293
  "Password reset email sent (if account exists)."
2229
2294
  );
@@ -2234,11 +2299,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2234
2299
  const password2 = body?.["password"];
2235
2300
  const rawToken = body?.["token"];
2236
2301
  if (typeof password2 !== "string") {
2237
- return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
2302
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
2238
2303
  }
2239
2304
  if (password2.length < 8) {
2240
- return (0, import_core6.setApiResponse)(
2241
- import_core6.HTTP.UNPROCESSABLE,
2305
+ return (0, import_core7.setApiResponse)(
2306
+ import_core7.HTTP.UNPROCESSABLE,
2242
2307
  "INVALID_PARAMETER",
2243
2308
  "password must be at least 8 characters"
2244
2309
  );
@@ -2248,8 +2313,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2248
2313
  row = await passwordReset.findByToken(rawToken.trim());
2249
2314
  } else {
2250
2315
  if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
2251
- return (0, import_core6.setApiResponse)(
2252
- import_core6.HTTP.UNPROCESSABLE,
2316
+ return (0, import_core7.setApiResponse)(
2317
+ import_core7.HTTP.UNPROCESSABLE,
2253
2318
  "INVALID_PARAMETER",
2254
2319
  "a 6-digit pin or a reset token is required"
2255
2320
  );
@@ -2257,33 +2322,41 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2257
2322
  row = await passwordReset.findByPin(raw.trim());
2258
2323
  }
2259
2324
  if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
2260
- return (0, import_core6.setApiResponse)(
2261
- import_core6.HTTP.BAD_REQUEST,
2325
+ return (0, import_core7.setApiResponse)(
2326
+ import_core7.HTTP.BAD_REQUEST,
2262
2327
  "PASSWORD_RESET_FAILED",
2263
2328
  "Invalid or expired reset credentials"
2264
2329
  );
2265
2330
  }
2266
2331
  const passwordHash = await hashPassword(password2);
2267
- await store.transaction(async (tx) => {
2268
- await Promise.all([
2269
- tx.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
2270
- passwordHash,
2271
- row.userId
2272
- ]),
2273
- tx.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [row.userId]),
2274
- // Revoke every session: the reset exists because the account may
2275
- // be compromised — a stolen session must not survive it.
2276
- tx.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [row.userId])
2277
- ]);
2278
- });
2279
- return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
2332
+ const [changed] = await store.query(
2333
+ `WITH changed AS (
2334
+ UPDATE fonderie_users SET password_hash = $1, updated_at = now()
2335
+ WHERE id = $2 AND deleted_at IS NULL
2336
+ RETURNING id
2337
+ ), spent AS (
2338
+ DELETE FROM fonderie_password_resets WHERE user_id = $2
2339
+ ), revoked AS (
2340
+ DELETE FROM fonderie_sessions WHERE user_id IN (SELECT id FROM changed)
2341
+ )
2342
+ SELECT id FROM changed`,
2343
+ [passwordHash, row.userId]
2344
+ );
2345
+ if (!changed) {
2346
+ return (0, import_core7.setApiResponse)(
2347
+ import_core7.HTTP.BAD_REQUEST,
2348
+ "PASSWORD_RESET_FAILED",
2349
+ "Invalid or expired reset credentials"
2350
+ );
2351
+ }
2352
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
2280
2353
  },
2281
2354
  verify: async (ctx) => {
2282
2355
  if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
2283
- return (0, import_core6.setApiResponse)(import_core6.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
2356
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
2284
2357
  }
2285
2358
  if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
2286
- return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFIED", "Email verified successfully.", {
2359
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "VERIFIED", "Email verified successfully.", {
2287
2360
  verified: true,
2288
2361
  email: ctx.user.email
2289
2362
  });
@@ -2291,8 +2364,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2291
2364
  const body = ctx.meta["body"];
2292
2365
  const raw = body?.["token"];
2293
2366
  if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
2294
- return (0, import_core6.setApiResponse)(
2295
- import_core6.HTTP.UNPROCESSABLE,
2367
+ return (0, import_core7.setApiResponse)(
2368
+ import_core7.HTTP.UNPROCESSABLE,
2296
2369
  "INVALID_PARAMETER",
2297
2370
  "token must be a 6-digit code"
2298
2371
  );
@@ -2310,7 +2383,7 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2310
2383
  failureReason: "invalid_pin",
2311
2384
  ...phoneMeta
2312
2385
  });
2313
- return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
2386
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
2314
2387
  }
2315
2388
  if (/* @__PURE__ */ new Date() > record.expiresAt) {
2316
2389
  await phoneVerif.deleteByUser(ctx.user.id);
@@ -2322,8 +2395,8 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2322
2395
  failureReason: "expired_pin",
2323
2396
  ...phoneMeta
2324
2397
  });
2325
- return (0, import_core6.setApiResponse)(
2326
- import_core6.HTTP.BAD_REQUEST,
2398
+ return (0, import_core7.setApiResponse)(
2399
+ import_core7.HTTP.BAD_REQUEST,
2327
2400
  "VERIFICATION_FAILED",
2328
2401
  "Verification code expired"
2329
2402
  );
@@ -2343,11 +2416,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2343
2416
  });
2344
2417
  const verifiedUser = await users.findById(ctx.user.id);
2345
2418
  if (!verifiedUser) {
2346
- return (0, import_core6.setApiResponse)(import_core6.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2419
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2347
2420
  }
2348
2421
  if (verifiedUser.suspended) {
2349
- return (0, import_core6.setApiResponse)(
2350
- import_core6.HTTP.FORBIDDEN,
2422
+ return (0, import_core7.setApiResponse)(
2423
+ import_core7.HTTP.FORBIDDEN,
2351
2424
  "ACCOUNT_SUSPENDED",
2352
2425
  "Account suspended. Please contact support."
2353
2426
  );
@@ -2369,10 +2442,10 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2369
2442
  }
2370
2443
  const row = await emailVerif.findByUser(ctx.user.id, pin);
2371
2444
  if (!row) {
2372
- return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
2445
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
2373
2446
  }
2374
2447
  if (/* @__PURE__ */ new Date() > row.expiresAt) {
2375
- return (0, import_core6.setApiResponse)(import_core6.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
2448
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
2376
2449
  }
2377
2450
  await store.transaction(async (tx) => {
2378
2451
  await Promise.all([
@@ -2386,30 +2459,30 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2386
2459
  ])
2387
2460
  ]);
2388
2461
  });
2389
- return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFIED", "Email verified successfully.", {
2462
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "VERIFIED", "Email verified successfully.", {
2390
2463
  verified: true,
2391
2464
  email: ctx.user.email
2392
2465
  });
2393
2466
  },
2394
2467
  sendVerification: async (ctx) => {
2395
2468
  if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
2396
- return (0, import_core6.setApiResponse)(import_core6.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
2469
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
2397
2470
  }
2398
2471
  const resolved = { ...config, ...config.resolve?.(ctx) };
2399
2472
  const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
2400
2473
  if (ctx.user.loginMethod === "phone") {
2401
2474
  const phone2 = ctx.user.phone;
2402
2475
  if (!phone2) {
2403
- return (0, import_core6.setApiResponse)(
2404
- import_core6.HTTP.BAD_REQUEST,
2476
+ return (0, import_core7.setApiResponse)(
2477
+ import_core7.HTTP.BAD_REQUEST,
2405
2478
  "NO_PHONE_ON_ACCOUNT",
2406
2479
  "No phone number associated with this account"
2407
2480
  );
2408
2481
  }
2409
2482
  const remaining2 = checkCooldown(await phoneVerif.findLastSentAt(ctx.user.id), cooldown);
2410
2483
  if (remaining2 > 0) {
2411
- return (0, import_core6.setApiResponse)(
2412
- import_core6.HTTP.TOO_MANY_REQUESTS,
2484
+ return (0, import_core7.setApiResponse)(
2485
+ import_core7.HTTP.TOO_MANY_REQUESTS,
2413
2486
  "VERIFICATION_COOLDOWN",
2414
2487
  "Please wait before requesting a new code.",
2415
2488
  {
@@ -2420,35 +2493,35 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2420
2493
  const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
2421
2494
  const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
2422
2495
  await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
2423
- await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
2496
+ await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
2424
2497
  type: MESSAGE_KEYS.phoneOtp,
2425
2498
  locale: ctx.user.locale,
2426
2499
  data: { otp },
2427
2500
  recipient: { email: null, phone: phone2, deviceToken: null }
2428
2501
  }));
2429
- return (0, import_core6.setApiResponse)(
2430
- import_core6.HTTP.OK,
2502
+ return (0, import_core7.setApiResponse)(
2503
+ import_core7.HTTP.OK,
2431
2504
  "VERIFICATION_SENT",
2432
2505
  "A verification code has been sent to your phone."
2433
2506
  );
2434
2507
  }
2435
2508
  if (!ctx.user.email) {
2436
- return (0, import_core6.setApiResponse)(
2437
- import_core6.HTTP.BAD_REQUEST,
2509
+ return (0, import_core7.setApiResponse)(
2510
+ import_core7.HTTP.BAD_REQUEST,
2438
2511
  "NO_EMAIL_ON_ACCOUNT",
2439
2512
  "No email address associated with this account"
2440
2513
  );
2441
2514
  }
2442
2515
  if (ctx.user.emailVerifiedAt) {
2443
- return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "EMAIL_VERIFIED", "Email already verified.", {
2516
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "EMAIL_VERIFIED", "Email already verified.", {
2444
2517
  verified: true,
2445
2518
  email: ctx.user.email
2446
2519
  });
2447
2520
  }
2448
2521
  const remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user.id), cooldown);
2449
2522
  if (remaining > 0) {
2450
- return (0, import_core6.setApiResponse)(
2451
- import_core6.HTTP.TOO_MANY_REQUESTS,
2523
+ return (0, import_core7.setApiResponse)(
2524
+ import_core7.HTTP.TOO_MANY_REQUESTS,
2452
2525
  "VERIFICATION_COOLDOWN",
2453
2526
  "Please wait before requesting a new code.",
2454
2527
  {
@@ -2459,13 +2532,13 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2459
2532
  const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
2460
2533
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
2461
2534
  await emailVerif.replace(ctx.user.id, pin, expiresAt);
2462
- await (0, import_core6.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
2535
+ await (0, import_core7.background)(bus?.emit(import_events2.NOTIFICATION_EVENT, {
2463
2536
  type: MESSAGE_KEYS.emailVerification,
2464
2537
  locale: ctx.user.locale,
2465
2538
  recipient: { email: ctx.user.email, phone: null, deviceToken: null },
2466
2539
  data: { pin }
2467
2540
  }));
2468
- return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
2541
+ return (0, import_core7.setApiResponse)(import_core7.HTTP.OK, "VERIFICATION_SENT", "Verification email sent.", {
2469
2542
  email: ctx.user.email
2470
2543
  });
2471
2544
  }
@@ -2474,11 +2547,11 @@ function authController(store, config, bus, locales = (0, import_core6.defineLoc
2474
2547
 
2475
2548
  // src/controllers/user.controller.ts
2476
2549
  var import_node_crypto7 = require("crypto");
2477
- var import_core8 = require("@fonderie/core");
2550
+ var import_core9 = require("@fonderie/core");
2478
2551
  var import_events3 = require("@fonderie/events");
2479
2552
 
2480
2553
  // src/dtos/login-activity.ts
2481
- var import_core7 = require("@fonderie/core");
2554
+ var import_core8 = require("@fonderie/core");
2482
2555
  function toLoginEventDTO(row) {
2483
2556
  return {
2484
2557
  id: row.id,
@@ -2495,10 +2568,10 @@ function toLoginEventDTO(row) {
2495
2568
  }
2496
2569
  function toLoginHistoryPageDTO(page) {
2497
2570
  const last = page.events[page.events.length - 1];
2498
- const nextCursor = page.hasMore && last ? (0, import_core7.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
2571
+ const nextCursor = page.hasMore && last ? (0, import_core8.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
2499
2572
  return { events: page.events.map(toLoginEventDTO), nextCursor };
2500
2573
  }
2501
- var decodeLoginCursor = import_core7.decodeKeysetCursor;
2574
+ var decodeLoginCursor = import_core8.decodeKeysetCursor;
2502
2575
  function toSessionDTO(row, currentSid) {
2503
2576
  return {
2504
2577
  id: row.id,
@@ -2529,9 +2602,9 @@ function userController(store, config, bus) {
2529
2602
  me: async (ctx) => {
2530
2603
  const user = await users.findById(ctx.user.id);
2531
2604
  if (!user) {
2532
- return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2605
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2533
2606
  }
2534
- return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
2607
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
2535
2608
  user: toUserDTO(user, ctx.user.phoneVerified)
2536
2609
  });
2537
2610
  },
@@ -2542,14 +2615,14 @@ function userController(store, config, bus) {
2542
2615
  const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
2543
2616
  const outcomeParam = params.get("outcome");
2544
2617
  if (outcomeParam !== null && outcomeParam !== "success" && outcomeParam !== "failed") {
2545
- return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
2618
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
2546
2619
  }
2547
2620
  const cursorParam = params.get("cursor");
2548
2621
  let cursor = null;
2549
2622
  if (cursorParam) {
2550
2623
  cursor = decodeLoginCursor(cursorParam);
2551
2624
  if (!cursor) {
2552
- return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
2625
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
2553
2626
  }
2554
2627
  }
2555
2628
  const query = { userId: ctx.user.id, limit };
@@ -2560,8 +2633,8 @@ function userController(store, config, bus) {
2560
2633
  if (from) query.from = new Date(from);
2561
2634
  if (to) query.to = new Date(to);
2562
2635
  const page = await loginEvents.listByUser(query);
2563
- return (0, import_core8.setApiResponse)(
2564
- import_core8.HTTP.OK,
2636
+ return (0, import_core9.setApiResponse)(
2637
+ import_core9.HTTP.OK,
2565
2638
  "LOGIN_HISTORY_FETCHED",
2566
2639
  "Login history retrieved.",
2567
2640
  toLoginHistoryPageDTO(page)
@@ -2571,35 +2644,35 @@ function userController(store, config, bus) {
2571
2644
  listSessions: async (ctx) => {
2572
2645
  const rows = await sessions.listLiveByUser(ctx.user.id);
2573
2646
  const currentSid = ctx.user.sid ?? null;
2574
- return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
2647
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
2575
2648
  sessions: rows.map((r) => toSessionDTO(r, currentSid))
2576
2649
  });
2577
2650
  },
2578
2651
  terminateSession: async (ctx) => {
2579
2652
  const id = ctx.meta["params"]?.["id"];
2580
2653
  if (!id) {
2581
- return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
2654
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
2582
2655
  }
2583
2656
  const removed = await sessions.terminateByIdReturningSid(ctx.user.id, id);
2584
2657
  if (removed === void 0) {
2585
- return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
2658
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "Session not found");
2586
2659
  }
2587
- await (0, import_core8.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: [removed], reason: "terminated" }));
2588
- return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSION_TERMINATED", "Session terminated.", { id });
2660
+ await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: [removed], reason: "terminated" }));
2661
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "SESSION_TERMINATED", "Session terminated.", { id });
2589
2662
  },
2590
2663
  terminateOtherSessions: async (ctx) => {
2591
2664
  const currentSid = ctx.user.sid ?? null;
2592
2665
  if (!currentSid) {
2593
- return (0, import_core8.setApiResponse)(
2594
- import_core8.HTTP.UNPROCESSABLE,
2666
+ return (0, import_core9.setApiResponse)(
2667
+ import_core9.HTTP.UNPROCESSABLE,
2595
2668
  "NO_CURRENT_SESSION",
2596
2669
  "Cannot terminate others without a current session context"
2597
2670
  );
2598
2671
  }
2599
2672
  const sids = await sessions.terminateOthersReturningSids(ctx.user.id, currentSid);
2600
2673
  const count = sids.length;
2601
- if (count) await (0, import_core8.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids, reason: "terminated" }));
2602
- return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
2674
+ if (count) await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids, reason: "terminated" }));
2675
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
2603
2676
  },
2604
2677
  updateProfile: async (ctx) => {
2605
2678
  const body = ctx.meta["body"];
@@ -2609,18 +2682,18 @@ function userController(store, config, bus) {
2609
2682
  if (body?.[key] !== void 0) fields[key] = body[key];
2610
2683
  }
2611
2684
  if (Object.keys(fields).length === 0) {
2612
- return (0, import_core8.setApiResponse)(
2613
- import_core8.HTTP.UNPROCESSABLE,
2685
+ return (0, import_core9.setApiResponse)(
2686
+ import_core9.HTTP.UNPROCESSABLE,
2614
2687
  "INVALID_PARAMETER",
2615
2688
  "Provide at least one of: firstName, lastName, avatarUrl"
2616
2689
  );
2617
2690
  }
2618
2691
  const row = await users.update(ctx.user.id, fields);
2619
2692
  if (!row) {
2620
- return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2693
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2621
2694
  }
2622
2695
  const updated = await users.findById(ctx.user.id);
2623
- return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PROFILE_UPDATED", "Profile updated.", {
2696
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "PROFILE_UPDATED", "Profile updated.", {
2624
2697
  user: toUserDTO(updated, ctx.user.phoneVerified)
2625
2698
  });
2626
2699
  },
@@ -2637,14 +2710,14 @@ function userController(store, config, bus) {
2637
2710
  if (Object.keys(patch).length > 0) fields.patch = patch;
2638
2711
  const row = await users.updatePreferences(ctx.user.id, fields);
2639
2712
  if (!row) {
2640
- return (0, import_core8.setApiResponse)(
2641
- import_core8.HTTP.UNPROCESSABLE,
2713
+ return (0, import_core9.setApiResponse)(
2714
+ import_core9.HTTP.UNPROCESSABLE,
2642
2715
  "INVALID_PARAMETER",
2643
2716
  "Provide at least one preference field"
2644
2717
  );
2645
2718
  }
2646
2719
  const updated = await users.findById(ctx.user.id);
2647
- return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
2720
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
2648
2721
  user: toUserDTO(updated, ctx.user.phoneVerified)
2649
2722
  });
2650
2723
  },
@@ -2653,44 +2726,44 @@ function userController(store, config, bus) {
2653
2726
  const newEmail = body?.["email"];
2654
2727
  const normalised = typeof newEmail === "string" ? normalizeEmailSafe(newEmail) : null;
2655
2728
  if (!normalised) {
2656
- return (0, import_core8.setApiResponse)(
2657
- import_core8.HTTP.UNPROCESSABLE,
2729
+ return (0, import_core9.setApiResponse)(
2730
+ import_core9.HTTP.UNPROCESSABLE,
2658
2731
  "INVALID_PARAMETER",
2659
2732
  "A valid email address is required"
2660
2733
  );
2661
2734
  }
2662
2735
  const oldEmail = ctx.user.email;
2663
2736
  if (normalised === oldEmail) {
2664
- return (0, import_core8.setApiResponse)(
2665
- import_core8.HTTP.UNPROCESSABLE,
2737
+ return (0, import_core9.setApiResponse)(
2738
+ import_core9.HTTP.UNPROCESSABLE,
2666
2739
  "INVALID_PARAMETER",
2667
2740
  "New email must differ from current email"
2668
2741
  );
2669
2742
  }
2670
2743
  const existing = await users.findByEmail(normalised);
2671
2744
  if (existing) {
2672
- return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
2745
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
2673
2746
  }
2674
2747
  const pin = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
2675
2748
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
2676
2749
  await emailVerif.replace(ctx.user.id, pin, expiresAt);
2677
2750
  await users.updateEmail(ctx.user.id, normalised);
2678
- await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2751
+ await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2679
2752
  type: MESSAGE_KEYS.emailVerification,
2680
2753
  locale: ctx.user.locale,
2681
2754
  data: { pin },
2682
2755
  recipient: { email: normalised, phone: null, deviceToken: null }
2683
2756
  }));
2684
2757
  if (oldEmail) {
2685
- await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2758
+ await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2686
2759
  type: MESSAGE_KEYS.emailChanged,
2687
2760
  locale: ctx.user.locale,
2688
2761
  data: { newEmail: normalised },
2689
2762
  recipient: { email: oldEmail, phone: null, deviceToken: null }
2690
2763
  }));
2691
2764
  }
2692
- return (0, import_core8.setApiResponse)(
2693
- import_core8.HTTP.OK,
2765
+ return (0, import_core9.setApiResponse)(
2766
+ import_core9.HTTP.OK,
2694
2767
  "EMAIL_UPDATED",
2695
2768
  "Email updated. A verification code has been sent to your new address.",
2696
2769
  {
@@ -2702,8 +2775,8 @@ function userController(store, config, bus) {
2702
2775
  const body = ctx.meta["body"];
2703
2776
  const newPhone = body?.["phone"];
2704
2777
  if (!isValidPhone2(newPhone)) {
2705
- return (0, import_core8.setApiResponse)(
2706
- import_core8.HTTP.UNPROCESSABLE,
2778
+ return (0, import_core9.setApiResponse)(
2779
+ import_core9.HTTP.UNPROCESSABLE,
2707
2780
  "INVALID_PARAMETER",
2708
2781
  "A valid phone number is required"
2709
2782
  );
@@ -2711,28 +2784,28 @@ function userController(store, config, bus) {
2711
2784
  const normalised = normalizePhone2(newPhone);
2712
2785
  const existing = await users.findByPhone(normalised);
2713
2786
  if (existing) {
2714
- return (0, import_core8.setApiResponse)(import_core8.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
2787
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
2715
2788
  }
2716
2789
  const otp = (0, import_node_crypto7.randomInt)(1e5, 1e6).toString();
2717
2790
  const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
2718
2791
  await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
2719
2792
  await users.updatePhone(ctx.user.id, normalised);
2720
- await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2793
+ await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2721
2794
  type: MESSAGE_KEYS.phoneOtp,
2722
2795
  locale: ctx.user.locale,
2723
2796
  data: { otp },
2724
2797
  recipient: { email: null, phone: normalised, deviceToken: null }
2725
2798
  }));
2726
2799
  if (ctx.user.email) {
2727
- await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2800
+ await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2728
2801
  type: MESSAGE_KEYS.phoneChanged,
2729
2802
  locale: ctx.user.locale,
2730
2803
  data: {},
2731
2804
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
2732
2805
  }));
2733
2806
  }
2734
- return (0, import_core8.setApiResponse)(
2735
- import_core8.HTTP.OK,
2807
+ return (0, import_core9.setApiResponse)(
2808
+ import_core9.HTTP.OK,
2736
2809
  "PHONE_UPDATED",
2737
2810
  "Phone number updated. A verification code has been sent to your new number.",
2738
2811
  {
@@ -2745,23 +2818,23 @@ function userController(store, config, bus) {
2745
2818
  const currentPassword = body?.["currentPassword"];
2746
2819
  const newPassword = body?.["newPassword"];
2747
2820
  if (typeof currentPassword !== "string" || !currentPassword) {
2748
- return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
2821
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
2749
2822
  }
2750
2823
  if (typeof newPassword !== "string" || newPassword.length < 8) {
2751
- return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
2824
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
2752
2825
  }
2753
2826
  const user = await users.findById(ctx.user.id);
2754
- if (!user) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2827
+ if (!user) return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2755
2828
  const { hashPassword: hashPassword2, verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password(), password_exports));
2756
2829
  const valid = await verifyPassword2(currentPassword, user.passwordHash ?? "");
2757
2830
  if (!valid) {
2758
- return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
2831
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
2759
2832
  }
2760
2833
  const hash = await hashPassword2(newPassword);
2761
2834
  await users.updatePassword(ctx.user.id, hash);
2762
2835
  await sessions.deleteByUser(ctx.user.id);
2763
- await (0, import_core8.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: null, reason: "password-changed" }));
2764
- return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "PASSWORD_CHANGED", "Password updated successfully.");
2836
+ await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: null, reason: "password-changed" }));
2837
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "PASSWORD_CHANGED", "Password updated successfully.");
2765
2838
  },
2766
2839
  /**
2767
2840
  * Remove an OAuth provider from the account.
@@ -2780,44 +2853,50 @@ function userController(store, config, bus) {
2780
2853
  const provider = String(ctx.meta["params"]?.["provider"] ?? "");
2781
2854
  const current = await users.findById(ctx.user.id);
2782
2855
  if (!current?.provider) {
2783
- return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_LINKED", "No sign-in provider is linked.");
2856
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_LINKED", "No sign-in provider is linked.");
2784
2857
  }
2785
2858
  if (provider && provider.toLowerCase() !== current.provider.toLowerCase()) {
2786
- return (0, import_core8.setApiResponse)(
2787
- import_core8.HTTP.NOT_FOUND,
2859
+ return (0, import_core9.setApiResponse)(
2860
+ import_core9.HTTP.NOT_FOUND,
2788
2861
  "NOT_LINKED",
2789
2862
  `This account is not linked to ${provider}.`
2790
2863
  );
2791
2864
  }
2792
2865
  const removed = await users.clearProvider(ctx.user.id);
2793
2866
  if (!removed) {
2794
- return (0, import_core8.setApiResponse)(
2795
- import_core8.HTTP.CONFLICT,
2867
+ return (0, import_core9.setApiResponse)(
2868
+ import_core9.HTTP.CONFLICT,
2796
2869
  "PASSWORD_REQUIRED",
2797
2870
  "Set a password before disconnecting this provider \u2014 it is currently the only way to sign in."
2798
2871
  );
2799
2872
  }
2800
2873
  if (ctx.user.email) {
2801
- await (0, import_core8.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2874
+ await (0, import_core9.background)(bus?.emit(import_events3.NOTIFICATION_EVENT, {
2802
2875
  type: MESSAGE_KEYS.oauthUnlinked,
2803
2876
  locale: ctx.user.locale,
2804
2877
  data: { provider: current.provider },
2805
2878
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
2806
2879
  }));
2807
2880
  }
2808
- return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
2881
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
2809
2882
  },
2810
2883
  deleteMe: async (ctx) => {
2811
2884
  const userId = ctx.user.id;
2812
2885
  await users.softDelete(userId);
2813
2886
  await sessions.deleteByUser(userId);
2887
+ await store.query(
2888
+ `WITH resets AS (DELETE FROM fonderie_password_resets WHERE user_id = $1),
2889
+ emails AS (DELETE FROM fonderie_email_verifications WHERE user_id = $1)
2890
+ DELETE FROM fonderie_phone_verifications WHERE user_id = $1`,
2891
+ [userId]
2892
+ );
2814
2893
  const reqId = ctx.meta["requestId"];
2815
- await (0, import_core8.background)(bus?.emit(
2894
+ await (0, import_core9.background)(bus?.emit(
2816
2895
  EVENT_KEYS.userDeleted,
2817
2896
  { userId },
2818
2897
  reqId !== void 0 ? { requestId: reqId } : void 0
2819
2898
  ));
2820
- await (0, import_core8.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId, sids: null, reason: "account-deleted" }));
2899
+ await (0, import_core9.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId, sids: null, reason: "account-deleted" }));
2821
2900
  return Response.json(
2822
2901
  { reason: "ACCOUNT_DELETED", explanation: "Account successfully deleted." },
2823
2902
  {
@@ -2831,7 +2910,7 @@ function userController(store, config, bus) {
2831
2910
  // no MFA secret, no session tokens (session metadata only).
2832
2911
  exportMe: async (ctx) => {
2833
2912
  const user = await users.findById(ctx.user.id);
2834
- if (!user) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2913
+ if (!user) return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
2835
2914
  const sessionRows = await sessions.listByUser(ctx.user.id);
2836
2915
  const bundle = {
2837
2916
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2840,8 +2919,8 @@ function userController(store, config, bus) {
2840
2919
  id: s.id,
2841
2920
  userAgent: s.userAgent,
2842
2921
  ipAddress: s.ipAddress,
2843
- createdAt: (0, import_core8.dateOrEmpty)(s.createdAt),
2844
- expiresAt: (0, import_core8.dateOrEmpty)(s.expiresAt)
2922
+ createdAt: (0, import_core9.dateOrEmpty)(s.createdAt),
2923
+ expiresAt: (0, import_core9.dateOrEmpty)(s.expiresAt)
2845
2924
  })),
2846
2925
  security: {
2847
2926
  mfaEnabled: user.mfaEnabled === true,
@@ -2860,7 +2939,7 @@ function userController(store, config, bus) {
2860
2939
  }
2861
2940
  bundle["modules"] = modules;
2862
2941
  }
2863
- return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "DATA_EXPORT", "Your account data.", bundle);
2942
+ return (0, import_core9.setApiResponse)(import_core9.HTTP.OK, "DATA_EXPORT", "Your account data.", bundle);
2864
2943
  }
2865
2944
  };
2866
2945
  }
@@ -2868,7 +2947,7 @@ function userController(store, config, bus) {
2868
2947
  // src/controllers/oauth.controller.ts
2869
2948
  var import_node_crypto9 = require("crypto");
2870
2949
  var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
2871
- var import_core9 = require("@fonderie/core");
2950
+ var import_core10 = require("@fonderie/core");
2872
2951
  var import_events4 = require("@fonderie/events");
2873
2952
 
2874
2953
  // src/services/jwks.ts
@@ -3019,7 +3098,7 @@ function oauthController(store, config, bus) {
3019
3098
  const label = provider.charAt(0).toUpperCase() + provider.slice(1);
3020
3099
  const recipient = { email: user.email, phone: null, deviceToken: null };
3021
3100
  if (upserted.clearedUnverifiedPassword && user.email) {
3022
- await (0, import_core9.background)(bus.emit(
3101
+ await (0, import_core10.background)(bus.emit(
3023
3102
  import_events4.NOTIFICATION_EVENT,
3024
3103
  {
3025
3104
  type: MESSAGE_KEYS.passwordRevoked,
@@ -3031,7 +3110,7 @@ function oauthController(store, config, bus) {
3031
3110
  ));
3032
3111
  }
3033
3112
  if (upserted.inserted) {
3034
- await (0, import_core9.background)(bus.emit(
3113
+ await (0, import_core10.background)(bus.emit(
3035
3114
  EVENT_KEYS.userRegistered,
3036
3115
  {
3037
3116
  userId: upserted.id,
@@ -3043,7 +3122,7 @@ function oauthController(store, config, bus) {
3043
3122
  reqOpts
3044
3123
  ));
3045
3124
  if (user.email) {
3046
- await (0, import_core9.background)(bus.emit(
3125
+ await (0, import_core10.background)(bus.emit(
3047
3126
  import_events4.NOTIFICATION_EVENT,
3048
3127
  {
3049
3128
  type: MESSAGE_KEYS.oauthRegistration,
@@ -3057,7 +3136,7 @@ function oauthController(store, config, bus) {
3057
3136
  return;
3058
3137
  }
3059
3138
  if (upserted.previousProvider !== provider && user.email) {
3060
- await (0, import_core9.background)(bus.emit(
3139
+ await (0, import_core10.background)(bus.emit(
3061
3140
  import_events4.NOTIFICATION_EVENT,
3062
3141
  {
3063
3142
  type: MESSAGE_KEYS.oauthLinked,
@@ -3078,14 +3157,29 @@ function oauthController(store, config, bus) {
3078
3157
  const method = provider === "apple" ? "oauth-apple" : "oauth-google";
3079
3158
  const meta = requestMeta(ctx);
3080
3159
  if (!claims.email) {
3081
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, p.failed, `No email in ${p.label} identity token`);
3160
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, p.failed, `No email in ${p.label} identity token`);
3082
3161
  }
3083
3162
  if (!claims.emailVerified) {
3084
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, p.failed, `${p.label} account email is not verified`);
3163
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, p.failed, `${p.label} account email is not verified`);
3085
3164
  }
3086
3165
  const normalizedEmail = normalizeEmailSafe(claims.email);
3087
3166
  if (!normalizedEmail) {
3088
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, p.failed, `Invalid email in ${p.label} identity token`);
3167
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, p.failed, `Invalid email in ${p.label} identity token`);
3168
+ }
3169
+ const archived = await users.findArchivedByEmail(normalizedEmail);
3170
+ if (archived?.deletedAt) {
3171
+ await loginEvents.recordSafe({
3172
+ userId: archived.id,
3173
+ emailAttempted: normalizedEmail,
3174
+ method,
3175
+ outcome: "failed",
3176
+ failureReason: "pending_deletion",
3177
+ ...meta
3178
+ });
3179
+ const res = pendingDeletionResponse(archived.deletedAt, config);
3180
+ const headers = cookieHeaders(clearCookies);
3181
+ headers.set("content-type", res.headers.get("content-type") ?? "application/json");
3182
+ return new Response(res.body, { status: res.status, headers });
3089
3183
  }
3090
3184
  const upserted = await users.upsertByProvider(normalizedEmail, provider, claims.sub ?? "");
3091
3185
  if (!upserted) {
@@ -3097,15 +3191,15 @@ function oauthController(store, config, bus) {
3097
3191
  failureReason: "provider_upsert_failed",
3098
3192
  ...meta
3099
3193
  });
3100
- return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
3194
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
3101
3195
  }
3102
3196
  const fullUser = await users.findById(upserted.id);
3103
3197
  if (!fullUser) {
3104
- return (0, import_core9.setApiResponse)(import_core9.HTTP.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
3198
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
3105
3199
  }
3106
3200
  await announceOAuthUpsert(ctx, upserted, provider, fullUser);
3107
3201
  if (fullUser.suspended) {
3108
- return (0, import_core9.setApiResponse)(import_core9.HTTP.FORBIDDEN, "ACCOUNT_SUSPENDED", "Account suspended. Please contact support.");
3202
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.FORBIDDEN, "ACCOUNT_SUSPENDED", "Account suspended. Please contact support.");
3109
3203
  }
3110
3204
  if (fullUser.mfaEnabled) {
3111
3205
  const mfaToken = issueMfaPendingToken(upserted.id, config, provider);
@@ -3148,8 +3242,8 @@ function oauthController(store, config, bus) {
3148
3242
  googleInit: async (_ctx) => {
3149
3243
  const google = config.google;
3150
3244
  if (!google || !google.clientSecret || !google.redirectUri) {
3151
- return (0, import_core9.setApiResponse)(
3152
- import_core9.HTTP.NOT_IMPLEMENTED,
3245
+ return (0, import_core10.setApiResponse)(
3246
+ import_core10.HTTP.NOT_IMPLEMENTED,
3153
3247
  "NOT_CONFIGURED",
3154
3248
  "Google OAuth not configured"
3155
3249
  );
@@ -3183,8 +3277,8 @@ function oauthController(store, config, bus) {
3183
3277
  googleCallback: async (ctx) => {
3184
3278
  const google = config.google;
3185
3279
  if (!google || !google.clientSecret || !google.redirectUri) {
3186
- return (0, import_core9.setApiResponse)(
3187
- import_core9.HTTP.NOT_IMPLEMENTED,
3280
+ return (0, import_core10.setApiResponse)(
3281
+ import_core10.HTTP.NOT_IMPLEMENTED,
3188
3282
  "NOT_CONFIGURED",
3189
3283
  "Google OAuth not configured"
3190
3284
  );
@@ -3192,14 +3286,14 @@ function oauthController(store, config, bus) {
3192
3286
  const url = new URL(ctx.request.url);
3193
3287
  const code = url.searchParams.get("code");
3194
3288
  if (!code) {
3195
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
3289
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
3196
3290
  }
3197
3291
  const returnedState = url.searchParams.get("state") ?? "";
3198
3292
  const cookieHeader = ctx.request.headers.get("cookie") ?? "";
3199
3293
  const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
3200
- if (!returnedState || !expectedState || !(0, import_core9.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
3201
- return (0, import_core9.setApiResponse)(
3202
- import_core9.HTTP.BAD_REQUEST,
3294
+ if (!returnedState || !expectedState || !(0, import_core10.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
3295
+ return (0, import_core10.setApiResponse)(
3296
+ import_core10.HTTP.BAD_REQUEST,
3203
3297
  "GOOGLE_AUTH_FAILED",
3204
3298
  "OAuth state mismatch \u2014 restart the sign-in flow"
3205
3299
  );
@@ -3217,8 +3311,8 @@ function oauthController(store, config, bus) {
3217
3311
  });
3218
3312
  const tokenData = await tokenRes.json();
3219
3313
  if (!tokenData.id_token) {
3220
- return (0, import_core9.setApiResponse)(
3221
- import_core9.HTTP.BAD_REQUEST,
3314
+ return (0, import_core10.setApiResponse)(
3315
+ import_core10.HTTP.BAD_REQUEST,
3222
3316
  "GOOGLE_AUTH_FAILED",
3223
3317
  "OAuth token exchange failed"
3224
3318
  );
@@ -3227,20 +3321,20 @@ function oauthController(store, config, bus) {
3227
3321
  Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
3228
3322
  );
3229
3323
  if (payload.aud !== google.clientId) {
3230
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
3324
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
3231
3325
  }
3232
3326
  if (payload.iss !== "https://accounts.google.com" && payload.iss !== "accounts.google.com") {
3233
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
3327
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
3234
3328
  }
3235
3329
  if (typeof payload.exp !== "number" || payload.exp * 1e3 < Date.now()) {
3236
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
3330
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
3237
3331
  }
3238
3332
  if (!payload.email) {
3239
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
3333
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
3240
3334
  }
3241
3335
  if (payload.email_verified !== true) {
3242
- return (0, import_core9.setApiResponse)(
3243
- import_core9.HTTP.BAD_REQUEST,
3336
+ return (0, import_core10.setApiResponse)(
3337
+ import_core10.HTTP.BAD_REQUEST,
3244
3338
  "GOOGLE_AUTH_FAILED",
3245
3339
  "Google account email is not verified"
3246
3340
  );
@@ -3254,7 +3348,7 @@ function oauthController(store, config, bus) {
3254
3348
  appleInit: async (_ctx) => {
3255
3349
  const apple = config.apple;
3256
3350
  if (!apple) {
3257
- return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3351
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3258
3352
  }
3259
3353
  const state = (0, import_node_crypto9.randomBytes)(16).toString("hex");
3260
3354
  const params = new URLSearchParams({
@@ -3285,20 +3379,20 @@ function oauthController(store, config, bus) {
3285
3379
  appleCallback: async (ctx) => {
3286
3380
  const apple = config.apple;
3287
3381
  if (!apple) {
3288
- return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3382
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3289
3383
  }
3290
3384
  const pre = ctx.meta["body"];
3291
3385
  const form = pre && typeof pre === "object" ? new URLSearchParams(pre) : new URLSearchParams(await ctx.request.text());
3292
3386
  const code = form.get("code");
3293
3387
  if (!code) {
3294
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
3388
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
3295
3389
  }
3296
3390
  const returnedState = form.get("state") ?? "";
3297
3391
  const cookieHeader = ctx.request.headers.get("cookie") ?? "";
3298
3392
  const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
3299
- if (!returnedState || !expectedState || !(0, import_core9.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
3300
- return (0, import_core9.setApiResponse)(
3301
- import_core9.HTTP.BAD_REQUEST,
3393
+ if (!returnedState || !expectedState || !(0, import_core10.constantTimeEqual)(Buffer.from(returnedState), Buffer.from(expectedState))) {
3394
+ return (0, import_core10.setApiResponse)(
3395
+ import_core10.HTTP.BAD_REQUEST,
3302
3396
  "APPLE_AUTH_FAILED",
3303
3397
  "OAuth state mismatch \u2014 restart the sign-in flow"
3304
3398
  );
@@ -3316,11 +3410,11 @@ function oauthController(store, config, bus) {
3316
3410
  });
3317
3411
  const tokenData = await tokenRes.json();
3318
3412
  if (!tokenData.id_token) {
3319
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
3413
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
3320
3414
  }
3321
3415
  const claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });
3322
3416
  if (!claims) {
3323
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
3417
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
3324
3418
  }
3325
3419
  return completeOAuthLogin(ctx, "apple", {
3326
3420
  email: claims.email,
@@ -3331,12 +3425,12 @@ function oauthController(store, config, bus) {
3331
3425
  appleNative: async (ctx) => {
3332
3426
  const apple = config.apple;
3333
3427
  if (!apple) {
3334
- return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3428
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3335
3429
  }
3336
3430
  const body = ctx.meta["body"];
3337
3431
  const identityToken = body?.identityToken;
3338
3432
  if (!identityToken) {
3339
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
3433
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
3340
3434
  }
3341
3435
  const audiences = apple.nativeClientIds && apple.nativeClientIds.length ? apple.nativeClientIds : [apple.clientId];
3342
3436
  const claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });
@@ -3350,7 +3444,7 @@ function oauthController(store, config, bus) {
3350
3444
  failureReason: "invalid_identity_token",
3351
3445
  ...meta
3352
3446
  });
3353
- return (0, import_core9.setApiResponse)(import_core9.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
3447
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
3354
3448
  }
3355
3449
  const tokenHash = (0, import_node_crypto9.createHash)("sha256").update(identityToken).digest("hex");
3356
3450
  const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
@@ -3365,8 +3459,8 @@ function oauthController(store, config, bus) {
3365
3459
  failureReason: "token_replayed",
3366
3460
  ...meta
3367
3461
  });
3368
- return (0, import_core9.setApiResponse)(
3369
- import_core9.HTTP.UNAUTHORIZED,
3462
+ return (0, import_core10.setApiResponse)(
3463
+ import_core10.HTTP.UNAUTHORIZED,
3370
3464
  "APPLE_AUTH_FAILED",
3371
3465
  "This Apple identity token has already been used"
3372
3466
  );
@@ -3383,12 +3477,12 @@ function oauthController(store, config, bus) {
3383
3477
  googleNative: async (ctx) => {
3384
3478
  const audiences = config.google?.nativeClientIds ?? [];
3385
3479
  if (!config.google || audiences.length === 0) {
3386
- return (0, import_core9.setApiResponse)(import_core9.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Native Google sign-in not configured");
3480
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Native Google sign-in not configured");
3387
3481
  }
3388
3482
  const body = ctx.meta["body"];
3389
3483
  const idToken = body?.idToken;
3390
3484
  if (!idToken) {
3391
- return (0, import_core9.setApiResponse)(import_core9.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing idToken");
3485
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.BAD_REQUEST, "INVALID_PARAMETER", "Missing idToken");
3392
3486
  }
3393
3487
  const claims = await verifyGoogleIdToken(idToken, { audiences, nonce: body?.nonce });
3394
3488
  if (!claims) {
@@ -3400,7 +3494,7 @@ function oauthController(store, config, bus) {
3400
3494
  failureReason: "invalid_identity_token",
3401
3495
  ...requestMeta(ctx)
3402
3496
  });
3403
- return (0, import_core9.setApiResponse)(import_core9.HTTP.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "Invalid Google ID token");
3497
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "Invalid Google ID token");
3404
3498
  }
3405
3499
  const tokenHash = (0, import_node_crypto9.createHash)("sha256").update(idToken).digest("hex");
3406
3500
  const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
@@ -3413,7 +3507,7 @@ function oauthController(store, config, bus) {
3413
3507
  failureReason: "token_replayed",
3414
3508
  ...requestMeta(ctx)
3415
3509
  });
3416
- return (0, import_core9.setApiResponse)(import_core9.HTTP.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "This Google ID token has already been used");
3510
+ return (0, import_core10.setApiResponse)(import_core10.HTTP.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "This Google ID token has already been used");
3417
3511
  }
3418
3512
  consumedTokens.sweepExpiredSafe();
3419
3513
  return completeOAuthLogin(ctx, "google", {
@@ -3459,7 +3553,7 @@ function buildAuthRoutes(store, config, bus, locales) {
3459
3553
  "providers",
3460
3554
  "GET",
3461
3555
  "/auth/providers",
3462
- async () => (0, import_core10.setApiResponse)(import_core10.HTTP.OK, "AUTH_PROVIDERS", "Sign-in methods available here", {
3556
+ async () => (0, import_core11.setApiResponse)(import_core11.HTTP.OK, "AUTH_PROVIDERS", "Sign-in methods available here", {
3463
3557
  providers: [...config.providers]
3464
3558
  })
3465
3559
  ),
@@ -3534,20 +3628,20 @@ function buildAuthRoutes(store, config, bus, locales) {
3534
3628
  }
3535
3629
 
3536
3630
  // src/services/config-guard.ts
3537
- var import_core11 = require("@fonderie/core");
3631
+ var import_core12 = require("@fonderie/core");
3538
3632
  var MODULE = "@fonderie/auth";
3539
3633
  function collectAuthConfigProblems(config) {
3540
3634
  const problems = [];
3541
3635
  const secret = config.jwtSecret ?? "";
3542
- const secretProblem = (0, import_core11.secretStrengthProblem)(secret);
3636
+ const secretProblem = (0, import_core12.secretStrengthProblem)(secret);
3543
3637
  if (secretProblem === "too-short") {
3544
3638
  problems.push({
3545
3639
  module: MODULE,
3546
3640
  severity: "error",
3547
- message: `jwtSecret must be at least ${import_core11.MIN_SECRET_LENGTH} characters (got ${secret.length})`,
3641
+ message: `jwtSecret must be at least ${import_core12.MIN_SECRET_LENGTH} characters (got ${secret.length})`,
3548
3642
  domain: "auth",
3549
3643
  reason: "JWT_SECRET_TOO_SHORT",
3550
- metadata: { min: import_core11.MIN_SECRET_LENGTH, got: secret.length }
3644
+ metadata: { min: import_core12.MIN_SECRET_LENGTH, got: secret.length }
3551
3645
  });
3552
3646
  } else if (secretProblem === "placeholder") {
3553
3647
  problems.push({
@@ -3559,7 +3653,7 @@ function collectAuthConfigProblems(config) {
3559
3653
  });
3560
3654
  }
3561
3655
  (config.jwtPreviousSecrets ?? []).forEach((previous, index) => {
3562
- if ((0, import_core11.secretStrengthProblem)(previous ?? "")) {
3656
+ if ((0, import_core12.secretStrengthProblem)(previous ?? "")) {
3563
3657
  problems.push({
3564
3658
  module: MODULE,
3565
3659
  severity: "error",
@@ -3592,7 +3686,7 @@ function collectAuthConfigProblems(config) {
3592
3686
  domain: "auth",
3593
3687
  reason: "GOOGLE_INCOMPLETE"
3594
3688
  });
3595
- } else if (clientSecret && import_core11.PLACEHOLDER_SECRET.test(clientSecret)) {
3689
+ } else if (clientSecret && import_core12.PLACEHOLDER_SECRET.test(clientSecret)) {
3596
3690
  problems.push({
3597
3691
  module: MODULE,
3598
3692
  severity: "error",
@@ -3723,8 +3817,8 @@ function extractToken(request) {
3723
3817
  }
3724
3818
 
3725
3819
  // src/admin.ts
3726
- var import_core12 = require("@fonderie/core");
3727
3820
  var import_core13 = require("@fonderie/core");
3821
+ var import_core14 = require("@fonderie/core");
3728
3822
  function toAdminUserDTO(user) {
3729
3823
  return {
3730
3824
  ...toUserDTO(user),
@@ -3735,12 +3829,12 @@ function toAdminUserDTO(user) {
3735
3829
  }
3736
3830
  function toAdminUserPageDTO(page) {
3737
3831
  const last = page.users[page.users.length - 1];
3738
- const nextCursor = page.hasMore && last ? (0, import_core12.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
3832
+ const nextCursor = page.hasMore && last ? (0, import_core13.encodeKeysetCursor)(last.createdAtRaw ?? last.createdAt.toISOString(), last.id) : null;
3739
3833
  return { users: page.users.map(toAdminUserDTO), nextCursor };
3740
3834
  }
3741
3835
  var ADMIN_PREFIX = "/_admin";
3742
3836
  var idOf = (ctx) => ctx.meta.params?.["id"] ?? "";
3743
- var NOT_FOUND = () => (0, import_core12.setApiResponse)(import_core12.HTTP.NOT_FOUND, "NOT_FOUND", "No such user");
3837
+ var NOT_FOUND = () => (0, import_core13.setApiResponse)(import_core13.HTTP.NOT_FOUND, "NOT_FOUND", "No such user");
3744
3838
  function adminRouteTable(store, bus) {
3745
3839
  const users = new UserModel(store);
3746
3840
  const sessions = new SessionModel(store);
@@ -3749,8 +3843,8 @@ function adminRouteTable(store, bus) {
3749
3843
  const ok = await users.setSuspended(idOf(ctx), suspended);
3750
3844
  if (!ok) return NOT_FOUND();
3751
3845
  const user = await users.findById(idOf(ctx));
3752
- return user ? (0, import_core12.setApiResponse)(
3753
- import_core12.HTTP.OK,
3846
+ return user ? (0, import_core13.setApiResponse)(
3847
+ import_core13.HTTP.OK,
3754
3848
  suspended ? "USER_SUSPENDED" : "USER_UNSUSPENDED",
3755
3849
  suspended ? "User suspended" : "User unsuspended",
3756
3850
  toAdminUserDTO(user)
@@ -3769,21 +3863,21 @@ function adminRouteTable(store, bus) {
3769
3863
  const email2 = raw ? normalizeEmailSafe(raw) ?? raw.toLowerCase() : void 0;
3770
3864
  if (email2) {
3771
3865
  const user = await users.findByEmail(email2);
3772
- return user ? (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3866
+ return user ? (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3773
3867
  }
3774
3868
  const rawLimit = Number(params.get("limit") ?? 50);
3775
3869
  const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
3776
3870
  const cursorParam = params.get("cursor");
3777
3871
  const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
3778
3872
  if (cursorParam && !cursor)
3779
- return (0, import_core12.setApiResponse)(import_core12.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3873
+ return (0, import_core13.setApiResponse)(import_core13.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3780
3874
  const deleted = params.get("deleted") === "1" || params.get("deleted") === "true";
3781
3875
  const page = await users.list({
3782
3876
  limit,
3783
3877
  ...cursor ? { cursor } : {},
3784
3878
  ...deleted ? { deleted } : {}
3785
3879
  });
3786
- return (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USERS", "Users", toAdminUserPageDTO(page));
3880
+ return (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "USERS", "Users", toAdminUserPageDTO(page));
3787
3881
  }
3788
3882
  ],
3789
3883
  [
@@ -3791,7 +3885,7 @@ function adminRouteTable(store, bus) {
3791
3885
  "/_admin/users/:id",
3792
3886
  async (ctx) => {
3793
3887
  const user = await users.findByIdIncludingDeleted(idOf(ctx));
3794
- return user ? (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3888
+ return user ? (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3795
3889
  }
3796
3890
  ],
3797
3891
  [
@@ -3800,8 +3894,8 @@ function adminRouteTable(store, bus) {
3800
3894
  async (ctx) => {
3801
3895
  if (!await users.findById(idOf(ctx))) return NOT_FOUND();
3802
3896
  const rows = await sessions.listLiveByUser(idOf(ctx));
3803
- return (0, import_core12.setApiResponse)(
3804
- import_core12.HTTP.OK,
3897
+ return (0, import_core13.setApiResponse)(
3898
+ import_core13.HTTP.OK,
3805
3899
  "SESSIONS",
3806
3900
  "Live sessions",
3807
3901
  rows.map((r) => toSessionDTO(r, null))
@@ -3815,8 +3909,8 @@ function adminRouteTable(store, bus) {
3815
3909
  async (ctx) => {
3816
3910
  if (!await users.findById(idOf(ctx))) return NOT_FOUND();
3817
3911
  await sessions.deleteByUser(idOf(ctx));
3818
- await (0, import_core13.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: idOf(ctx), sids: null, reason: "admin" }));
3819
- return (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "SESSIONS_REVOKED", "All sessions revoked");
3912
+ await (0, import_core14.background)(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: idOf(ctx), sids: null, reason: "admin" }));
3913
+ return (0, import_core13.setApiResponse)(import_core13.HTTP.OK, "SESSIONS_REVOKED", "All sessions revoked");
3820
3914
  }
3821
3915
  ],
3822
3916
  [
@@ -3830,14 +3924,14 @@ function adminRouteTable(store, bus) {
3830
3924
  const cursorParam = params.get("cursor");
3831
3925
  const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
3832
3926
  if (cursorParam && !cursor)
3833
- return (0, import_core12.setApiResponse)(import_core12.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3927
+ return (0, import_core13.setApiResponse)(import_core13.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3834
3928
  const page = await events.listByUser({
3835
3929
  userId: idOf(ctx),
3836
3930
  limit,
3837
3931
  ...cursor ? { cursor } : {}
3838
3932
  });
3839
- return (0, import_core12.setApiResponse)(
3840
- import_core12.HTTP.OK,
3933
+ return (0, import_core13.setApiResponse)(
3934
+ import_core13.HTTP.OK,
3841
3935
  "LOGIN_HISTORY",
3842
3936
  "Login history",
3843
3937
  toLoginHistoryPageDTO(page)
@@ -3870,7 +3964,7 @@ var AuthModule = class {
3870
3964
  name = "@fonderie/auth";
3871
3965
  // Baked in at build time by tsup.base, so the operator's Modules page can
3872
3966
  // say what is actually deployed rather than 'not reported'.
3873
- version = "7.22.1";
3967
+ version = "7.23.0";
3874
3968
  // Users, sessions, login history, suspend — only through @fonderie/admin.
3875
3969
  describeAdmin() {
3876
3970
  return { routes: describeAuthAdminRoutes(this.store, this.bus) };
@@ -3901,7 +3995,7 @@ var AuthModule = class {
3901
3995
  };
3902
3996
 
3903
3997
  // src/templates.ts
3904
- var import_core14 = require("@fonderie/core");
3998
+ var import_core15 = require("@fonderie/core");
3905
3999
 
3906
4000
  // src/templates.es.ts
3907
4001
  var ES_TEMPLATES = {
@@ -4686,7 +4780,7 @@ change your password \u2014 someone else may be able to sign in as you.`
4686
4780
  If you did this, you're all set. If not, contact support right away.`
4687
4781
  }
4688
4782
  };
4689
- var DEFAULT_TEMPLATES = (0, import_core14.withTranslations)(EN_TEMPLATES, {
4783
+ var DEFAULT_TEMPLATES = (0, import_core15.withTranslations)(EN_TEMPLATES, {
4690
4784
  fr: FR_TEMPLATES,
4691
4785
  es: ES_TEMPLATES,
4692
4786
  "zh-Hans": ZH_HANS_TEMPLATES,