@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/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
1
  export { IMfaChallenge, ISession, IUser } from './types.cjs';
2
- import { IFonderieModule, IFonderieApp } from '@fonderie/core';
2
+ import { IFonderieModule, IReadinessProblem, IFonderieApp } from '@fonderie/core';
3
3
  import { IStoreAdapter } from '@fonderie/store';
4
4
  import { EventBus } from '@fonderie/events';
5
- import { I as IAuthConfig } from './session-BoJjRB7t.cjs';
6
- export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, M as MESSAGE_KEYS, f as buildAuthAccountLimiter, g as buildAuthIpLimiter, w as withSession } from './session-BoJjRB7t.cjs';
5
+ import { I as IAuthConfig } from './session-DxsmQjXX.cjs';
6
+ export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, M as MESSAGE_KEYS, f as buildAuthAccountLimiter, g as buildAuthIpLimiter, w as withSession } from './session-DxsmQjXX.cjs';
7
7
  export { IUserDTO, toUserDTO } from './dtos/user.cjs';
8
8
  export { requireAuth, validate } from '@fonderie/core/middlewares';
9
9
  import { z } from 'zod';
@@ -15,6 +15,7 @@ declare class AuthModule implements IFonderieModule {
15
15
  private bus?;
16
16
  readonly name = "@fonderie/auth";
17
17
  constructor(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus | undefined);
18
+ checkReadiness(): IReadinessProblem[];
18
19
  install(app: IFonderieApp): void;
19
20
  }
20
21
 
@@ -99,4 +100,24 @@ declare namespace schemas {
99
100
  declare function normalizeEmail(email: string): string;
100
101
  declare function normalizeEmailSafe(email: string): string | null;
101
102
 
102
- export { AuthModule, type ChangePasswordInput, IAuthConfig, type LoginInput, type RegisterInput, type ResetPasswordInput, normalizeEmail, normalizeEmailSafe, schemas };
103
+ interface IImportUser {
104
+ id?: string;
105
+ email: string;
106
+ passwordHash?: string | null;
107
+ firstName?: string | null;
108
+ lastName?: string | null;
109
+ phone?: string | null;
110
+ profileImageUrl?: string | null;
111
+ locale?: string;
112
+ timezone?: string;
113
+ emailVerifiedAt?: Date | null;
114
+ mfaEnabled?: boolean;
115
+ createdAt?: Date;
116
+ }
117
+ declare function importUser(store: IStoreAdapter, user: IImportUser): Promise<{
118
+ id: string;
119
+ }>;
120
+
121
+ declare function validateAuthConfig(config: IAuthConfig): void;
122
+
123
+ export { AuthModule, type ChangePasswordInput, IAuthConfig, type IImportUser, type LoginInput, type RegisterInput, type ResetPasswordInput, importUser, normalizeEmail, normalizeEmailSafe, schemas, validateAuthConfig };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export { IMfaChallenge, ISession, IUser } from './types.js';
2
- import { IFonderieModule, IFonderieApp } from '@fonderie/core';
2
+ import { IFonderieModule, IReadinessProblem, IFonderieApp } from '@fonderie/core';
3
3
  import { IStoreAdapter } from '@fonderie/store';
4
4
  import { EventBus } from '@fonderie/events';
5
- import { I as IAuthConfig } from './session-BoJjRB7t.js';
6
- export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, M as MESSAGE_KEYS, f as buildAuthAccountLimiter, g as buildAuthIpLimiter, w as withSession } from './session-BoJjRB7t.js';
5
+ import { I as IAuthConfig } from './session-DxsmQjXX.js';
6
+ export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, M as MESSAGE_KEYS, f as buildAuthAccountLimiter, g as buildAuthIpLimiter, w as withSession } from './session-DxsmQjXX.js';
7
7
  export { IUserDTO, toUserDTO } from './dtos/user.js';
8
8
  export { requireAuth, validate } from '@fonderie/core/middlewares';
