@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 +1 -0
- package/brain/signatures.md +1 -0
- package/dist/index.cjs +68 -38
- 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 +59 -29
- 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(),
|
|
@@ -1146,7 +1149,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
1146
1149
|
}
|
|
1147
1150
|
|
|
1148
1151
|
// src/controllers/auth.controller.ts
|
|
1149
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
);
|
|
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
|
-
|
|
1736
|
-
|
|
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)(
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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]: {
|
|
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]: {},
|