@fonderie/auth 2.1.0 → 4.0.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/signatures.md +21 -0
- package/dist/index.cjs +124 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -4
- package/dist/index.d.ts +25 -4
- package/dist/index.js +122 -2
- 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/{session-BoJjRB7t.d.cts → session-DxsmQjXX.d.cts} +1 -0
- package/dist/{session-BoJjRB7t.d.ts → session-DxsmQjXX.d.ts} +1 -0
- package/package.json +5 -5
package/brain/signatures.md
CHANGED
|
@@ -49,6 +49,7 @@ interface IMfaChallenge {
|
|
|
49
49
|
|
|
50
50
|
new AuthModule(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus | undefined): AuthModule
|
|
51
51
|
.name: "@fonderie/auth"
|
|
52
|
+
.checkReadiness(): IReadinessProblem[]
|
|
52
53
|
.install(app: IFonderieApp): void
|
|
53
54
|
|
|
54
55
|
interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
|
|
@@ -61,6 +62,7 @@ interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
|
|
|
61
62
|
meta: Record<string, unknown>;
|
|
62
63
|
}) => Partial<IAuthRuntimeConfig>;
|
|
63
64
|
routes?: Partial<Record<AuthRouteId, AuthRouteOverride>>;
|
|
65
|
+
legacyVerify?: (plain: string, hash: string) => boolean | Promise<boolean>;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
interface IAuthSecrets {
|
|
@@ -127,6 +129,25 @@ function normalizeEmail(email: string): string
|
|
|
127
129
|
|
|
128
130
|
function normalizeEmailSafe(email: string): string | null
|
|
129
131
|
|
|
132
|
+
function importUser(store: IStoreAdapter, user: IImportUser): Promise<{ id: string; }>
|
|
133
|
+
|
|
134
|
+
interface IImportUser {
|
|
135
|
+
id?: string;
|
|
136
|
+
email: string;
|
|
137
|
+
passwordHash?: string | null;
|
|
138
|
+
firstName?: string | null;
|
|
139
|
+
lastName?: string | null;
|
|
140
|
+
phone?: string | null;
|
|
141
|
+
profileImageUrl?: string | null;
|
|
142
|
+
locale?: string;
|
|
143
|
+
timezone?: string;
|
|
144
|
+
emailVerifiedAt?: Date | null;
|
|
145
|
+
mfaEnabled?: boolean;
|
|
146
|
+
createdAt?: Date;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateAuthConfig(config: IAuthConfig): void
|
|
150
|
+
|
|
130
151
|
function buildAuthIpLimiter(route: AuthLimitedRoute, store: IStoreAdapter, config: false | IAuthRateLimitConfig | undefined): Middleware | null
|
|
131
152
|
|
|
132
153
|
function buildAuthAccountLimiter(route: AuthLimitedRoute, store: IStoreAdapter, config: false | IAuthRateLimitConfig | undefined): Middleware | null
|
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
34
34
|
var password_exports = {};
|
|
35
35
|
__export(password_exports, {
|
|
36
36
|
hashPassword: () => hashPassword,
|
|
37
|
-
verifyPassword: () => verifyPassword
|
|
37
|
+
verifyPassword: () => verifyPassword,
|
|
38
|
+
verifyPasswordForLogin: () => verifyPasswordForLogin
|
|
38
39
|
});
|
|
39
40
|
async function hashPassword(plain) {
|
|
40
41
|
return import_bcryptjs.default.hash(plain, ROUNDS);
|
|
@@ -42,6 +43,19 @@ async function hashPassword(plain) {
|
|
|
42
43
|
async function verifyPassword(plain, hash) {
|
|
43
44
|
return import_bcryptjs.default.compare(plain, hash);
|
|
44
45
|
}
|
|
46
|
+
async function verifyPasswordForLogin(plain, hash, legacyVerify) {
|
|
47
|
+
if (await verifyPassword(plain, hash)) return { valid: true, needsRehash: false };
|
|
48
|
+
if (legacyVerify) {
|
|
49
|
+
let legacyOk = false;
|
|
50
|
+
try {
|
|
51
|
+
legacyOk = await legacyVerify(plain, hash);
|
|
52
|
+
} catch {
|
|
53
|
+
legacyOk = false;
|
|
54
|
+
}
|
|
55
|
+
if (legacyOk) return { valid: true, needsRehash: true };
|
|
56
|
+
}
|
|
57
|
+
return { valid: false, needsRehash: false };
|
|
58
|
+
}
|
|
45
59
|
var import_bcryptjs, ROUNDS;
|
|
46
60
|
var init_password = __esm({
|
|
47
61
|
"src/services/password.ts"() {
|
|
@@ -59,12 +73,14 @@ __export(index_exports, {
|
|
|
59
73
|
MESSAGE_KEYS: () => MESSAGE_KEYS,
|
|
60
74
|
buildAuthAccountLimiter: () => buildAuthAccountLimiter,
|
|
61
75
|
buildAuthIpLimiter: () => buildAuthIpLimiter,
|
|
76
|
+
importUser: () => importUser,
|
|
62
77
|
normalizeEmail: () => normalizeEmail,
|
|
63
78
|
normalizeEmailSafe: () => normalizeEmailSafe,
|
|
64
79
|
requireAuth: () => import_middlewares3.requireAuth,
|
|
65
80
|
schemas: () => schemas_exports,
|
|
66
81
|
toUserDTO: () => toUserDTO,
|
|
67
82
|
validate: () => import_middlewares.validate,
|
|
83
|
+
validateAuthConfig: () => validateAuthConfig,
|
|
68
84
|
withSession: () => withSession
|
|
69
85
|
});
|
|
70
86
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -753,6 +769,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
753
769
|
await users.confirmMfaSecret(ctx.user.id);
|
|
754
770
|
bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
755
771
|
type: MESSAGE_KEYS.mfaEnabled,
|
|
772
|
+
locale: ctx.user.locale,
|
|
756
773
|
data: {},
|
|
757
774
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
758
775
|
}).catch(() => {
|
|
@@ -844,6 +861,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
844
861
|
await backupCodes.replace(ctx.user.id, codeHashes);
|
|
845
862
|
bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
846
863
|
type: MESSAGE_KEYS.mfaBackupCodesRegenerated,
|
|
864
|
+
locale: ctx.user.locale,
|
|
847
865
|
data: {},
|
|
848
866
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
849
867
|
}).catch(() => {
|
|
@@ -870,6 +888,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
870
888
|
await Promise.all([users.disableMfa(ctx.user.id), backupCodes.deleteByUser(ctx.user.id)]);
|
|
871
889
|
bus?.emit(import_events.NOTIFICATION_EVENT, {
|
|
872
890
|
type: MESSAGE_KEYS.mfaDisabled,
|
|
891
|
+
locale: user.locale,
|
|
873
892
|
data: {},
|
|
874
893
|
recipient: { email: user.email, phone: null, deviceToken: null }
|
|
875
894
|
}).catch(() => {
|
|
@@ -1117,6 +1136,7 @@ function authController(store, config, bus) {
|
|
|
1117
1136
|
import_events2.NOTIFICATION_EVENT,
|
|
1118
1137
|
{
|
|
1119
1138
|
type: MESSAGE_KEYS.emailRegistration,
|
|
1139
|
+
locale: user.locale,
|
|
1120
1140
|
data: { pin, firstName: firstName ?? "" },
|
|
1121
1141
|
recipient: { email: normalizedEmail, phone: null, deviceToken: null }
|
|
1122
1142
|
},
|
|
@@ -1180,6 +1200,7 @@ function authController(store, config, bus) {
|
|
|
1180
1200
|
import_events2.NOTIFICATION_EVENT,
|
|
1181
1201
|
{
|
|
1182
1202
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1203
|
+
locale: user.locale,
|
|
1183
1204
|
data: { otp },
|
|
1184
1205
|
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1185
1206
|
},
|
|
@@ -1235,10 +1256,17 @@ function authController(store, config, bus) {
|
|
|
1235
1256
|
if (!user || !user.passwordHash) {
|
|
1236
1257
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1237
1258
|
}
|
|
1238
|
-
const valid = await
|
|
1259
|
+
const { valid, needsRehash } = await verifyPasswordForLogin(
|
|
1260
|
+
password2,
|
|
1261
|
+
user.passwordHash,
|
|
1262
|
+
config.legacyVerify
|
|
1263
|
+
);
|
|
1239
1264
|
if (!valid) {
|
|
1240
1265
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1241
1266
|
}
|
|
1267
|
+
if (needsRehash) {
|
|
1268
|
+
await users.updatePassword(user.id, await hashPassword(password2));
|
|
1269
|
+
}
|
|
1242
1270
|
if (user.suspended) {
|
|
1243
1271
|
return (0, import_core4.setApiResponse)(
|
|
1244
1272
|
import_core4.HTTP.FORBIDDEN,
|
|
@@ -1292,6 +1320,7 @@ function authController(store, config, bus) {
|
|
|
1292
1320
|
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1293
1321
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1294
1322
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1323
|
+
locale: user.locale,
|
|
1295
1324
|
data: { otp },
|
|
1296
1325
|
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1297
1326
|
}).catch(() => {
|
|
@@ -1406,6 +1435,7 @@ function authController(store, config, bus) {
|
|
|
1406
1435
|
await passwordReset.create(user.id, pin, expiresAt);
|
|
1407
1436
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1408
1437
|
type: MESSAGE_KEYS.passwordReset,
|
|
1438
|
+
locale: user.locale,
|
|
1409
1439
|
recipient: { email: email2, phone: null, deviceToken: null },
|
|
1410
1440
|
data: { pin }
|
|
1411
1441
|
}).catch(() => {
|
|
@@ -1572,6 +1602,7 @@ function authController(store, config, bus) {
|
|
|
1572
1602
|
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
1573
1603
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1574
1604
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1605
|
+
locale: ctx.user.locale,
|
|
1575
1606
|
data: { otp },
|
|
1576
1607
|
recipient: { email: null, phone: phone2, deviceToken: null }
|
|
1577
1608
|
}).catch(() => {
|
|
@@ -1611,6 +1642,7 @@ function authController(store, config, bus) {
|
|
|
1611
1642
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
1612
1643
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
1613
1644
|
type: MESSAGE_KEYS.emailVerification,
|
|
1645
|
+
locale: ctx.user.locale,
|
|
1614
1646
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null },
|
|
1615
1647
|
data: { pin }
|
|
1616
1648
|
}).catch(() => {
|
|
@@ -1722,6 +1754,7 @@ function userController(store, config, bus) {
|
|
|
1722
1754
|
await users.updateEmail(ctx.user.id, normalised);
|
|
1723
1755
|
bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
1724
1756
|
type: MESSAGE_KEYS.emailVerification,
|
|
1757
|
+
locale: ctx.user.locale,
|
|
1725
1758
|
data: { pin },
|
|
1726
1759
|
recipient: { email: normalised, phone: null, deviceToken: null }
|
|
1727
1760
|
}).catch(() => {
|
|
@@ -1729,6 +1762,7 @@ function userController(store, config, bus) {
|
|
|
1729
1762
|
if (oldEmail) {
|
|
1730
1763
|
bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
1731
1764
|
type: MESSAGE_KEYS.emailChanged,
|
|
1765
|
+
locale: ctx.user.locale,
|
|
1732
1766
|
data: { newEmail: normalised },
|
|
1733
1767
|
recipient: { email: oldEmail, phone: null, deviceToken: null }
|
|
1734
1768
|
}).catch(() => {
|
|
@@ -1764,6 +1798,7 @@ function userController(store, config, bus) {
|
|
|
1764
1798
|
await users.updatePhone(ctx.user.id, normalised);
|
|
1765
1799
|
bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
1766
1800
|
type: MESSAGE_KEYS.phoneOtp,
|
|
1801
|
+
locale: ctx.user.locale,
|
|
1767
1802
|
data: { otp },
|
|
1768
1803
|
recipient: { email: null, phone: normalised, deviceToken: null }
|
|
1769
1804
|
}).catch(() => {
|
|
@@ -1771,6 +1806,7 @@ function userController(store, config, bus) {
|
|
|
1771
1806
|
if (ctx.user.email) {
|
|
1772
1807
|
bus?.emit(import_events3.NOTIFICATION_EVENT, {
|
|
1773
1808
|
type: MESSAGE_KEYS.phoneChanged,
|
|
1809
|
+
locale: ctx.user.locale,
|
|
1774
1810
|
data: {},
|
|
1775
1811
|
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
1776
1812
|
}).catch(() => {
|
|
@@ -1985,6 +2021,53 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1985
2021
|
return routes;
|
|
1986
2022
|
}
|
|
1987
2023
|
|
|
2024
|
+
// src/services/config-guard.ts
|
|
2025
|
+
var MODULE = "@fonderie/auth";
|
|
2026
|
+
var PLACEHOLDER_SECRET = /dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|min-32-chars/i;
|
|
2027
|
+
var MIN_SECRET_LENGTH = 32;
|
|
2028
|
+
function collectAuthConfigProblems(config) {
|
|
2029
|
+
const problems = [];
|
|
2030
|
+
const secret = config.jwtSecret ?? "";
|
|
2031
|
+
if (secret.length < MIN_SECRET_LENGTH) {
|
|
2032
|
+
problems.push({
|
|
2033
|
+
module: MODULE,
|
|
2034
|
+
severity: "error",
|
|
2035
|
+
message: `jwtSecret must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`
|
|
2036
|
+
});
|
|
2037
|
+
} else if (PLACEHOLDER_SECRET.test(secret)) {
|
|
2038
|
+
problems.push({
|
|
2039
|
+
module: MODULE,
|
|
2040
|
+
severity: "error",
|
|
2041
|
+
message: "jwtSecret looks like a placeholder or dev-default value"
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
if (config.secureCookies === false) {
|
|
2045
|
+
problems.push({
|
|
2046
|
+
module: MODULE,
|
|
2047
|
+
severity: "warning",
|
|
2048
|
+
message: "secureCookies is false \u2014 auth cookies may be sent over non-HTTPS connections in production"
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
return problems;
|
|
2052
|
+
}
|
|
2053
|
+
function validateAuthConfig(config) {
|
|
2054
|
+
const isProduction = process.env["NODE_ENV"] === "production";
|
|
2055
|
+
const problems = collectAuthConfigProblems(config);
|
|
2056
|
+
const errors = problems.filter((p) => p.severity === "error");
|
|
2057
|
+
if (isProduction && errors.length > 0) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
`[auth] insecure config \u2014 ${errors.map((e) => e.message).join("; ")}. Set a long, random jwtSecret (e.g. \`openssl rand -base64 32\`). Refusing to boot in production.`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
if (isProduction) {
|
|
2063
|
+
for (const p of problems) console.warn(`[auth] ${p.message}`);
|
|
2064
|
+
} else {
|
|
2065
|
+
for (const p of errors) {
|
|
2066
|
+
console.warn(`[auth] ${p.message} (insecure \u2014 would refuse to boot in production)`);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
1988
2071
|
// src/middlewares/session.ts
|
|
1989
2072
|
function withSession(store, config) {
|
|
1990
2073
|
const users = new UserModel(store);
|
|
@@ -2032,11 +2115,16 @@ var AuthModule = class {
|
|
|
2032
2115
|
this.store = store;
|
|
2033
2116
|
this.config = config;
|
|
2034
2117
|
this.bus = bus;
|
|
2118
|
+
validateAuthConfig(config);
|
|
2035
2119
|
}
|
|
2036
2120
|
store;
|
|
2037
2121
|
config;
|
|
2038
2122
|
bus;
|
|
2039
2123
|
name = "@fonderie/auth";
|
|
2124
|
+
// Report config problems for app.checkProductionReadiness() (data, not throw).
|
|
2125
|
+
checkReadiness() {
|
|
2126
|
+
return collectAuthConfigProblems(this.config);
|
|
2127
|
+
}
|
|
2040
2128
|
install(app) {
|
|
2041
2129
|
app.use(withSession(this.store, this.config));
|
|
2042
2130
|
const routes = buildAuthRoutes(this.store, this.config, this.bus);
|
|
@@ -2048,6 +2136,38 @@ var AuthModule = class {
|
|
|
2048
2136
|
|
|
2049
2137
|
// src/middlewares/require-auth.ts
|
|
2050
2138
|
var import_middlewares3 = require("@fonderie/core/middlewares");
|
|
2139
|
+
|
|
2140
|
+
// src/migrate.ts
|
|
2141
|
+
async function importUser(store, user) {
|
|
2142
|
+
const cols = [];
|
|
2143
|
+
const vals = [];
|
|
2144
|
+
const placeholders = [];
|
|
2145
|
+
const add = (col, val) => {
|
|
2146
|
+
cols.push(col);
|
|
2147
|
+
vals.push(val);
|
|
2148
|
+
placeholders.push(`$${vals.length}`);
|
|
2149
|
+
};
|
|
2150
|
+
if (user.id !== void 0) add("id", user.id);
|
|
2151
|
+
add("email", user.email.toLowerCase().trim());
|
|
2152
|
+
if (user.passwordHash !== void 0) add("password_hash", user.passwordHash);
|
|
2153
|
+
if (user.firstName !== void 0) add("first_name", user.firstName);
|
|
2154
|
+
if (user.lastName !== void 0) add("last_name", user.lastName);
|
|
2155
|
+
if (user.phone !== void 0) add("phone", user.phone);
|
|
2156
|
+
if (user.profileImageUrl !== void 0) add("profile_image_url", user.profileImageUrl);
|
|
2157
|
+
if (user.locale !== void 0) add("locale", user.locale);
|
|
2158
|
+
if (user.timezone !== void 0) add("timezone", user.timezone);
|
|
2159
|
+
if (user.emailVerifiedAt !== void 0) add("email_verified_at", user.emailVerifiedAt);
|
|
2160
|
+
if (user.mfaEnabled !== void 0) add("mfa_enabled", user.mfaEnabled);
|
|
2161
|
+
if (user.createdAt !== void 0) add("created_at", user.createdAt);
|
|
2162
|
+
const [row] = await store.query(
|
|
2163
|
+
`INSERT INTO fonderie_users (${cols.join(", ")})
|
|
2164
|
+
VALUES (${placeholders.join(", ")})
|
|
2165
|
+
RETURNING id`,
|
|
2166
|
+
vals
|
|
2167
|
+
);
|
|
2168
|
+
if (!row) throw new Error("[auth] importUser: insert returned no row");
|
|
2169
|
+
return row;
|
|
2170
|
+
}
|
|
2051
2171
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2052
2172
|
0 && (module.exports = {
|
|
2053
2173
|
AUTH_CONFIG_KEYS,
|
|
@@ -2055,12 +2175,14 @@ var import_middlewares3 = require("@fonderie/core/middlewares");
|
|
|
2055
2175
|
MESSAGE_KEYS,
|
|
2056
2176
|
buildAuthAccountLimiter,
|
|
2057
2177
|
buildAuthIpLimiter,
|
|
2178
|
+
importUser,
|
|
2058
2179
|
normalizeEmail,
|
|
2059
2180
|
normalizeEmailSafe,
|
|
2060
2181
|
requireAuth,
|
|
2061
2182
|
schemas,
|
|
2062
2183
|
toUserDTO,
|
|
2063
2184
|
validate,
|
|
2185
|
+
validateAuthConfig,
|
|
2064
2186
|
withSession
|
|
2065
2187
|
});
|
|
2066
2188
|
//# sourceMappingURL=index.cjs.map
|