9
9
  import { z } from 'zod';
@@ -15,6 +15,7 @@ declare class AuthModule implements IFonderieModule {
15
15
  private bus?;
16
16
  readonly name = "@fonderie/auth";
17
17
  constructor(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus | undefined);
18
+ checkReadiness(): IReadinessProblem[];
18
19
  install(app: IFonderieApp): void;
19
20
  }
20
21
 
@@ -99,4 +100,24 @@ declare namespace schemas {
99
100
  declare function normalizeEmail(email: string): string;
100
101
  declare function normalizeEmailSafe(email: string): string | null;
101
102
 
102
- export { AuthModule, type ChangePasswordInput, IAuthConfig, type LoginInput, type RegisterInput, type ResetPasswordInput, normalizeEmail, normalizeEmailSafe, schemas };
103
+ interface IImportUser {
104
+ id?: string;
105
+ email: string;
106
+ passwordHash?: string | null;
107
+ firstName?: string | null;
108
+ lastName?: string | null;
109
+ phone?: string | null;
110
+ profileImageUrl?: string | null;
111
+ locale?: string;
112
+ timezone?: string;
113
+ emailVerifiedAt?: Date | null;
114
+ mfaEnabled?: boolean;
115
+ createdAt?: Date;
116
+ }
117
+ declare function importUser(store: IStoreAdapter, user: IImportUser): Promise<{
118
+ id: string;
119
+ }>;
120
+
121
+ declare function validateAuthConfig(config: IAuthConfig): void;
122
+
123
+ export { AuthModule, type ChangePasswordInput, IAuthConfig, type IImportUser, type LoginInput, type RegisterInput, type ResetPasswordInput, importUser, normalizeEmail, normalizeEmailSafe, schemas, validateAuthConfig };
package/dist/index.js CHANGED
@@ -12,7 +12,8 @@ var __export = (target, all) => {
12
12
  var password_exports = {};
13
13
  __export(password_exports, {
14
14
  hashPassword: () => hashPassword,
15
- verifyPassword: () => verifyPassword
15
+ verifyPassword: () => verifyPassword,
16
+ verifyPasswordForLogin: () => verifyPasswordForLogin
16
17
  });
17
18
  import bcrypt from "bcryptjs";
18
19
  async function hashPassword(plain) {
@@ -21,6 +22,19 @@ async function hashPassword(plain) {
21
22
  async function verifyPassword(plain, hash) {
22
23
  return bcrypt.compare(plain, hash);
23
24
  }
25
+ async function verifyPasswordForLogin(plain, hash, legacyVerify) {
26
+ if (await verifyPassword(plain, hash)) return { valid: true, needsRehash: false };
27
+ if (legacyVerify) {
28
+ let legacyOk = false;
29
+ try {
30
+ legacyOk = await legacyVerify(plain, hash);
31
+ } catch {
32
+ legacyOk = false;
33
+ }
34
+ if (legacyOk) return { valid: true, needsRehash: true };
35
+ }
36
+ return { valid: false, needsRehash: false };
37
+ }
24
38
  var ROUNDS;
25
39
  var init_password = __esm({
26
40
  "src/services/password.ts"() {
@@ -718,6 +732,7 @@ function mfaController(store, config, issuer, bus) {
718
732
  await users.confirmMfaSecret(ctx.user.id);
719
733
  bus?.emit(NOTIFICATION_EVENT, {
720
734
  type: MESSAGE_KEYS.mfaEnabled,
735
+ locale: ctx.user.locale,
721
736
  data: {},
722
737
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
723
738
  }).catch(() => {
@@ -809,6 +824,7 @@ function mfaController(store, config, issuer, bus) {
809
824
  await backupCodes.replace(ctx.user.id, codeHashes);
810
825
  bus?.emit(NOTIFICATION_EVENT, {
811
826
  type: MESSAGE_KEYS.mfaBackupCodesRegenerated,
827
+ locale: ctx.user.locale,
812
828
  data: {},
813
829
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
814
830
  }).catch(() => {
@@ -835,6 +851,7 @@ function mfaController(store, config, issuer, bus) {
835
851
  await Promise.all([users.disableMfa(ctx.user.id), backupCodes.deleteByUser(ctx.user.id)]);
836
852
  bus?.emit(NOTIFICATION_EVENT, {
837
853
  type: MESSAGE_KEYS.mfaDisabled,
854
+ locale: user.locale,
838
855
  data: {},
839
856
  recipient: { email: user.email, phone: null, deviceToken: null }
840
857
  }).catch(() => {
@@ -1082,6 +1099,7 @@ function authController(store, config, bus) {
1082
1099
  NOTIFICATION_EVENT2,
1083
1100
  {
1084
1101
  type: MESSAGE_KEYS.emailRegistration,
1102
+ locale: user.locale,
1085
1103
  data: { pin, firstName: firstName ?? "" },
1086
1104
  recipient: { email: normalizedEmail, phone: null, deviceToken: null }
1087
1105
  },
@@ -1145,6 +1163,7 @@ function authController(store, config, bus) {
1145
1163
  NOTIFICATION_EVENT2,
1146
1164
  {
1147
1165
  type: MESSAGE_KEYS.phoneOtp,
1166
+ locale: user.locale,
1148
1167
  data: { otp },
1149
1168
  recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
1150
1169
  },
@@ -1200,10 +1219,17 @@ function authController(store, config, bus) {
1200
1219
  if (!user || !user.passwordHash) {
1201
1220
  return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1202
1221
  }
1203
- const valid = await verifyPassword(password2, user.passwordHash);
1222
+ const { valid, needsRehash } = await verifyPasswordForLogin(
1223
+ password2,
1224
+ user.passwordHash,
1225
+ config.legacyVerify
1226
+ );
1204
1227
  if (!valid) {
1205
1228
  return setApiResponse3(HTTP3.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1206
1229
  }
1230
+ if (needsRehash) {
1231
+ await users.updatePassword(user.id, await hashPassword(password2));
1232
+ }
1207
1233
  if (user.suspended) {
1208
1234
  return setApiResponse3(
1209
1235
  HTTP3.FORBIDDEN,
@@ -1257,6 +1283,7 @@ function authController(store, config, bus) {
1257
1283
  await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
1258
1284
  bus?.emit(NOTIFICATION_EVENT2, {
1259
1285
  type: MESSAGE_KEYS.phoneOtp,
1286
+ locale: user.locale,
1260
1287
  data: { otp },
1261
1288
  recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
1262
1289
  }).catch(() => {
@@ -1371,6 +1398,7 @@ function authController(store, config, bus) {
1371
1398
  await passwordReset.create(user.id, pin, expiresAt);
1372
1399
  bus?.emit(NOTIFICATION_EVENT2, {
1373
1400
  type: MESSAGE_KEYS.passwordReset,
1401
+ locale: user.locale,
1374
1402
  recipient: { email: email2, phone: null, deviceToken: null },
1375
1403
  data: { pin }
1376
1404
  }).catch(() => {
@@ -1537,6 +1565,7 @@ function authController(store, config, bus) {
1537
1565
  await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
1538
1566
  bus?.emit(NOTIFICATION_EVENT2, {
1539
1567
  type: MESSAGE_KEYS.phoneOtp,
1568
+ locale: ctx.user.locale,
1540
1569
  data: { otp },
1541
1570
  recipient: { email: null, phone: phone2, deviceToken: null }
1542
1571
  }).catch(() => {
@@ -1576,6 +1605,7 @@ function authController(store, config, bus) {
1576
1605
  await emailVerif.replace(ctx.user.id, pin, expiresAt);
1577
1606
  bus?.emit(NOTIFICATION_EVENT2, {
1578
1607
  type: MESSAGE_KEYS.emailVerification,
1608
+ locale: ctx.user.locale,
1579
1609
  recipient: { email: ctx.user.email, phone: null, deviceToken: null },
1580
1610
  data: { pin }
1581
1611
  }).catch(() => {
@@ -1687,6 +1717,7 @@ function userController(store, config, bus) {
1687
1717
  await users.updateEmail(ctx.user.id, normalised);
1688
1718
  bus?.emit(NOTIFICATION_EVENT3, {
1689
1719
  type: MESSAGE_KEYS.emailVerification,
1720
+ locale: ctx.user.locale,
1690
1721
  data: { pin },
1691
1722
  recipient: { email: normalised, phone: null, deviceToken: null }
1692
1723
  }).catch(() => {
@@ -1694,6 +1725,7 @@ function userController(store, config, bus) {
1694
1725
  if (oldEmail) {
1695
1726
  bus?.emit(NOTIFICATION_EVENT3, {
1696
1727
  type: MESSAGE_KEYS.emailChanged,
1728
+ locale: ctx.user.locale,
1697
1729
  data: { newEmail: normalised },
1698
1730
  recipient: { email: oldEmail, phone: null, deviceToken: null }
1699
1731
  }).catch(() => {
@@ -1729,6 +1761,7 @@ function userController(store, config, bus) {
1729
1761
  await users.updatePhone(ctx.user.id, normalised);
1730
1762
  bus?.emit(NOTIFICATION_EVENT3, {
1731
1763
  type: MESSAGE_KEYS.phoneOtp,
1764
+ locale: ctx.user.locale,
1732
1765
  data: { otp },
1733
1766
  recipient: { email: null, phone: normalised, deviceToken: null }
1734
1767
  }).catch(() => {
@@ -1736,6 +1769,7 @@ function userController(store, config, bus) {
1736
1769
  if (ctx.user.email) {
1737
1770
  bus?.emit(NOTIFICATION_EVENT3, {
1738
1771
  type: MESSAGE_KEYS.phoneChanged,
1772
+ locale: ctx.user.locale,
1739
1773
  data: {},
1740
1774
  recipient: { email: ctx.user.email, phone: null, deviceToken: null }
1741
1775
  }).catch(() => {
@@ -1950,6 +1984,53 @@ function buildAuthRoutes(store, config, bus) {
1950
1984
  return routes;
1951
1985
  }
1952
1986
 
1987
+ // src/services/config-guard.ts
1988
+ var MODULE = "@fonderie/auth";
1989
+ var PLACEHOLDER_SECRET = /dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|min-32-chars/i;
1990
+ var MIN_SECRET_LENGTH = 32;
1991
+ function collectAuthConfigProblems(config) {
1992
+ const problems = [];
1993
+ const secret = config.jwtSecret ?? "";
1994
+ if (secret.length < MIN_SECRET_LENGTH) {
1995
+ problems.push({
1996
+ module: MODULE,
1997
+ severity: "error",
1998
+ message: `jwtSecret must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`
1999
+ });
2000
+ } else if (PLACEHOLDER_SECRET.test(secret)) {
2001
+ problems.push({
2002
+ module: MODULE,
2003
+ severity: "error",
2004
+ message: "jwtSecret looks like a placeholder or dev-default value"
2005
+ });
2006
+ }
2007
+ if (config.secureCookies === false) {
2008
+ problems.push({
2009
+ module: MODULE,
2010
+ severity: "warning",
2011
+ message: "secureCookies is false \u2014 auth cookies may be sent over non-HTTPS connections in production"
2012
+ });
2013
+ }
2014
+ return problems;
2015
+ }
2016
+ function validateAuthConfig(config) {
2017
+ const isProduction = process.env["NODE_ENV"] === "production";
2018
+ const problems = collectAuthConfigProblems(config);
2019
+ const errors = problems.filter((p) => p.severity === "error");
2020
+ if (isProduction && errors.length > 0) {
2021
+ throw new Error(
2022
+ `[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.`
2023
+ );
2024
+ }
2025
+ if (isProduction) {
2026
+ for (const p of problems) console.warn(`[auth] ${p.message}`);
2027
+ } else {
2028
+ for (const p of errors) {
2029
+ console.warn(`[auth] ${p.message} (insecure \u2014 would refuse to boot in production)`);
2030
+ }
2031
+ }
2032
+ }
2033
+
1953
2034
  // src/middlewares/session.ts
1954
2035
  function withSession(store, config) {
1955
2036
  const users = new UserModel(store);
@@ -1997,11 +2078,16 @@ var AuthModule = class {
1997
2078
  this.store = store;
1998
2079
  this.config = config;
1999
2080
  this.bus = bus;
2081
+ validateAuthConfig(config);
2000
2082
  }
2001
2083
  store;
2002
2084
  config;
2003
2085
  bus;
2004
2086
  name = "@fonderie/auth";
2087
+ // Report config problems for app.checkProductionReadiness() (data, not throw).
2088
+ checkReadiness() {
2089
+ return collectAuthConfigProblems(this.config);
2090
+ }
2005
2091
  install(app) {
2006
2092
  app.use(withSession(this.store, this.config));
2007
2093
  const routes = buildAuthRoutes(this.store, this.config, this.bus);
@@ -2013,18 +2099,52 @@ var AuthModule = class {
2013
2099
 
2014
2100
  // src/middlewares/require-auth.ts
2015
2101
  import { requireAuth as requireAuth2, requireAnyAuth as requireAnyAuth2 } from "@fonderie/core/middlewares";
2102
+
2103
+ // src/migrate.ts
2104
+ async function importUser(store, user) {
2105
+ const cols = [];
2106
+ const vals = [];
2107
+ const placeholders = [];
2108
+ const add = (col, val) => {
2109
+ cols.push(col);
2110
+ vals.push(val);
2111
+ placeholders.push(`$${vals.length}`);
2112
+ };
2113
+ if (user.id !== void 0) add("id", user.id);
2114
+ add("email", user.email.toLowerCase().trim());
2115
+ if (user.passwordHash !== void 0) add("password_hash", user.passwordHash);
2116
+ if (user.firstName !== void 0) add("first_name", user.firstName);
2117
+ if (user.lastName !== void 0) add("last_name", user.lastName);
2118
+ if (user.phone !== void 0) add("phone", user.phone);
2119
+ if (user.profileImageUrl !== void 0) add("profile_image_url", user.profileImageUrl);
2120
+ if (user.locale !== void 0) add("locale", user.locale);
2121
+ if (user.timezone !== void 0) add("timezone", user.timezone);
2122
+ if (user.emailVerifiedAt !== void 0) add("email_verified_at", user.emailVerifiedAt);
2123
+ if (user.mfaEnabled !== void 0) add("mfa_enabled", user.mfaEnabled);
2124
+ if (user.createdAt !== void 0) add("created_at", user.createdAt);
2125
+ const [row] = await store.query(
2126
+ `INSERT INTO fonderie_users (${cols.join(", ")})
2127
+ VALUES (${placeholders.join(", ")})
2128
+ RETURNING id`,
2129
+ vals
2130
+ );
2131
+ if (!row) throw new Error("[auth] importUser: insert returned no row");
2132
+ return row;
2133
+ }
2016
2134
  export {
2017
2135
  AUTH_CONFIG_KEYS,
2018
2136
  AuthModule,
2019
2137
  MESSAGE_KEYS,
2020
2138
  buildAuthAccountLimiter,
2021
2139
  buildAuthIpLimiter,
2140
+ importUser,
2022
2141
  normalizeEmail,
2023
2142
  normalizeEmailSafe,
2024
2143
  requireAuth2 as requireAuth,
2025
2144
  schemas_exports as schemas,
2026
2145
  toUserDTO,
2027
2146
  validate,
2147
+ validateAuthConfig,
2028
2148
  withSession
2029
2149
  };
2030
2150
  //# sourceMappingURL=index.js.map