@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.js CHANGED
@@ -44,7 +44,7 @@ var init_password = __esm({
44
44
  });
45
45
 
46
46
  // src/routes.ts
47
- import { setApiResponse as setApiResponse6, HTTP as HTTP6 } from "@fonderie/core";
47
+ import { setApiResponse as setApiResponse7, HTTP as HTTP7 } from "@fonderie/core";
48
48
  import { requireAuth, requireAnyAuth, requireVerified } from "@fonderie/core/middlewares";
49
49
 
50
50
  // src/middlewares/require-email-login.ts
@@ -338,6 +338,10 @@ var EVENT_KEYS = {
338
338
  // 'password-changed', 'admin', 'refresh-reuse' (theft signal).
339
339
  sessionRevoked: "fonderie.session.revoked"
340
340
  };
341
+ var deletionGracePeriodDays = (config) => {
342
+ const days = config.accountDeletion?.gracePeriodDays;
343
+ return typeof days === "number" && Number.isFinite(days) && days >= 0 ? days : 30;
344
+ };
341
345
 
342
346
  // src/services/jwt.ts
343
347
  import { createHash, randomUUID } from "crypto";
@@ -754,6 +758,27 @@ var UserModel = class {
754
758
  );
755
759
  return row ?? null;
756
760
  }
