@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.
@@ -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 verifyPassword(password2, user.passwordHash);
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