@fonderie/auth 7.0.1 → 7.1.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/brain/outcomes.md +1 -0
- package/brain/signatures.md +1 -0
- package/dist/index.cjs +53 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +56 -28
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/migrations/sql/015_password_reset_token.sql +11 -0
- package/dist/{session-Db6KZca3.d.cts → session-6B77MAFn.d.cts} +1 -0
- package/dist/{session-Db6KZca3.d.ts → session-6B77MAFn.d.ts} +1 -0
- package/package.json +1 -1
package/brain/outcomes.md
CHANGED
package/brain/signatures.md
CHANGED
|
@@ -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.
|
|
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(),
|
|
@@ -1198,13 +1201,13 @@ var PasswordResetModel = class {
|
|
|
1198
1201
|
this.store = store;
|
|
1199
1202
|
}
|
|
1200
1203
|
store;
|
|
1201
|
-
async create(userId, pin, expiresAt) {
|
|
1204
|
+
async create(userId, pin, token, expiresAt) {
|
|
1202
1205
|
await this.store.query(
|
|
1203
|
-
`INSERT INTO fonderie_password_resets (user_id, pin, expires_at, created_at)
|
|
1204
|
-
VALUES ($1, $2, $3, now())
|
|
1206
|
+
`INSERT INTO fonderie_password_resets (user_id, pin, token, expires_at, created_at)
|
|
1207
|
+
VALUES ($1, $2, $3, $4, now())
|
|
1205
1208
|
ON CONFLICT (user_id) DO UPDATE
|
|
1206
|
-
SET pin = $2,
|
|
1207
|
-
[userId, pin, expiresAt]
|
|
1209
|
+
SET pin = $2, token = $3, expires_at = $4, created_at = now()`,
|
|
1210
|
+
[userId, pin, token, expiresAt]
|
|
1208
1211
|
);
|
|
1209
1212
|
}
|
|
1210
1213
|
async findLastSentAt(userId) {
|
|
@@ -1223,6 +1226,19 @@ var PasswordResetModel = class {
|
|
|
1223
1226
|
if (!row) return null;
|
|
1224
1227
|
return { userId: row.user_id, expiresAt: new Date(row.expires_at) };
|
|
1225
1228
|
}
|
|
1229
|
+
// Lookup by the high-entropy reset token. Unlike the 6-digit pin this is
|
|
1230
|
+
// not brute-forceable, so the token path needs no rate limit. Empty/short
|
|
1231
|
+
// values are refused before the query so a NULL/blank token column can
|
|
1232
|
+
// never be matched.
|
|
1233
|
+
async findByToken(token) {
|
|
1234
|
+
if (typeof token !== "string" || token.length < 32) return null;
|
|
1235
|
+
const [row] = await this.store.query(
|
|
1236
|
+
`SELECT user_id, expires_at FROM fonderie_password_resets WHERE token = $1`,
|
|
1237
|
+
[token]
|
|
1238
|
+
);
|
|
1239
|
+
if (!row) return null;
|
|
1240
|
+
return { userId: row.user_id, expiresAt: new Date(row.expires_at) };
|
|
1241
|
+
}
|
|
1226
1242
|
async deleteByUser(userId) {
|
|
1227
1243
|
await this.store.query(`DELETE FROM fonderie_password_resets WHERE user_id = $1`, [userId]);
|
|
1228
1244
|
}
|
|
@@ -1692,13 +1708,16 @@ function authController(store, config, bus) {
|
|
|
1692
1708
|
);
|
|
1693
1709
|
}
|
|
1694
1710
|
const pin = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
|
|
1711
|
+
const token = (0, import_node_crypto4.randomBytes)(32).toString("hex");
|
|
1695
1712
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60);
|
|
1696
|
-
await passwordReset.create(user.id, pin, expiresAt);
|
|
1713
|
+
await passwordReset.create(user.id, pin, token, expiresAt);
|
|
1714
|
+
const base = resolved.passwordResetUrl;
|
|
1715
|
+
const resetUrl = base ? `${base}${base.includes("?") ? "&" : "?"}token=${token}` : "";
|
|
1697
1716
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1698
1717
|
type: MESSAGE_KEYS.passwordReset,
|
|
1699
1718
|
locale: user.locale,
|
|
1700
1719
|
recipient: { email: email2, phone: null, deviceToken: null },
|
|
1701
|
-
data: { pin }
|
|
1720
|
+
data: { pin, token, resetUrl }
|
|
1702
1721
|
}).catch(() => {
|
|
1703
1722
|
});
|
|
1704
1723
|
return (0, import_core5.setApiResponse)(
|
|
@@ -1711,19 +1730,9 @@ function authController(store, config, bus) {
|
|
|
1711
1730
|
const body = ctx.meta["body"];
|
|
1712
1731
|
const raw = body?.["pin"];
|
|
1713
1732
|
const password2 = body?.["password"];
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
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
|
-
);
|
|
1733
|
+
const rawToken = body?.["token"];
|
|
1734
|
+
if (typeof password2 !== "string") {
|
|
1735
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "password is required");
|
|
1727
1736
|
}
|
|
1728
1737
|
if (password2.length < 8) {
|
|
1729
1738
|
return (0, import_core5.setApiResponse)(
|
|
@@ -1732,10 +1741,25 @@ function authController(store, config, bus) {
|
|
|
1732
1741
|
"password must be at least 8 characters"
|
|
1733
1742
|
);
|
|
1734
1743
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1744
|
+
let row;
|
|
1745
|
+
if (typeof rawToken === "string" && rawToken.trim().length >= 32) {
|
|
1746
|
+
row = await passwordReset.findByToken(rawToken.trim());
|
|
1747
|
+
} else {
|
|
1748
|
+
if (typeof raw !== "string" || !/^\d{6}$/.test(raw.trim())) {
|
|
1749
|
+
return (0, import_core5.setApiResponse)(
|
|
1750
|
+
import_core5.HTTP.UNPROCESSABLE,
|
|
1751
|
+
"INVALID_PARAMETER",
|
|
1752
|
+
"a 6-digit pin or a reset token is required"
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
row = await passwordReset.findByPin(raw.trim());
|
|
1756
|
+
}
|
|
1737
1757
|
if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
|
|
1738
|
-
return (0, import_core5.setApiResponse)(
|
|
1758
|
+
return (0, import_core5.setApiResponse)(
|
|
1759
|
+
import_core5.HTTP.BAD_REQUEST,
|
|
1760
|
+
"PASSWORD_RESET_FAILED",
|
|
1761
|
+
"Invalid or expired reset credentials"
|
|
1762
|
+
);
|
|
1739
1763
|
}
|
|
1740
1764
|
const passwordHash = await hashPassword(password2);
|
|
1741
1765
|
await store.transaction(async (tx) => {
|
|
@@ -2750,7 +2774,11 @@ If you made this change, you're all set. If not, contact support right away \u20
|
|
|
2750
2774
|
var SAMPLE_PAYLOADS = {
|
|
2751
2775
|
[MESSAGE_KEYS.emailRegistration]: { firstName: "", pin: "123456" },
|
|
2752
2776
|
[MESSAGE_KEYS.emailVerification]: { firstName: "Ada", pin: "123456" },
|
|
2753
|
-
[MESSAGE_KEYS.passwordReset]: {
|
|
2777
|
+
[MESSAGE_KEYS.passwordReset]: {
|
|
2778
|
+
pin: "123456",
|
|
2779
|
+
token: "0".repeat(64),
|
|
2780
|
+
resetUrl: "https://app.example.com/reset?token=" + "0".repeat(64)
|
|
2781
|
+
},
|
|
2754
2782
|
[MESSAGE_KEYS.phoneOtp]: { otp: "123456" },
|
|
2755
2783
|
[MESSAGE_KEYS.mfaEnabled]: {},
|
|
2756
2784
|
[MESSAGE_KEYS.mfaDisabled]: {},
|