@fonderie/auth 7.0.1 → 7.1.1

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/brain/outcomes.md CHANGED
@@ -61,6 +61,7 @@ user_id UUID PRIMARY KEY REFERENCES fonderie_users(id) ON DELET
61
61
  expires_at TIMESTAMPTZ NOT NULL
62
62
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
63
63
  pin TEXT NOT NULL UNIQUE
64
+ token TEXT
64
65
  -- INDEX idx_fonderie_password_resets_pin (pin)
65
66
  ```
66
67
 
@@ -58,6 +58,7 @@ interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
58
58
  accessTokenDuration?: string;
59
59
  providers: ('email' | 'phone' | 'google' | 'github')[];
60
60
  appName?: string;
61
+ passwordResetUrl?: string;
61
62
  resolve?: (ctx: {
62
63
  meta: Record<string, unknown>;
63
64
  }) => Partial<IAuthRuntimeConfig>;
package/dist/index.cjs CHANGED
@@ -201,7 +201,10 @@ var loginSchema = import_zod.z.union([
201
201
  ]);
202
202
  var refreshSchema = import_zod.z.object({ refreshToken: import_zod.z.string().min(1).optional() });
203
203
  var forgotPasswordSchema = import_zod.z.object({ email });
204
- var resetPasswordSchema = import_zod.z.object({ pin: sixDigitPin, password });
204
+ var resetPasswordSchema = import_zod.z.union([
205
+ import_zod.z.object({ pin: sixDigitPin, password }),
206
+ import_zod.z.object({ token: import_zod.z.string().trim().min(32, "invalid reset token"), password })
207
+ ]);
205
208
  var verifySchema = import_zod.z.object({ token: sixDigitPin });
206
209
  var updateProfileSchema = import_zod.z.object({
207
210
  firstName: import_zod.z.string().max(100).nullable().optional(),
@@ -1146,7 +1149,7 @@ function mfaController(store, config, issuer, bus) {
1146
1149
  }
1147
1150
 
1148
1151
  // src/controllers/auth.controller.ts
1149
- var import_node_crypto4 = require("crypto");
1152
+ var import_node_crypto5 = require("crypto");
1150
1153
  var import_events2 = require("@fonderie/events");
1151
1154
  var import_core5 = require("@fonderie/core");
1152
1155
 
@@ -1193,18 +1196,20 @@ function normalizeEmailSafe(email2) {
1193
1196
  }
1194
1197
 
1195
1198
  // src/models/password-reset.model.ts
1199
+ var import_node_crypto4 = require("crypto");
1200
+ var hashSecret = (value) => (0, import_node_crypto4.createHash)("sha256").update(value).digest("hex");
1196
1201
  var PasswordResetModel = class {
1197
1202
  constructor(store) {
1198
1203
  this.store = store;
1199
1204
  }
1200
1205
  store;
1201
- async create(userId, pin, expiresAt) {
1206
+ async create(userId, pin, token, expiresAt) {
1202
1207
  await this.store.query(
1203
- `INSERT INTO fonderie_password_resets (user_id, pin, expires_at, created_at)
1204
- VALUES ($1, $2, $3, now())
1208
+ `INSERT INTO fonderie_password_resets (user_id, pin, token, expires_at, created_at)
1209
+ VALUES ($1, $2, $3, $4, now())
1205
1210
  ON CONFLICT (user_id) DO UPDATE
1206
- SET pin = $2, expires_at = $3, created_at = now()`,
1207
- [userId, pin, expiresAt]
1211
+ SET pin = $2, token = $3, expires_at = $4, created_at = now()`,
1212
+ [userId, hashSecret(pin), hashSecret(token), expiresAt]
1208
1213
  );
1209
1214
  }
1210
1215
  async findLastSentAt(userId) {
@@ -1218,7 +1223,20 @@ var PasswordResetModel = class {
1218
1223
  async findByPin(pin) {
1219
1224
  const [row] = await this.store.query(
1220
1225
  `SELECT user_id, expires_at FROM fonderie_password_resets WHERE pin = $1`,
1221
- [pin]
1226
+ [hashSecret(pin)]
1227
+ );
1228
+ if (!row) return null;
1229
+ return { userId: row.user_id, expiresAt: new Date(row.expires_at) };
1230
+ }
1231
+ // Lookup by the high-entropy reset token. Unlike the 6-digit pin this is
1232
+ // not brute-forceable, so the token path needs no rate limit. Empty/short
1233
+ // values are refused before the query so a NULL/blank token column can
1234
+ // never be matched.
1235
+ async findByToken(token) {
1236
+ if (typeof token !== "string" || token.length < 32) return null;
1237
+ const [row] = await this.store.query(
1238
+ `SELECT user_id, expires_at FROM fonderie_password_resets WHERE token = $1`,
1239
+ [hashSecret(token)]
1222
1240
  );
1223
1241
  if (!row) return null;
1224
1242
  return { userId: row.user_id, expiresAt: new Date(row.expires_at) };
@@ -1371,7 +1389,7 @@ function authController(store, config, bus) {
1371
1389
  if (!row) {
1372
1390
  return (0, import_core5.setApiResponse)(import_core5.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
1373
1391
  }
1374
- const pin = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
1392
+ const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1375
1393
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
1376
1394
  await emailVerif.create(row.id, pin, expiresAt);
1377
1395
  const user = await users.findById(row.id);
@@ -1435,7 +1453,7 @@ function authController(store, config, bus) {
1435
1453
  firstName ?? null,
1436
1454
  lastName ?? null
1437
1455
  );
1438
- const otp = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
1456
+ const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1439
1457
  const expiresAt = new Date(Date.now() + OTP_TTL_MS);
1440
1458
  await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
1441
1459
  const user = await users.findById(id);
@@ -1581,7 +1599,7 @@ function authController(store, config, bus) {
1581
1599
  "Account suspended. Please contact support."
1582
1600
  );
1583
1601
  }
1584
- const otp = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
1602
+ const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1585
1603
  const expiresAt = new Date(Date.now() + OTP_TTL_MS);
1586
1604
  await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
1587
1605
  bus?.emit(import_events2.NOTIFICATION_EVENT, {
@@ -1691,14 +1709,17 @@ function authController(store, config, bus) {
1691
1709
  "Password reset email sent (if account exists)."
1692
1710
  );
1693
1711
  }
1694
- const pin = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
1712
+ const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1713
+ const token = (0, import_node_crypto5.randomBytes)(32).toString("hex");
1695
1714
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60);
1696
- await passwordReset.create(user.id, pin, expiresAt);
1715
+ await passwordReset.create(user.id, pin, token, expiresAt);
1716
+ const base = resolved.passwordResetUrl;
1717
+ const resetUrl = base ? `${base}${base.includes("?") ? "&" : "?"}token=${token}` : "";
1697
1718
  bus?.emit(import_events2.NOTIFICATION_EVENT, {
1698
1719
  type: MESSAGE_KEYS.passwordReset,
1699
1720
  locale: user.locale,
1700
1721
  recipient: { email: email2, phone: null, deviceToken: null },
1701
- data: { pin }
1722
+ data: { pin, token, resetUrl }
1702
1723
  }).catch(() => {
1703
1724
  });
1704
1725
  return (0, import_core5.setApiResponse)(
@@ -1711,19 +1732,9 @@ function authController(store, config, bus) {
1711
1732
  const body = ctx.meta["body"];
1712
1733
  const raw = body?.["pin"];
1713
1734
  const password2 = body?.["password"];
1714
- if (typeof raw !== "string" || typeof password2 !== "string") {
1715
- return (0, import_core5.setApiResponse)(
1716
- import_core5.HTTP.UNPROCESSABLE,
1717
- "INVALID_PARAMETER",
1718
- "pin and password are required"
1719
- );
1720
- }
1721
- if (!/^\d{6}$/.test(raw.trim())) {
1722
- return (0, import_core5.setApiResponse)(
1723
- import_core5.HTTP.UNPROCESSABLE,
1724
- "INVALID_PARAMETER",
1725
- "pin must be a 6-digit code"
1726
- );
1735
+ const rawToken = body?.["token"];
1736
+ if (typeof password2 !== "string") {
1737
+ return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
1727
1738
  }
1728
1739
  if (password2.length < 8) {
1729
1740
  return (0, import_core5.setApiResponse)(
@@ -1732,10 +1743,25 @@ function authController(store, config, bus) {
1732
1743
  "password must be at least 8 characters"
1733
1744
  );
1734
1745
  }
1735
- const pin = raw.trim();
1736
- const row = await passwordReset.findByPin(pin);
1746
+ let row;
1747
+ if (typeof rawToken === "string" && rawToken.trim().length >= 32) {
1748
+ row = await passwordReset.findByToken(rawToken.trim());
1749
+ } else {
1750
+ if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
1751
+ return (0, import_core5.setApiResponse)(
1752
+ import_core5.HTTP.UNPROCESSABLE,
1753
+ "INVALID_PARAMETER",
1754
+ "a 6-digit pin or a reset token is required"
1755
+ );
1756
+ }
1757
+ row = await passwordReset.findByPin(raw.trim());
1758
+ }
1737
1759
  if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
1738
- return (0, import_core5.setApiResponse)(import_core5.HTTP.BAD_REQUEST, "PASSWORD_RESET_FAILED", "Invalid or expired pin");
1760
+ return (0, import_core5.setApiResponse)(
1761
+ import_core5.HTTP.BAD_REQUEST,
1762
+ "PASSWORD_RESET_FAILED",
1763
+ "Invalid or expired reset credentials"
1764
+ );
1739
1765
  }
1740
1766
  const passwordHash = await hashPassword(password2);
1741
1767
  await store.transaction(async (tx) => {
@@ -1867,7 +1893,7 @@ function authController(store, config, bus) {
1867
1893
  }
1868
1894
  );
1869
1895
  }
1870
- const otp = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
1896
+ const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1871
1897
  const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
1872
1898
  await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
1873
1899
  bus?.emit(import_events2.NOTIFICATION_EVENT, {
@@ -1907,7 +1933,7 @@ function authController(store, config, bus) {
1907
1933
  }
1908
1934
  );
1909
1935
  }
1910
- const pin = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
1936
+ const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
1911
1937
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
1912
1938
  await emailVerif.replace(ctx.user.id, pin, expiresAt);
1913
1939
  bus?.emit(import_events2.NOTIFICATION_EVENT, {
@@ -1925,7 +1951,7 @@ function authController(store, config, bus) {
1925
1951
  }
1926
1952
 
1927
1953
  // src/controllers/user.controller.ts
1928
- var import_node_crypto5 = require("crypto");
1954
+ var import_node_crypto6 = require("crypto");
1929
1955
  var import_core7 = require("@fonderie/core");
1930
1956
  var import_events3 = require("@fonderie/events");
1931
1957
 
@@ -2115,7 +2141,7 @@ function userController(store, config, bus) {
2115
2141
  if (existing) {
2116
2142
  return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
2117
2143
  }
2118
- const pin = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
2144
+ const pin = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
2119
2145
  const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
2120
2146
  await emailVerif.replace(ctx.user.id, pin, expiresAt);
2121
2147
  await users.updateEmail(ctx.user.id, normalised);
@@ -2159,7 +2185,7 @@ function userController(store, config, bus) {
2159
2185
  if (existing) {
2160
2186
  return (0, import_core7.setApiResponse)(import_core7.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
2161
2187
  }
2162
- const otp = (0, import_node_crypto5.randomInt)(1e5, 1e6).toString();
2188
+ const otp = (0, import_node_crypto6.randomInt)(1e5, 1e6).toString();
2163
2189
  const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
2164
2190
  await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
2165
2191
  await users.updatePhone(ctx.user.id, normalised);
@@ -2268,7 +2294,7 @@ function userController(store, config, bus) {
2268
2294
  }
2269
2295
 
2270
2296
  // src/controllers/oauth.controller.ts
2271
- var import_node_crypto6 = require("crypto");
2297
+ var import_node_crypto7 = require("crypto");
2272
2298
  var import_core8 = require("@fonderie/core");
2273
2299
  function oauthController(store, config) {
2274
2300
  const users = new UserModel(store);
@@ -2284,7 +2310,7 @@ function oauthController(store, config) {
2284
2310
  "Google OAuth not configured"
2285
2311
  );
2286
2312
  }
2287
- const state = (0, import_node_crypto6.randomBytes)(16).toString("hex");
2313
+ const state = (0, import_node_crypto7.randomBytes)(16).toString("hex");
2288
2314
  const params = new URLSearchParams({
2289
2315
  client_id: google.clientId,
2290
2316
  redirect_uri: google.redirectUri,
@@ -2750,7 +2776,11 @@ If you made this change, you're all set. If not, contact support right away \u20
2750
2776
  var SAMPLE_PAYLOADS = {
2751
2777
  [MESSAGE_KEYS.emailRegistration]: { firstName: "", pin: "123456" },
2752
2778
  [MESSAGE_KEYS.emailVerification]: { firstName: "Ada", pin: "123456" },
2753
- [MESSAGE_KEYS.passwordReset]: { pin: "123456" },
2779
+ [MESSAGE_KEYS.passwordReset]: {
2780
+ pin: "123456",
2781
+ token: "0".repeat(64),
2782
+ resetUrl: "https://app.example.com/reset?token=" + "0".repeat(64)
2783
+ },
2754
2784
  [MESSAGE_KEYS.phoneOtp]: { otp: "123456" },
2755
2785
  [MESSAGE_KEYS.mfaEnabled]: {},
2756
2786
  [MESSAGE_KEYS.mfaDisabled]: {},