761
+ /**
762
+ * An ARCHIVED (soft-deleted, not yet purged) account holding this address.
763
+ * Only for telling its owner — after they prove it is theirs — that it is
764
+ * scheduled for deletion, and for refusing to create a second account on
765
+ * the address meanwhile. Never for signing in.
766
+ */
767
+ async findArchivedByEmail(email2) {
768
+ const [row] = await this.store.query(
769
+ `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NOT NULL`,
770
+ [email2]
771
+ );
772
+ return row ?? null;
773
+ }
774
+ /** As findArchivedByEmail, for a phone number. */
775
+ async findArchivedByPhone(phone2) {
776
+ const [row] = await this.store.query(
777
+ `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NOT NULL`,
778
+ [phone2]
779
+ );
780
+ return row ?? null;
781
+ }
757
782
  async findByPhone(phone2) {
758
783
  const [row] = await this.store.query(
759
784
  `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
@@ -1608,7 +1633,7 @@ function mfaController(store, config, issuer, bus) {
1608
1633
  // src/controllers/auth.controller.ts
1609
1634
  import { randomBytes as randomBytes3, randomInt } from "crypto";
1610
1635
  import { NOTIFICATION_EVENT as NOTIFICATION_EVENT2 } from "@fonderie/events";
1611
- import { setApiResponse as setApiResponse3, HTTP as HTTP3, background as background3, canonicalLocale, defineLocales } from "@fonderie/core";
1636
+ import { setApiResponse as setApiResponse4, HTTP as HTTP4, background as background3, canonicalLocale, defineLocales } from "@fonderie/core";
1612
1637
 
1613
1638
  // src/services/cooldown.ts
1614
1639
  function checkCooldown(lastSentAt, cooldownMs) {
@@ -1619,6 +1644,28 @@ function checkCooldown(lastSentAt, cooldownMs) {
1619
1644
  // src/controllers/auth.controller.ts
1620
1645
  init_password();
1621
1646
 
1647
+ // src/services/pending-deletion.ts
1648
+ import { setApiResponse as setApiResponse3, HTTP as HTTP3 } from "@fonderie/core";
1649
+ function deletionDate(deletedAt, config) {
1650
+ return new Date(deletedAt.getTime() + deletionGracePeriodDays(config) * 24 * 60 * 60 * 1e3);
1651
+ }
1652
+ function pendingDeletionResponse(deletedAt, config) {
1653
+ const deleteOn = deletionDate(deletedAt, config);
1654
+ return setApiResponse3(
1655
+ HTTP3.FORBIDDEN,
1656
+ "ACCOUNT_PENDING_DELETION",
1657
+ `This account is scheduled for deletion on ${deleteOn.toISOString().slice(0, 10)}.`,
1658
+ { requestedAt: deletedAt.toISOString(), deleteOn: deleteOn.toISOString() }
1659
+ );
1660
+ }
1661
+ function archivedAddressResponse() {
1662
+ return setApiResponse3(
1663
+ HTTP3.CONFLICT,
1664
+ "ACCOUNT_PENDING_DELETION",
1665
+ "An account using this address is scheduled for deletion. Sign in to keep it, or wait until it is deleted."
1666
+ );
1667
+ }
1668
+
1622
1669
  // src/models/password-reset.model.ts
1623
1670
  import { createHash as createHash3 } from "crypto";
1624
1671
  var hashSecret = (value) => createHash3("sha256").update(value).digest("hex");
@@ -1791,19 +1838,20 @@ function authController(store, config, bus, locales = defineLocales()) {
1791
1838
  if (typeof email2 === "string" && typeof password2 === "string") {
1792
1839
  const normalizedEmail = normalizeEmailSafe(email2);
1793
1840
  if (!normalizedEmail) {
1794
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1841
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1795
1842
  }
1796
1843
  if (password2.length < 8) {
1797
- return setApiResponse3(
1798
- HTTP3.UNPROCESSABLE,
1844
+ return setApiResponse4(
1845
+ HTTP4.UNPROCESSABLE,
1799
1846
  "INVALID_PARAMETER",
1800
1847
  "password must be at least 8 characters"
1801
1848
  );
1802
1849
  }
1803
1850
  const existing = await users.findByEmail(normalizedEmail);
1804
1851
  if (existing) {
1805
- return setApiResponse3(HTTP3.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
1852
+ return setApiResponse4(HTTP4.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
1806
1853
  }
1854
+ if (await users.findArchivedByEmail(normalizedEmail)) return archivedAddressResponse();
1807
1855
  const passwordHash = await hashPassword(password2);
1808
1856
  const row = await users.create(
1809
1857
  normalizedEmail,
@@ -1813,14 +1861,14 @@ function authController(store, config, bus, locales = defineLocales()) {
1813
1861
  locale
1814
1862
  );
1815
1863
  if (!row) {
1816
- return setApiResponse3(HTTP3.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1864
+ return setApiResponse4(HTTP4.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1817
1865
  }
1818
1866
  const pin = randomInt(1e5, 1e6).toString();
1819
1867
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
1820
1868
  await emailVerif.create(row.id, pin, expiresAt);
1821
1869
  const user = await users.findById(row.id);
1822
1870
  if (!user) {
1823
- return setApiResponse3(HTTP3.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1871
+ return setApiResponse4(HTTP4.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1824
1872
  }
1825
1873
  const reqId = ctx.meta["requestId"];
1826
1874
  const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
@@ -1878,8 +1926,9 @@ function authController(store, config, bus, locales = defineLocales()) {
1878
1926
  if (isValidPhone(phone2)) {
1879
1927
  const existing = await users.findByPhone(normalizePhone(phone2));
1880
1928
  if (existing) {
1881
- return setApiResponse3(HTTP3.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
1929
+ return setApiResponse4(HTTP4.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
1882
1930
  }
1931
+ if (await users.findArchivedByPhone(normalizePhone(phone2))) return archivedAddressResponse();
1883
1932
  const { id } = await users.findOrCreateByPhone(
1884
1933
  normalizePhone(phone2),
1885
1934
  firstName ?? null,
@@ -1891,7 +1940,7 @@ function authController(store, config, bus, locales = defineLocales()) {
1891
1940
  await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
1892
1941
  const user = await users.findById(id);
1893
1942
  if (!user) {
1894
- return setApiResponse3(HTTP3.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1943
+ return setApiResponse4(HTTP4.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1895
1944
  }
1896
1945
  const reqId2 = ctx.meta["requestId"];
1897
1946
  const reqOpts2 = reqId2 !== void 0 ? { requestId: reqId2 } : void 0;
@@ -1929,8 +1978,8 @@ function authController(store, config, bus, locales = defineLocales()) {
1929
1978
  { status: 202 }
1930
1979
  );
1931
1980
  }
1932
- return setApiResponse3(
1933
- HTTP3.UNPROCESSABLE,
1981
+ return setApiResponse4(
1982
+ HTTP4.UNPROCESSABLE,
1934
1983
  "INVALID_PARAMETER",
1935
1984
  "Provide email + password or a valid phone number"
1936
1985
  );
@@ -1942,9 +1991,25 @@ function authController(store, config, bus, locales = defineLocales()) {
1942
1991
  const { email: rawEmail, password: password2 } = body;
1943
1992
  const email2 = normalizeEmailSafe(rawEmail);
1944
1993
  if (!email2) {
1945
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1994
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1946
1995
  }
1947
1996
  const user = await users.findByEmail(email2);
1997
+ if (!user) {
1998
+ const archived = await users.findArchivedByEmail(email2);
1999
+ if (archived?.passwordHash && archived.deletedAt) {
2000
+ const { valid: valid2 } = await verifyPasswordForLogin(password2, archived.passwordHash, config.legacyVerify);
2001
+ await loginEvents.recordSafe({
2002
+ userId: archived.id,
2003
+ emailAttempted: email2,
2004
+ method: "password",
2005
+ outcome: "failed",
2006
+ failureReason: valid2 ? "pending_deletion" : "bad_password",
2007
+ ...meta
2008
+ });
2009
+ if (valid2) return pendingDeletionResponse(archived.deletedAt, config);
2010
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2011
+ }
2012
+ }
1948
2013
  if (!user || !user.passwordHash) {
1949
2014
  await loginEvents.recordSafe({
1950
2015
  userId: null,
@@ -1954,7 +2019,7 @@ function authController(store, config, bus, locales = defineLocales()) {
1954
2019
  failureReason: "unknown_email",
1955
2020
  ...meta
1956
2021
  });
1957
- return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2022
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1958
2023
  }
1959
2024
  const { valid, needsRehash } = await verifyPasswordForLogin(
1960
2025
  password2,
@@ -1970,21 +2035,21 @@ function authController(store, config, bus, locales = defineLocales()) {
1970
2035
  failureReason: "bad_password",
1971
2036
  ...meta
1972
2037
  });
1973
- return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2038
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1974
2039
  }
1975
2040
  if (needsRehash) {
1976
2041
  await users.updatePassword(user.id, await hashPassword(password2));
1977
2042
  }
1978
2043
  if (user.suspended) {
1979
- return setApiResponse3(
1980
- HTTP3.FORBIDDEN,
2044
+ return setApiResponse4(
2045
+ HTTP4.FORBIDDEN,
1981
2046
  "ACCOUNT_SUSPENDED",
1982
2047
  "Account suspended. Please contact support."
1983
2048
  );
1984
2049
  }
1985
2050
  if (user.mfaEnabled) {
1986
2051
  const mfaToken = issueMfaPendingToken(user.id, config, "email");
1987
- return setApiResponse3(HTTP3.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
2052
+ return setApiResponse4(HTTP4.OK, "MFA_REQUIRED", "Multi-factor authentication required", {
1988
2053
  mfaToken
1989
2054
  });
1990
2055
  }
@@ -2021,11 +2086,11 @@ function authController(store, config, bus, locales = defineLocales()) {
2021
2086
  if (isValidPhone(phone2)) {
2022
2087
  const user = await users.findByPhone(normalizePhone(phone2));
2023
2088
  if (!user) {
2024
- return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2089
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
2025
2090
  }
2026
2091
  if (user.suspended) {
2027
- return setApiResponse3(
2028
- HTTP3.FORBIDDEN,
2092
+ return setApiResponse4(
2093
+ HTTP4.FORBIDDEN,
2029
2094
  "ACCOUNT_SUSPENDED",
2030
2095
  "Account suspended. Please contact support."
2031
2096
  );
@@ -2049,8 +2114,8 @@ function authController(store, config, bus, locales = defineLocales()) {
2049
2114
  { status: 202 }
2050
2115
  );
2051
2116
  }
2052
- return setApiResponse3(
2053
- HTTP3.UNPROCESSABLE,
2117
+ return setApiResponse4(
2118
+ HTTP4.UNPROCESSABLE,
2054
2119
  "INVALID_PARAMETER",
2055
2120
  "Provide email + password or a valid phone number"
2056
2121
  );
@@ -2075,35 +2140,35 @@ function authController(store, config, bus, locales = defineLocales()) {
2075
2140
  refresh: async (ctx) => {
2076
2141
  const token = extractRefreshToken(ctx);
2077
2142
  if (!token) {
2078
- return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
2143
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "INVALID_PARAMETER", "No refresh token provided");
2079
2144
  }
2080
2145
  const payload = verifyToken(token, config);
2081
2146
  if (!payload || payload.type !== "refresh") {
2082
- return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
2147
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Invalid refresh token");
2083
2148
  }
2084
2149
  const runtime = config.resolve?.(ctx);
2085
2150
  let issued = null;
2086
2151
  for (let attempt = 0; attempt < 3 && !issued; attempt++) {
2087
2152
  const found = await sessions.match(token);
2088
2153
  if (found.kind === "unknown") {
2089
- return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2154
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2090
2155
  }
2091
2156
  if (found.kind === "reused") {
2092
2157
  await sessions.revokeById(found.row.id);
2093
2158
  await background3(
2094
2159
  bus?.emit(EVENT_KEYS.sessionRevoked, { userId: found.row.userId, sids: [found.row.sid], reason: "refresh-reuse" })
2095
2160
  );
2096
- return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2161
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2097
2162
  }
2098
2163
  const resolvedRefresh = configForClient(config, runtime, found.row.clientKind);
2099
2164
  const maxAgeMs = resolvedRefresh.sessionMaxAge ? durationMs(String(resolvedRefresh.sessionMaxAge)) : null;
2100
2165
  if (maxAgeMs !== null && Date.now() - found.row.createdAt.getTime() > maxAgeMs) {
2101
2166
  await sessions.revokeById(found.row.id);
2102
- return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
2167
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session reached its maximum age \u2014 sign in again");
2103
2168
  }
2104
2169
  const user = await users.findById(found.row.userId);
2105
2170
  if (!user || user.suspended || user.deletedAt || user.id !== payload.sub) {
2106
- return setApiResponse3(HTTP3.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
2171
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
2107
2172
  }
2108
2173
  const pair = issueTokenPair(user.id, resolvedRefresh, {
2109
2174
  loginMethod: payload.loginMethod ?? "email",
@@ -2116,7 +2181,7 @@ function authController(store, config, bus, locales = defineLocales()) {
2116
2181
  if (await sessions.rotate(found.row, token, next, found.kind)) issued = pair;
2117
2182
  }
2118
2183
  if (!issued) {
2119
- return setApiResponse3(HTTP3.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2184
+ return setApiResponse4(HTTP4.UNAUTHORIZED, "TOKEN_REFRESH_FAILED", "Session expired or already revoked");
2120
2185
  }
2121
2186
  const { accessToken, refreshToken } = issued;
2122
2187
  return Response.json(
@@ -2135,16 +2200,16 @@ function authController(store, config, bus, locales = defineLocales()) {
2135
2200
  const body = ctx.meta["body"];
2136
2201
  const rawEmail = body?.["email"];
2137
2202
  if (typeof rawEmail !== "string") {
2138
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
2203
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
2139
2204
  }
2140
2205
  const email2 = normalizeEmailSafe(rawEmail);
2141
2206
  if (!email2) {
2142
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
2207
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
2143
2208
  }
2144
2209
  const user = await users.findByEmail(email2);
2145
2210
  if (!user) {
2146
- return setApiResponse3(
2147
- HTTP3.OK,
2211
+ return setApiResponse4(
2212
+ HTTP4.OK,
2148
2213
  "PASSWORD_RESET_EMAIL_SENT",
2149
2214
  "Password reset email sent (if account exists)."
2150
2215
  );
@@ -2153,8 +2218,8 @@ function authController(store, config, bus, locales = defineLocales()) {
2153
2218
  const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
2154
2219
  const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
2155
2220
  if (remaining > 0) {
2156
- return setApiResponse3(
2157
- HTTP3.OK,
2221
+ return setApiResponse4(
2222
+ HTTP4.OK,
2158
2223
  "PASSWORD_RESET_EMAIL_SENT",
2159
2224
  "Password reset email sent (if account exists)."
2160
2225
  );
@@ -2171,8 +2236,8 @@ function authController(store, config, bus, locales = defineLocales()) {
2171
2236
  recipient: { email: email2, phone: null, deviceToken: null },
2172
2237
  data: { pin, token, resetUrl }
2173
2238
  }));
2174
- return setApiResponse3(
2175
- HTTP3.OK,
2239
+ return setApiResponse4(
2240
+ HTTP4.OK,
2176
2241
  "PASSWORD_RESET_EMAIL_SENT",
2177
2242
  "Password reset email sent (if account exists)."
2178
2243
  );
@@ -2183,11 +2248,11 @@ function authController(store, config, bus, locales = defineLocales()) {
2183
2248
  const password2 = body?.["password"];
2184
2249
  const rawToken = body?.["token"];
2185
2250
  if (typeof password2 !== "string") {
2186
- return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
2251
+ return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
2187
2252
  }
2188
2253
  if (password2.length < 8) {
2189
- return setApiResponse3(
2190
- HTTP3.UNPROCESSABLE,
2254
+ return setApiResponse4(
2255
+ HTTP4.UNPROCESSABLE,
2191
2256
  "INVALID_PARAMETER",
2192
2257
  "password must be at least 8 characters"
2193
2258
  );
@@ -2197,8 +2262,8 @@ function authController(store, config, bus, locales = defineLocales()) {
2197
2262
  row = await passwordReset.findByToken(rawToken.trim());
2198
2263
  } else {
2199
2264
  if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
2200
- return setApiResponse3(
2201
- HTTP3.UNPROCESSABLE,
2265
+ return setApiResponse4(
2266
+ HTTP4.UNPROCESSABLE,
2202
2267
  "INVALID_PARAMETER",
2203
2268
  "a 6-digit pin or a reset token is required"
2204
2269
  );
@@ -2206,33 +2271,41 @@ function authController(store, config, bus, locales = defineLocales()) {
2206
2271
  row = await passwordReset.findByPin(raw.trim());
2207
2272
  }
2208
2273
  if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
2209
- return setApiResponse3(
2210
- HTTP3.BAD_REQUEST,
2274
+ return setApiResponse4(
2275
+ HTTP4.BAD_REQUEST,
2211
2276
  "PASSWORD_RESET_FAILED",
2212
2277
  "Invalid or expired reset credentials"
2213
2278
  );
2214
2279
  }
2215
2280
  const passwordHash = await hashPassword(password2);
2216
- await store.transaction(async (tx) => {
2217
- await Promise.all([
2218
- tx.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
2219
- passwordHash,
2220
- row.userId
2221
- ]),
2222
- tx.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [row.userId]),
2223
- // Revoke every session: the reset exists because the account may
2224
- // be compromised — a stolen session must not survive it.
2225
- tx.query(`DELETE FROM fonderie_sessions WHERE user_id = $1`, [row.userId])
2226
- ]);
2227
- });
2228
- return setApiResponse3(HTTP3.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
2281
+ const [changed] = await store.query(
2282
+ `WITH changed AS (
2283
+ UPDATE fonderie_users SET password_hash = $1, updated_at = now()
2284
+ WHERE id = $2 AND deleted_at IS NULL
2285
+ RETURNING id
2286
+ ), spent AS (
2287
+ DELETE FROM fonderie_password_resets WHERE user_id = $2
2288
+ ), revoked AS (
2289
+ DELETE FROM fonderie_sessions WHERE user_id IN (SELECT id FROM changed)
2290
+ )
2291
+ SELECT id FROM changed`,
2292
+ [passwordHash, row.userId]
2293
+ );
2294
+ if (!changed) {
2295
+ return setApiResponse4(
2296
+ HTTP4.BAD_REQUEST,
2297
+ "PASSWORD_RESET_FAILED",
2298
+ "Invalid or expired reset credentials"
2299
+ );
2300
+ }
2301
+ return setApiResponse4(HTTP4.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
2229
2302
  },
2230
2303
  verify: async (ctx) => {
2231
2304
  if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
2232
- return setApiResponse3(HTTP3.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
2305
+ return setApiResponse4(HTTP4.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
2233
2306
  }
2234
2307
  if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
2235
- return setApiResponse3(HTTP3.OK, "VERIFIED", "Email verified successfully.", {
2308
+ return setApiResponse4(HTTP4.OK, "VERIFIED", "Email verified successfully.", {
2236
2309
  verified: true,
2237
2310
  email: ctx.user.email
2238
2311
  });
@@ -2240,8 +2313,8 @@ function authController(store, config, bus, locales = defineLocales()) {
2240
2313
  const body = ctx.meta["body"];
2241
2314
  const raw = body?.["token"];
2242
2315
  if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
2243
- return setApiResponse3(
2244
- HTTP3.UNPROCESSABLE,
2316
+ return setApiResponse4(
2317
+ HTTP4.UNPROCESSABLE,
2245
2318
  "INVALID_PARAMETER",
2246
2319
  "token must be a 6-digit code"
2247
2320
  );
@@ -2259,7 +2332,7 @@ function authController(store, config, bus, locales = defineLocales()) {
2259
2332
  failureReason: "invalid_pin",
2260
2333
  ...phoneMeta
2261
2334
  });
2262
- return setApiResponse3(HTTP3.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
2335
+ return setApiResponse4(HTTP4.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
2263
2336
  }
2264
2337
  if (/* @__PURE__ */ new Date() > record.expiresAt) {
2265
2338
  await phoneVerif.deleteByUser(ctx.user.id);
@@ -2271,8 +2344,8 @@ function authController(store, config, bus, locales = defineLocales()) {
2271
2344
  failureReason: "expired_pin",
2272
2345
  ...phoneMeta
2273
2346
  });
2274
- return setApiResponse3(
2275
- HTTP3.BAD_REQUEST,
2347
+ return setApiResponse4(
2348
+ HTTP4.BAD_REQUEST,
2276
2349
  "VERIFICATION_FAILED",
2277
2350
  "Verification code expired"
2278
2351
  );
@@ -2292,11 +2365,11 @@ function authController(store, config, bus, locales = defineLocales()) {
2292
2365
  });
2293
2366
  const verifiedUser = await users.findById(ctx.user.id);
2294
2367
  if (!verifiedUser) {
2295
- return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "User not found");
2368
+ return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "User not found");
2296
2369
  }
2297
2370
  if (verifiedUser.suspended) {
2298
- return setApiResponse3(
2299
- HTTP3.FORBIDDEN,
2371
+ return setApiResponse4(
2372
+ HTTP4.FORBIDDEN,
2300
2373
  "ACCOUNT_SUSPENDED",
2301
2374
  "Account suspended. Please contact support."
2302
2375
  );
@@ -2318,10 +2391,10 @@ function authController(store, config, bus, locales = defineLocales()) {
2318
2391
  }
2319
2392
  const row = await emailVerif.findByUser(ctx.user.id, pin);
2320
2393
  if (!row) {
2321
- return setApiResponse3(HTTP3.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
2394
+ return setApiResponse4(HTTP4.BAD_REQUEST, "VERIFICATION_FAILED", "Invalid or expired pin");
2322
2395
  }
2323
2396
  if (/* @__PURE__ */ new Date() > row.expiresAt) {
2324
- return setApiResponse3(HTTP3.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
2397
+ return setApiResponse4(HTTP4.BAD_REQUEST, "VERIFICATION_FAILED", "Pin expired");
2325
2398
  }
2326
2399
  await store.transaction(async (tx) => {
2327
2400
  await Promise.all([
@@ -2335,30 +2408,30 @@ function authController(store, config, bus, locales = defineLocales()) {
2335
2408
  ])
2336
2409
  ]);
2337
2410
  });
2338
- return setApiResponse3(HTTP3.OK, "VERIFIED", "Email verified successfully.", {
2411
+ return setApiResponse4(HTTP4.OK, "VERIFIED", "Email verified successfully.", {
2339
2412
  verified: true,
2340
2413
  email: ctx.user.email
2341
2414
  });
2342
2415
  },
2343
2416
  sendVerification: async (ctx) => {
2344
2417
  if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
2345
- return setApiResponse3(HTTP3.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
2418
+ return setApiResponse4(HTTP4.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
2346
2419
  }
2347
2420
  const resolved = { ...config, ...config.resolve?.(ctx) };
2348
2421
  const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
2349
2422
  if (ctx.user.loginMethod === "phone") {
2350
2423
  const phone2 = ctx.user.phone;
2351
2424
  if (!phone2) {
2352
- return setApiResponse3(
2353
- HTTP3.BAD_REQUEST,
2425
+ return setApiResponse4(
2426
+ HTTP4.BAD_REQUEST,
2354
2427
  "NO_PHONE_ON_ACCOUNT",
2355
2428
  "No phone number associated with this account"
2356
2429
  );
2357
2430
  }
2358
2431
  const remaining2 = checkCooldown(await phoneVerif.findLastSentAt(ctx.user.id), cooldown);
2359
2432
  if (remaining2 > 0) {
2360
- return setApiResponse3(
2361
- HTTP3.TOO_MANY_REQUESTS,
2433
+ return setApiResponse4(
2434
+ HTTP4.TOO_MANY_REQUESTS,
2362
2435
  "VERIFICATION_COOLDOWN",
2363
2436
  "Please wait before requesting a new code.",
2364
2437
  {
@@ -2375,29 +2448,29 @@ function authController(store, config, bus, locales = defineLocales()) {
2375
2448
  data: { otp },
2376
2449
  recipient: { email: null, phone: phone2, deviceToken: null }
2377
2450
  }));
2378
- return setApiResponse3(
2379
- HTTP3.OK,
2451
+ return setApiResponse4(
2452
+ HTTP4.OK,
2380
2453
  "VERIFICATION_SENT",
2381
2454
  "A verification code has been sent to your phone."
2382
2455
  );
2383
2456
  }
2384
2457
  if (!ctx.user.email) {
2385
- return setApiResponse3(
2386
- HTTP3.BAD_REQUEST,
2458
+ return setApiResponse4(
2459
+ HTTP4.BAD_REQUEST,
2387
2460
  "NO_EMAIL_ON_ACCOUNT",
2388
2461
  "No email address associated with this account"
2389
2462
  );
2390
2463
  }
2391
2464
  if (ctx.user.emailVerifiedAt) {
2392
- return setApiResponse3(HTTP3.OK, "EMAIL_VERIFIED", "Email already verified.", {
2465
+ return setApiResponse4(HTTP4.OK, "EMAIL_VERIFIED", "Email already verified.", {
2393
2466
  verified: true,
2394
2467
  email: ctx.user.email
2395
2468
  });
2396
2469
  }
2397
2470
  const remaining = checkCooldown(await emailVerif.findLastSentAt(ctx.user.id), cooldown);
2398
2471
  if (remaining > 0) {
2399
- return setApiResponse3(
2400
- HTTP3.TOO_MANY_REQUESTS,
2472
+ return setApiResponse4(
2473
+ HTTP4.TOO_MANY_REQUESTS,
2401
2474
  "VERIFICATION_COOLDOWN",
2402
2475
  "Please wait before requesting a new code.",
2403
2476
  {
@@ -2414,7 +2487,7 @@ function authController(store, config, bus, locales = defineLocales()) {
2414
2487
  recipient: { email: ctx.user.email, phone: null, deviceToken: null },
2415
2488
  data: { pin }
2416
2489
  }));
2417
- return setApiResponse3(HTTP3.OK, "VERIFICATION_SENT", "Verification email sent.", {
2490
+ return setApiResponse4(HTTP4.OK, "VERIFICATION_SENT", "Verification email sent.", {
2418
2491
  email: ctx.user.email
2419
2492
  });
2420
2493
  }
@@ -2423,7 +2496,7 @@ function authController(store, config, bus, locales = defineLocales()) {
2423
2496
 
2424
2497
  // src/controllers/user.controller.ts
2425
2498
  import { randomInt as randomInt2 } from "crypto";
2426
- import { setApiResponse as setApiResponse4, HTTP as HTTP4, dateOrEmpty as dateOrEmpty2, background as background4 } from "@fonderie/core";
2499
+ import { setApiResponse as setApiResponse5, HTTP as HTTP5, dateOrEmpty as dateOrEmpty2, background as background4 } from "@fonderie/core";
2427
2500
  import { NOTIFICATION_EVENT as NOTIFICATION_EVENT3 } from "@fonderie/events";
2428
2501
 
2429
2502
  // src/dtos/login-activity.ts
@@ -2478,9 +2551,9 @@ function userController(store, config, bus) {
2478
2551
  me: async (ctx) => {
2479
2552
  const user = await users.findById(ctx.user.id);
2480
2553
  if (!user) {
2481
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "User not found");
2554
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "User not found");
2482
2555
  }
2483
- return setApiResponse4(HTTP4.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
2556
+ return setApiResponse5(HTTP5.OK, "USER_ACCOUNT_FETCHED", "User account fetched successful.", {
2484
2557
  user: toUserDTO(user, ctx.user.phoneVerified)
2485
2558
  });
2486
2559
  },
@@ -2491,14 +2564,14 @@ function userController(store, config, bus) {
2491
2564
  const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
2492
2565
  const outcomeParam = params.get("outcome");
2493
2566
  if (outcomeParam !== null && outcomeParam !== "success" && outcomeParam !== "failed") {
2494
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
2567
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "outcome must be 'success' or 'failed'");
2495
2568
  }
2496
2569
  const cursorParam = params.get("cursor");
2497
2570
  let cursor = null;
2498
2571
  if (cursorParam) {
2499
2572
  cursor = decodeLoginCursor(cursorParam);
2500
2573
  if (!cursor) {
2501
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
2574
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
2502
2575
  }
2503
2576
  }
2504
2577
  const query = { userId: ctx.user.id, limit };
@@ -2509,8 +2582,8 @@ function userController(store, config, bus) {
2509
2582
  if (from) query.from = new Date(from);
2510
2583
  if (to) query.to = new Date(to);
2511
2584
  const page = await loginEvents.listByUser(query);
2512
- return setApiResponse4(
2513
- HTTP4.OK,
2585
+ return setApiResponse5(
2586
+ HTTP5.OK,
2514
2587
  "LOGIN_HISTORY_FETCHED",
2515
2588
  "Login history retrieved.",
2516
2589
  toLoginHistoryPageDTO(page)
@@ -2520,27 +2593,27 @@ function userController(store, config, bus) {
2520
2593
  listSessions: async (ctx) => {
2521
2594
  const rows = await sessions.listLiveByUser(ctx.user.id);
2522
2595
  const currentSid = ctx.user.sid ?? null;
2523
- return setApiResponse4(HTTP4.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
2596
+ return setApiResponse5(HTTP5.OK, "SESSIONS_FETCHED", "Active sessions retrieved.", {
2524
2597
  sessions: rows.map((r) => toSessionDTO(r, currentSid))
2525
2598
  });
2526
2599
  },
2527
2600
  terminateSession: async (ctx) => {
2528
2601
  const id = ctx.meta["params"]?.["id"];
2529
2602
  if (!id) {
2530
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Session not found");
2603
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Session not found");
2531
2604
  }
2532
2605
  const removed = await sessions.terminateByIdReturningSid(ctx.user.id, id);
2533
2606
  if (removed === void 0) {
2534
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "Session not found");
2607
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "Session not found");
2535
2608
  }
2536
2609
  await background4(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: [removed], reason: "terminated" }));
2537
- return setApiResponse4(HTTP4.OK, "SESSION_TERMINATED", "Session terminated.", { id });
2610
+ return setApiResponse5(HTTP5.OK, "SESSION_TERMINATED", "Session terminated.", { id });
2538
2611
  },
2539
2612
  terminateOtherSessions: async (ctx) => {
2540
2613
  const currentSid = ctx.user.sid ?? null;
2541
2614
  if (!currentSid) {
2542
- return setApiResponse4(
2543
- HTTP4.UNPROCESSABLE,
2615
+ return setApiResponse5(
2616
+ HTTP5.UNPROCESSABLE,
2544
2617
  "NO_CURRENT_SESSION",
2545
2618
  "Cannot terminate others without a current session context"
2546
2619
  );
@@ -2548,7 +2621,7 @@ function userController(store, config, bus) {
2548
2621
  const sids = await sessions.terminateOthersReturningSids(ctx.user.id, currentSid);
2549
2622
  const count = sids.length;
2550
2623
  if (count) await background4(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids, reason: "terminated" }));
2551
- return setApiResponse4(HTTP4.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
2624
+ return setApiResponse5(HTTP5.OK, "SESSIONS_TERMINATED", "Other sessions terminated.", { count });
2552
2625
  },
2553
2626
  updateProfile: async (ctx) => {
2554
2627
  const body = ctx.meta["body"];
@@ -2558,18 +2631,18 @@ function userController(store, config, bus) {
2558
2631
  if (body?.[key] !== void 0) fields[key] = body[key];
2559
2632
  }
2560
2633
  if (Object.keys(fields).length === 0) {
2561
- return setApiResponse4(
2562
- HTTP4.UNPROCESSABLE,
2634
+ return setApiResponse5(
2635
+ HTTP5.UNPROCESSABLE,
2563
2636
  "INVALID_PARAMETER",
2564
2637
  "Provide at least one of: firstName, lastName, avatarUrl"
2565
2638
  );
2566
2639
  }
2567
2640
  const row = await users.update(ctx.user.id, fields);
2568
2641
  if (!row) {
2569
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "User not found");
2642
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "User not found");
2570
2643
  }
2571
2644
  const updated = await users.findById(ctx.user.id);
2572
- return setApiResponse4(HTTP4.OK, "PROFILE_UPDATED", "Profile updated.", {
2645
+ return setApiResponse5(HTTP5.OK, "PROFILE_UPDATED", "Profile updated.", {
2573
2646
  user: toUserDTO(updated, ctx.user.phoneVerified)
2574
2647
  });
2575
2648
  },
@@ -2586,14 +2659,14 @@ function userController(store, config, bus) {
2586
2659
  if (Object.keys(patch).length > 0) fields.patch = patch;
2587
2660
  const row = await users.updatePreferences(ctx.user.id, fields);
2588
2661
  if (!row) {
2589
- return setApiResponse4(
2590
- HTTP4.UNPROCESSABLE,
2662
+ return setApiResponse5(
2663
+ HTTP5.UNPROCESSABLE,
2591
2664
  "INVALID_PARAMETER",
2592
2665
  "Provide at least one preference field"
2593
2666
  );
2594
2667
  }
2595
2668
  const updated = await users.findById(ctx.user.id);
2596
- return setApiResponse4(HTTP4.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
2669
+ return setApiResponse5(HTTP5.OK, "PREFERENCES_UPDATED", "Preferences updated.", {
2597
2670
  user: toUserDTO(updated, ctx.user.phoneVerified)
2598
2671
  });
2599
2672
  },
@@ -2602,23 +2675,23 @@ function userController(store, config, bus) {
2602
2675
  const newEmail = body?.["email"];
2603
2676
  const normalised = typeof newEmail === "string" ? normalizeEmailSafe(newEmail) : null;
2604
2677
  if (!normalised) {
2605
- return setApiResponse4(
2606
- HTTP4.UNPROCESSABLE,
2678
+ return setApiResponse5(
2679
+ HTTP5.UNPROCESSABLE,
2607
2680
  "INVALID_PARAMETER",
2608
2681
  "A valid email address is required"
2609
2682
  );
2610
2683
  }
2611
2684
  const oldEmail = ctx.user.email;
2612
2685
  if (normalised === oldEmail) {
2613
- return setApiResponse4(
2614
- HTTP4.UNPROCESSABLE,
2686
+ return setApiResponse5(
2687
+ HTTP5.UNPROCESSABLE,
2615
2688
  "INVALID_PARAMETER",
2616
2689
  "New email must differ from current email"
2617
2690
  );
2618
2691
  }
2619
2692
  const existing = await users.findByEmail(normalised);
2620
2693
  if (existing) {
2621
- return setApiResponse4(HTTP4.CONFLICT, "EMAIL_IN_USE", "Email already in use");
2694
+ return setApiResponse5(HTTP5.CONFLICT, "EMAIL_IN_USE", "Email already in use");
2622
2695
  }
2623
2696
  const pin = randomInt2(1e5, 1e6).toString();
2624
2697
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
@@ -2638,8 +2711,8 @@ function userController(store, config, bus) {
2638
2711
  recipient: { email: oldEmail, phone: null, deviceToken: null }
2639
2712
  }));
2640
2713
  }
2641
- return setApiResponse4(
2642
- HTTP4.OK,
2714
+ return setApiResponse5(
2715
+ HTTP5.OK,
2643
2716
  "EMAIL_UPDATED",
2644
2717
  "Email updated. A verification code has been sent to your new address.",
2645
2718
  {
@@ -2651,8 +2724,8 @@ function userController(store, config, bus) {
2651
2724
  const body = ctx.meta["body"];
2652
2725
  const newPhone = body?.["phone"];
2653
2726
  if (!isValidPhone2(newPhone)) {
2654
- return setApiResponse4(
2655
- HTTP4.UNPROCESSABLE,
2727
+ return setApiResponse5(
2728
+ HTTP5.UNPROCESSABLE,
2656
2729
  "INVALID_PARAMETER",
2657
2730
  "A valid phone number is required"
2658
2731
  );
@@ -2660,7 +2733,7 @@ function userController(store, config, bus) {
2660
2733
  const normalised = normalizePhone2(newPhone);
2661
2734
  const existing = await users.findByPhone(normalised);
2662
2735
  if (existing) {
2663
- return setApiResponse4(HTTP4.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
2736
+ return setApiResponse5(HTTP5.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
2664
2737
  }
2665
2738
  const otp = randomInt2(1e5, 1e6).toString();
2666
2739
  const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
@@ -2680,8 +2753,8 @@ function userController(store, config, bus) {
2680
2753
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
2681
2754
  }));
2682
2755
  }
2683
- return setApiResponse4(
2684
- HTTP4.OK,
2756
+ return setApiResponse5(
2757
+ HTTP5.OK,
2685
2758
  "PHONE_UPDATED",
2686
2759
  "Phone number updated. A verification code has been sent to your new number.",
2687
2760
  {
@@ -2694,23 +2767,23 @@ function userController(store, config, bus) {
2694
2767
  const currentPassword = body?.["currentPassword"];
2695
2768
  const newPassword = body?.["newPassword"];
2696
2769
  if (typeof currentPassword !== "string" || !currentPassword) {
2697
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
2770
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "currentPassword is required");
2698
2771
  }
2699
2772
  if (typeof newPassword !== "string" || newPassword.length < 8) {
2700
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
2773
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "newPassword must be at least 8 characters");
2701
2774
  }
2702
2775
  const user = await users.findById(ctx.user.id);
2703
- if (!user) return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "User not found");
2776
+ if (!user) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "User not found");
2704
2777
  const { hashPassword: hashPassword2, verifyPassword: verifyPassword2 } = await Promise.resolve().then(() => (init_password(), password_exports));
2705
2778
  const valid = await verifyPassword2(currentPassword, user.passwordHash ?? "");
2706
2779
  if (!valid) {
2707
- return setApiResponse4(HTTP4.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
2780
+ return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_CREDENTIAL", "Current password is incorrect");
2708
2781
  }
2709
2782
  const hash = await hashPassword2(newPassword);
2710
2783
  await users.updatePassword(ctx.user.id, hash);
2711
2784
  await sessions.deleteByUser(ctx.user.id);
2712
2785
  await background4(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: ctx.user.id, sids: null, reason: "password-changed" }));
2713
- return setApiResponse4(HTTP4.OK, "PASSWORD_CHANGED", "Password updated successfully.");
2786
+ return setApiResponse5(HTTP5.OK, "PASSWORD_CHANGED", "Password updated successfully.");
2714
2787
  },
2715
2788
  /**
2716
2789
  * Remove an OAuth provider from the account.
@@ -2729,19 +2802,19 @@ function userController(store, config, bus) {
2729
2802
  const provider = String(ctx.meta["params"]?.["provider"] ?? "");
2730
2803
  const current = await users.findById(ctx.user.id);
2731
2804
  if (!current?.provider) {
2732
- return setApiResponse4(HTTP4.NOT_FOUND, "NOT_LINKED", "No sign-in provider is linked.");
2805
+ return setApiResponse5(HTTP5.NOT_FOUND, "NOT_LINKED", "No sign-in provider is linked.");
2733
2806
  }
2734
2807
  if (provider && provider.toLowerCase() !== current.provider.toLowerCase()) {
2735
- return setApiResponse4(
2736
- HTTP4.NOT_FOUND,
2808
+ return setApiResponse5(
2809
+ HTTP5.NOT_FOUND,
2737
2810
  "NOT_LINKED",
2738
2811
  `This account is not linked to ${provider}.`
2739
2812
  );
2740
2813
  }
2741
2814
  const removed = await users.clearProvider(ctx.user.id);
2742
2815
  if (!removed) {
2743
- return setApiResponse4(
2744
- HTTP4.CONFLICT,
2816
+ return setApiResponse5(
2817
+ HTTP5.CONFLICT,
2745
2818
  "PASSWORD_REQUIRED",
2746
2819
  "Set a password before disconnecting this provider \u2014 it is currently the only way to sign in."
2747
2820
  );
@@ -2754,12 +2827,18 @@ function userController(store, config, bus) {
2754
2827
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
2755
2828
  }));
2756
2829
  }
2757
- return setApiResponse4(HTTP4.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
2830
+ return setApiResponse5(HTTP5.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
2758
2831
  },
2759
2832
  deleteMe: async (ctx) => {
2760
2833
  const userId = ctx.user.id;
2761
2834
  await users.softDelete(userId);
2762
2835
  await sessions.deleteByUser(userId);
2836
+ await store.query(
2837
+ `WITH resets AS (DELETE FROM fonderie_password_resets WHERE user_id = $1),
2838
+ emails AS (DELETE FROM fonderie_email_verifications WHERE user_id = $1)
2839
+ DELETE FROM fonderie_phone_verifications WHERE user_id = $1`,
2840
+ [userId]
2841
+ );
2763
2842
  const reqId = ctx.meta["requestId"];
2764
2843
  await background4(bus?.emit(
2765
2844
  EVENT_KEYS.userDeleted,
@@ -2780,7 +2859,7 @@ function userController(store, config, bus) {
2780
2859
  // no MFA secret, no session tokens (session metadata only).
2781
2860
  exportMe: async (ctx) => {
2782
2861
  const user = await users.findById(ctx.user.id);
2783
- if (!user) return setApiResponse4(HTTP4.NOT_FOUND, "NOT_FOUND", "User not found");
2862
+ if (!user) return setApiResponse5(HTTP5.NOT_FOUND, "NOT_FOUND", "User not found");
2784
2863
  const sessionRows = await sessions.listByUser(ctx.user.id);
2785
2864
  const bundle = {
2786
2865
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2809,7 +2888,7 @@ function userController(store, config, bus) {
2809
2888
  }
2810
2889
  bundle["modules"] = modules;
2811
2890
  }
2812
- return setApiResponse4(HTTP4.OK, "DATA_EXPORT", "Your account data.", bundle);
2891
+ return setApiResponse5(HTTP5.OK, "DATA_EXPORT", "Your account data.", bundle);
2813
2892
  }
2814
2893
  };
2815
2894
  }
@@ -2817,7 +2896,7 @@ function userController(store, config, bus) {
2817
2896
  // src/controllers/oauth.controller.ts
2818
2897
  import { randomBytes as randomBytes4, createHash as createHash4 } from "crypto";
2819
2898
  import jwt2 from "jsonwebtoken";
2820
- import { setApiResponse as setApiResponse5, HTTP as HTTP5, constantTimeEqual as constantTimeEqual2, background as background5 } from "@fonderie/core";
2899
+ import { setApiResponse as setApiResponse6, HTTP as HTTP6, constantTimeEqual as constantTimeEqual2, background as background5 } from "@fonderie/core";
2821
2900
  import { NOTIFICATION_EVENT as NOTIFICATION_EVENT4 } from "@fonderie/events";
2822
2901
 
2823
2902
  // src/services/jwks.ts
@@ -3027,14 +3106,29 @@ function oauthController(store, config, bus) {
3027
3106
  const method = provider === "apple" ? "oauth-apple" : "oauth-google";
3028
3107
  const meta = requestMeta(ctx);
3029
3108
  if (!claims.email) {
3030
- return setApiResponse5(HTTP5.BAD_REQUEST, p.failed, `No email in ${p.label} identity token`);
3109
+ return setApiResponse6(HTTP6.BAD_REQUEST, p.failed, `No email in ${p.label} identity token`);
3031
3110
  }
3032
3111
  if (!claims.emailVerified) {
3033
- return setApiResponse5(HTTP5.BAD_REQUEST, p.failed, `${p.label} account email is not verified`);
3112
+ return setApiResponse6(HTTP6.BAD_REQUEST, p.failed, `${p.label} account email is not verified`);
3034
3113
  }
3035
3114
  const normalizedEmail = normalizeEmailSafe(claims.email);
3036
3115
  if (!normalizedEmail) {
3037
- return setApiResponse5(HTTP5.BAD_REQUEST, p.failed, `Invalid email in ${p.label} identity token`);
3116
+ return setApiResponse6(HTTP6.BAD_REQUEST, p.failed, `Invalid email in ${p.label} identity token`);
3117
+ }
3118
+ const archived = await users.findArchivedByEmail(normalizedEmail);
3119
+ if (archived?.deletedAt) {
3120
+ await loginEvents.recordSafe({
3121
+ userId: archived.id,
3122
+ emailAttempted: normalizedEmail,
3123
+ method,
3124
+ outcome: "failed",
3125
+ failureReason: "pending_deletion",
3126
+ ...meta
3127
+ });
3128
+ const res = pendingDeletionResponse(archived.deletedAt, config);
3129
+ const headers = cookieHeaders(clearCookies);
3130
+ headers.set("content-type", res.headers.get("content-type") ?? "application/json");
3131
+ return new Response(res.body, { status: res.status, headers });
3038
3132
  }
3039
3133
  const upserted = await users.upsertByProvider(normalizedEmail, provider, claims.sub ?? "");
3040
3134
  if (!upserted) {
@@ -3046,15 +3140,15 @@ function oauthController(store, config, bus) {
3046
3140
  failureReason: "provider_upsert_failed",
3047
3141
  ...meta
3048
3142
  });
3049
- return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
3143
+ return setApiResponse6(HTTP6.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
3050
3144
  }
3051
3145
  const fullUser = await users.findById(upserted.id);
3052
3146
  if (!fullUser) {
3053
- return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
3147
+ return setApiResponse6(HTTP6.SERVER_ERROR, "SERVER_ERROR", `${p.label} login failed`);
3054
3148
  }
3055
3149
  await announceOAuthUpsert(ctx, upserted, provider, fullUser);
3056
3150
  if (fullUser.suspended) {
3057
- return setApiResponse5(HTTP5.FORBIDDEN, "ACCOUNT_SUSPENDED", "Account suspended. Please contact support.");
3151
+ return setApiResponse6(HTTP6.FORBIDDEN, "ACCOUNT_SUSPENDED", "Account suspended. Please contact support.");
3058
3152
  }
3059
3153
  if (fullUser.mfaEnabled) {
3060
3154
  const mfaToken = issueMfaPendingToken(upserted.id, config, provider);
@@ -3097,8 +3191,8 @@ function oauthController(store, config, bus) {
3097
3191
  googleInit: async (_ctx) => {
3098
3192
  const google = config.google;
3099
3193
  if (!google || !google.clientSecret || !google.redirectUri) {
3100
- return setApiResponse5(
3101
- HTTP5.NOT_IMPLEMENTED,
3194
+ return setApiResponse6(
3195
+ HTTP6.NOT_IMPLEMENTED,
3102
3196
  "NOT_CONFIGURED",
3103
3197
  "Google OAuth not configured"
3104
3198
  );
@@ -3132,8 +3226,8 @@ function oauthController(store, config, bus) {
3132
3226
  googleCallback: async (ctx) => {
3133
3227
  const google = config.google;
3134
3228
  if (!google || !google.clientSecret || !google.redirectUri) {
3135
- return setApiResponse5(
3136
- HTTP5.NOT_IMPLEMENTED,
3229
+ return setApiResponse6(
3230
+ HTTP6.NOT_IMPLEMENTED,
3137
3231
  "NOT_CONFIGURED",
3138
3232
  "Google OAuth not configured"
3139
3233
  );
@@ -3141,14 +3235,14 @@ function oauthController(store, config, bus) {
3141
3235
  const url = new URL(ctx.request.url);
3142
3236
  const code = url.searchParams.get("code");
3143
3237
  if (!code) {
3144
- return setApiResponse5(HTTP5.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
3238
+ return setApiResponse6(HTTP6.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
3145
3239
  }
3146
3240
  const returnedState = url.searchParams.get("state") ?? "";
3147
3241
  const cookieHeader = ctx.request.headers.get("cookie") ?? "";
3148
3242
  const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
3149
3243
  if (!returnedState || !expectedState || !constantTimeEqual2(Buffer.from(returnedState), Buffer.from(expectedState))) {
3150
- return setApiResponse5(
3151
- HTTP5.BAD_REQUEST,
3244
+ return setApiResponse6(
3245
+ HTTP6.BAD_REQUEST,
3152
3246
  "GOOGLE_AUTH_FAILED",
3153
3247
  "OAuth state mismatch \u2014 restart the sign-in flow"
3154
3248
  );
@@ -3166,8 +3260,8 @@ function oauthController(store, config, bus) {
3166
3260
  });
3167
3261
  const tokenData = await tokenRes.json();
3168
3262
  if (!tokenData.id_token) {
3169
- return setApiResponse5(
3170
- HTTP5.BAD_REQUEST,
3263
+ return setApiResponse6(
3264
+ HTTP6.BAD_REQUEST,
3171
3265
  "GOOGLE_AUTH_FAILED",
3172
3266
  "OAuth token exchange failed"
3173
3267
  );
@@ -3176,20 +3270,20 @@ function oauthController(store, config, bus) {
3176
3270
  Buffer.from(tokenData.id_token.split(".")[1] ?? "", "base64url").toString()
3177
3271
  );
3178
3272
  if (payload.aud !== google.clientId) {
3179
- return setApiResponse5(HTTP5.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
3273
+ return setApiResponse6(HTTP6.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token audience mismatch");
3180
3274
  }
3181
3275
  if (payload.iss !== "https://accounts.google.com" && payload.iss !== "accounts.google.com") {
3182
- return setApiResponse5(HTTP5.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
3276
+ return setApiResponse6(HTTP6.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token issuer mismatch");
3183
3277
  }
3184
3278
  if (typeof payload.exp !== "number" || payload.exp * 1e3 < Date.now()) {
3185
- return setApiResponse5(HTTP5.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
3279
+ return setApiResponse6(HTTP6.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "OAuth token expired");
3186
3280
  }
3187
3281
  if (!payload.email) {
3188
- return setApiResponse5(HTTP5.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
3282
+ return setApiResponse6(HTTP6.BAD_REQUEST, "GOOGLE_AUTH_FAILED", "No email in OAuth response");
3189
3283
  }
3190
3284
  if (payload.email_verified !== true) {
3191
- return setApiResponse5(
3192
- HTTP5.BAD_REQUEST,
3285
+ return setApiResponse6(
3286
+ HTTP6.BAD_REQUEST,
3193
3287
  "GOOGLE_AUTH_FAILED",
3194
3288
  "Google account email is not verified"
3195
3289
  );
@@ -3203,7 +3297,7 @@ function oauthController(store, config, bus) {
3203
3297
  appleInit: async (_ctx) => {
3204
3298
  const apple = config.apple;
3205
3299
  if (!apple) {
3206
- return setApiResponse5(HTTP5.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3300
+ return setApiResponse6(HTTP6.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3207
3301
  }
3208
3302
  const state = randomBytes4(16).toString("hex");
3209
3303
  const params = new URLSearchParams({
@@ -3234,20 +3328,20 @@ function oauthController(store, config, bus) {
3234
3328
  appleCallback: async (ctx) => {
3235
3329
  const apple = config.apple;
3236
3330
  if (!apple) {
3237
- return setApiResponse5(HTTP5.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3331
+ return setApiResponse6(HTTP6.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3238
3332
  }
3239
3333
  const pre = ctx.meta["body"];
3240
3334
  const form = pre && typeof pre === "object" ? new URLSearchParams(pre) : new URLSearchParams(await ctx.request.text());
3241
3335
  const code = form.get("code");
3242
3336
  if (!code) {
3243
- return setApiResponse5(HTTP5.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
3337
+ return setApiResponse6(HTTP6.BAD_REQUEST, "INVALID_PARAMETER", "Missing code");
3244
3338
  }
3245
3339
  const returnedState = form.get("state") ?? "";
3246
3340
  const cookieHeader = ctx.request.headers.get("cookie") ?? "";
3247
3341
  const expectedState = cookieHeader.match(/(?:^|;\s*)oauth_state=([^;]+)/)?.[1] ?? "";
3248
3342
  if (!returnedState || !expectedState || !constantTimeEqual2(Buffer.from(returnedState), Buffer.from(expectedState))) {
3249
- return setApiResponse5(
3250
- HTTP5.BAD_REQUEST,
3343
+ return setApiResponse6(
3344
+ HTTP6.BAD_REQUEST,
3251
3345
  "APPLE_AUTH_FAILED",
3252
3346
  "OAuth state mismatch \u2014 restart the sign-in flow"
3253
3347
  );
@@ -3265,11 +3359,11 @@ function oauthController(store, config, bus) {
3265
3359
  });
3266
3360
  const tokenData = await tokenRes.json();
3267
3361
  if (!tokenData.id_token) {
3268
- return setApiResponse5(HTTP5.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
3362
+ return setApiResponse6(HTTP6.BAD_REQUEST, "APPLE_AUTH_FAILED", "OAuth token exchange failed");
3269
3363
  }
3270
3364
  const claims = await verifyAppleIdToken(tokenData.id_token, { audiences: [apple.clientId] });
3271
3365
  if (!claims) {
3272
- return setApiResponse5(HTTP5.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
3366
+ return setApiResponse6(HTTP6.BAD_REQUEST, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
3273
3367
  }
3274
3368
  return completeOAuthLogin(ctx, "apple", {
3275
3369
  email: claims.email,
@@ -3280,12 +3374,12 @@ function oauthController(store, config, bus) {
3280
3374
  appleNative: async (ctx) => {
3281
3375
  const apple = config.apple;
3282
3376
  if (!apple) {
3283
- return setApiResponse5(HTTP5.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3377
+ return setApiResponse6(HTTP6.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Apple OAuth not configured");
3284
3378
  }
3285
3379
  const body = ctx.meta["body"];
3286
3380
  const identityToken = body?.identityToken;
3287
3381
  if (!identityToken) {
3288
- return setApiResponse5(HTTP5.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
3382
+ return setApiResponse6(HTTP6.BAD_REQUEST, "INVALID_PARAMETER", "Missing identityToken");
3289
3383
  }
3290
3384
  const audiences = apple.nativeClientIds && apple.nativeClientIds.length ? apple.nativeClientIds : [apple.clientId];
3291
3385
  const claims = await verifyAppleIdToken(identityToken, { audiences, nonce: body?.nonce });
@@ -3299,7 +3393,7 @@ function oauthController(store, config, bus) {
3299
3393
  failureReason: "invalid_identity_token",
3300
3394
  ...meta
3301
3395
  });
3302
- return setApiResponse5(HTTP5.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
3396
+ return setApiResponse6(HTTP6.UNAUTHORIZED, "APPLE_AUTH_FAILED", "Invalid Apple identity token");
3303
3397
  }
3304
3398
  const tokenHash = createHash4("sha256").update(identityToken).digest("hex");
3305
3399
  const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
@@ -3314,8 +3408,8 @@ function oauthController(store, config, bus) {
3314
3408
  failureReason: "token_replayed",
3315
3409
  ...meta
3316
3410
  });
3317
- return setApiResponse5(
3318
- HTTP5.UNAUTHORIZED,
3411
+ return setApiResponse6(
3412
+ HTTP6.UNAUTHORIZED,
3319
3413
  "APPLE_AUTH_FAILED",
3320
3414
  "This Apple identity token has already been used"
3321
3415
  );
@@ -3332,12 +3426,12 @@ function oauthController(store, config, bus) {
3332
3426
  googleNative: async (ctx) => {
3333
3427
  const audiences = config.google?.nativeClientIds ?? [];
3334
3428
  if (!config.google || audiences.length === 0) {
3335
- return setApiResponse5(HTTP5.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Native Google sign-in not configured");
3429
+ return setApiResponse6(HTTP6.NOT_IMPLEMENTED, "NOT_CONFIGURED", "Native Google sign-in not configured");
3336
3430
  }
3337
3431
  const body = ctx.meta["body"];
3338
3432
  const idToken = body?.idToken;
3339
3433
  if (!idToken) {
3340
- return setApiResponse5(HTTP5.BAD_REQUEST, "INVALID_PARAMETER", "Missing idToken");
3434
+ return setApiResponse6(HTTP6.BAD_REQUEST, "INVALID_PARAMETER", "Missing idToken");
3341
3435
  }
3342
3436
  const claims = await verifyGoogleIdToken(idToken, { audiences, nonce: body?.nonce });
3343
3437
  if (!claims) {
@@ -3349,7 +3443,7 @@ function oauthController(store, config, bus) {
3349
3443
  failureReason: "invalid_identity_token",
3350
3444
  ...requestMeta(ctx)
3351
3445
  });
3352
- return setApiResponse5(HTTP5.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "Invalid Google ID token");
3446
+ return setApiResponse6(HTTP6.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "Invalid Google ID token");
3353
3447
  }
3354
3448
  const tokenHash = createHash4("sha256").update(idToken).digest("hex");
3355
3449
  const expMs = typeof claims.exp === "number" ? claims.exp * 1e3 : Date.now() + 5 * 60 * 1e3;
@@ -3362,7 +3456,7 @@ function oauthController(store, config, bus) {
3362
3456
  failureReason: "token_replayed",
3363
3457
  ...requestMeta(ctx)
3364
3458
  });
3365
- return setApiResponse5(HTTP5.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "This Google ID token has already been used");
3459
+ return setApiResponse6(HTTP6.UNAUTHORIZED, "GOOGLE_AUTH_FAILED", "This Google ID token has already been used");
3366
3460
  }
3367
3461
  consumedTokens.sweepExpiredSafe();
3368
3462
  return completeOAuthLogin(ctx, "google", {
@@ -3408,7 +3502,7 @@ function buildAuthRoutes(store, config, bus, locales) {
3408
3502
  "providers",
3409
3503
  "GET",
3410
3504
  "/auth/providers",
3411
- async () => setApiResponse6(HTTP6.OK, "AUTH_PROVIDERS", "Sign-in methods available here", {
3505
+ async () => setApiResponse7(HTTP7.OK, "AUTH_PROVIDERS", "Sign-in methods available here", {
3412
3506
  providers: [...config.providers]
3413
3507
  })
3414
3508
  ),
@@ -3672,7 +3766,7 @@ function extractToken(request) {
3672
3766
  }
3673
3767
 
3674
3768
  // src/admin.ts
3675
- import { HTTP as HTTP7, encodeKeysetCursor as encodeKeysetCursor2, setApiResponse as setApiResponse7 } from "@fonderie/core";
3769
+ import { HTTP as HTTP8, encodeKeysetCursor as encodeKeysetCursor2, setApiResponse as setApiResponse8 } from "@fonderie/core";
3676
3770
  import { background as background6 } from "@fonderie/core";
3677
3771
  function toAdminUserDTO(user) {
3678
3772
  return {
@@ -3689,7 +3783,7 @@ function toAdminUserPageDTO(page) {
3689
3783
  }
3690
3784
  var ADMIN_PREFIX = "/_admin";
3691
3785
  var idOf = (ctx) => ctx.meta.params?.["id"] ?? "";
3692
- var NOT_FOUND = () => setApiResponse7(HTTP7.NOT_FOUND, "NOT_FOUND", "No such user");
3786
+ var NOT_FOUND = () => setApiResponse8(HTTP8.NOT_FOUND, "NOT_FOUND", "No such user");
3693
3787
  function adminRouteTable(store, bus) {
3694
3788
  const users = new UserModel(store);
3695
3789
  const sessions = new SessionModel(store);
@@ -3698,8 +3792,8 @@ function adminRouteTable(store, bus) {
3698
3792
  const ok = await users.setSuspended(idOf(ctx), suspended);
3699
3793
  if (!ok) return NOT_FOUND();
3700
3794
  const user = await users.findById(idOf(ctx));
3701
- return user ? setApiResponse7(
3702
- HTTP7.OK,
3795
+ return user ? setApiResponse8(
3796
+ HTTP8.OK,
3703
3797
  suspended ? "USER_SUSPENDED" : "USER_UNSUSPENDED",
3704
3798
  suspended ? "User suspended" : "User unsuspended",
3705
3799
  toAdminUserDTO(user)
@@ -3718,21 +3812,21 @@ function adminRouteTable(store, bus) {
3718
3812
  const email2 = raw ? normalizeEmailSafe(raw) ?? raw.toLowerCase() : void 0;
3719
3813
  if (email2) {
3720
3814
  const user = await users.findByEmail(email2);
3721
- return user ? setApiResponse7(HTTP7.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3815
+ return user ? setApiResponse8(HTTP8.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3722
3816
  }
3723
3817
  const rawLimit = Number(params.get("limit") ?? 50);
3724
3818
  const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
3725
3819
  const cursorParam = params.get("cursor");
3726
3820
  const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
3727
3821
  if (cursorParam && !cursor)
3728
- return setApiResponse7(HTTP7.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3822
+ return setApiResponse8(HTTP8.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3729
3823
  const deleted = params.get("deleted") === "1" || params.get("deleted") === "true";
3730
3824
  const page = await users.list({
3731
3825
  limit,
3732
3826
  ...cursor ? { cursor } : {},
3733
3827
  ...deleted ? { deleted } : {}
3734
3828
  });
3735
- return setApiResponse7(HTTP7.OK, "USERS", "Users", toAdminUserPageDTO(page));
3829
+ return setApiResponse8(HTTP8.OK, "USERS", "Users", toAdminUserPageDTO(page));
3736
3830
  }
3737
3831
  ],
3738
3832
  [
@@ -3740,7 +3834,7 @@ function adminRouteTable(store, bus) {
3740
3834
  "/_admin/users/:id",
3741
3835
  async (ctx) => {
3742
3836
  const user = await users.findByIdIncludingDeleted(idOf(ctx));
3743
- return user ? setApiResponse7(HTTP7.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3837
+ return user ? setApiResponse8(HTTP8.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3744
3838
  }
3745
3839
  ],
3746
3840
  [
@@ -3749,8 +3843,8 @@ function adminRouteTable(store, bus) {
3749
3843
  async (ctx) => {
3750
3844
  if (!await users.findById(idOf(ctx))) return NOT_FOUND();
3751
3845
  const rows = await sessions.listLiveByUser(idOf(ctx));
3752
- return setApiResponse7(
3753
- HTTP7.OK,
3846
+ return setApiResponse8(
3847
+ HTTP8.OK,
3754
3848
  "SESSIONS",
3755
3849
  "Live sessions",
3756
3850
  rows.map((r) => toSessionDTO(r, null))
@@ -3765,7 +3859,7 @@ function adminRouteTable(store, bus) {
3765
3859
  if (!await users.findById(idOf(ctx))) return NOT_FOUND();
3766
3860
  await sessions.deleteByUser(idOf(ctx));
3767
3861
  await background6(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: idOf(ctx), sids: null, reason: "admin" }));
3768
- return setApiResponse7(HTTP7.OK, "SESSIONS_REVOKED", "All sessions revoked");
3862
+ return setApiResponse8(HTTP8.OK, "SESSIONS_REVOKED", "All sessions revoked");
3769
3863
  }
3770
3864
  ],
3771
3865
  [
@@ -3779,14 +3873,14 @@ function adminRouteTable(store, bus) {
3779
3873
  const cursorParam = params.get("cursor");
3780
3874
  const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
3781
3875
  if (cursorParam && !cursor)
3782
- return setApiResponse7(HTTP7.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3876
+ return setApiResponse8(HTTP8.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3783
3877
  const page = await events.listByUser({
3784
3878
  userId: idOf(ctx),
3785
3879
  limit,
3786
3880
  ...cursor ? { cursor } : {}
3787
3881
  });
3788
- return setApiResponse7(
3789
- HTTP7.OK,
3882
+ return setApiResponse8(
3883
+ HTTP8.OK,
3790
3884
  "LOGIN_HISTORY",
3791
3885
  "Login history",
3792
3886
  toLoginHistoryPageDTO(page)
@@ -3819,7 +3913,7 @@ var AuthModule = class {
3819
3913
  name = "@fonderie/auth";
3820
3914
  // Baked in at build time by tsup.base, so the operator's Modules page can
3821
3915
  // say what is actually deployed rather than 'not reported'.
3822
- version = "7.22.1";
3916
+ version = "7.23.0";
3823
3917
  // Users, sessions, login history, suspend — only through @fonderie/admin.
3824
3918
  describeAdmin() {
3825
3919
  return { routes: describeAuthAdminRoutes(this.store, this.bus) };