@fonderie/auth 3.0.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);
@@ -1240,10 +1256,17 @@ function authController(store, config, bus) {
1240
1256
  if (!user || !user.passwordHash) {
1241
1257
  return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1242
1258
  }
1243
- const valid = await verifyPassword(password2, user.passwordHash);
1259
+ const { valid, needsRehash } = await verifyPasswordForLogin(
1260
+ password2,
1261
+ user.passwordHash,
1262
+ config.legacyVerify
1263
+ );
1244
1264
  if (!valid) {
1245
1265
  return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1246
1266
  }
1267
+ if (needsRehash) {
1268
+ await users.updatePassword(user.id, await hashPassword(password2));
1269
+ }
1247
1270
  if (user.suspended) {
1248
1271
  return (0, import_core4.setApiResponse)(
1249
1272
  import_core4.HTTP.FORBIDDEN,
@@ -1998,6 +2021,53 @@ function buildAuthRoutes(store, config, bus) {
1998
2021
  return routes;
1999
2022
  }
2000
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
+
2001
2071
  // src/middlewares/session.ts
2002
2072
  function withSession(store, config) {
2003
2073
  const users = new UserModel(store);
@@ -2045,11 +2115,16 @@ var AuthModule = class {
2045
2115
  this.store = store;
2046
2116
  this.config = config;
2047
2117
  this.bus = bus;
2118
+ validateAuthConfig(config);
2048
2119
  }
2049
2120
  store;
2050
2121
  config;
2051
2122
  bus;
2052
2123
  name = "@fonderie/auth";
2124
+ // Report config problems for app.checkProductionReadiness() (data, not throw).
2125
+ checkReadiness() {
2126
+ return collectAuthConfigProblems(this.config);
2127
+ }
2053
2128
  install(app) {
2054
2129
  app.use(withSession(this.store, this.config));
2055
2130
  const routes = buildAuthRoutes(this.store, this.config, this.bus);
@@ -2061,6 +2136,38 @@ var AuthModule = class {
2061
2136
 
2062
2137
  // src/middlewares/require-auth.ts
2063
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
+ }
2064
2171
  // Annotate the CommonJS export names for ESM import in node:
2065
2172
  0 && (module.exports = {
2066
2173
  AUTH_CONFIG_KEYS,
@@ -2068,12 +2175,14 @@ var import_middlewares3 = require("@fonderie/core/middlewares");
2068
2175
  MESSAGE_KEYS,
2069
2176
  buildAuthAccountLimiter,
2070
2177
  buildAuthIpLimiter,
2178
+ importUser,
2071
2179
  normalizeEmail,
2072
2180
  normalizeEmailSafe,
2073
2181
  requireAuth,
2074
2182
  schemas,
2075
2183
  toUserDTO,
2076
2184
  validate,
2185
+ validateAuthConfig,
2077
2186
  withSession
2078
2187
  });
2079
2188
  //# sourceMappingURL=index.cjs.map