@fonderie/auth 1.0.0 → 1.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/dist/index.cjs CHANGED
@@ -59,14 +59,16 @@ __export(index_exports, {
59
59
  MESSAGE_KEYS: () => MESSAGE_KEYS,
60
60
  normalizeEmail: () => normalizeEmail,
61
61
  normalizeEmailSafe: () => normalizeEmailSafe,
62
- requireAuth: () => import_middlewares2.requireAuth,
62
+ requireAuth: () => import_middlewares3.requireAuth,
63
+ schemas: () => schemas_exports,
63
64
  toUserDTO: () => toUserDTO,
65
+ validate: () => import_middlewares.validate,
64
66
  withSession: () => withSession
65
67
  });
66
68
  module.exports = __toCommonJS(index_exports);
67
69
 
68
70
  // src/routes.ts
69
- var import_middlewares = require("@fonderie/core/middlewares");
71
+ var import_middlewares2 = require("@fonderie/core/middlewares");
70
72
 
71
73
  // src/middlewares/require-email-login.ts
72
74
  var import_core = require("@fonderie/core");
@@ -81,6 +83,94 @@ var requireEmailLogin = async (ctx, next) => {
81
83
  return next();
82
84
  };
83
85
 
86
+ // src/middlewares/validate.ts
87
+ var import_middlewares = require("@fonderie/core/middlewares");
88
+
89
+ // src/schemas.ts
90
+ var schemas_exports = {};
91
+ __export(schemas_exports, {
92
+ changePasswordSchema: () => changePasswordSchema,
93
+ forgotPasswordSchema: () => forgotPasswordSchema,
94
+ loginSchema: () => loginSchema,
95
+ mfaTokenSchema: () => mfaTokenSchema,
96
+ refreshSchema: () => refreshSchema,
97
+ registerSchema: () => registerSchema,
98
+ resetPasswordSchema: () => resetPasswordSchema,
99
+ updateEmailSchema: () => updateEmailSchema,
100
+ updatePhoneSchema: () => updatePhoneSchema,
101
+ updatePreferencesSchema: () => updatePreferencesSchema,
102
+ updateProfileSchema: () => updateProfileSchema,
103
+ verifySchema: () => verifySchema
104
+ });
105
+ var import_zod = require("zod");
106
+ var email = import_zod.z.string().trim().pipe(import_zod.z.email());
107
+ var password = import_zod.z.string().min(8, "password must be at least 8 characters").max(128);
108
+ var phone = import_zod.z.string().refine((v) => /^\+?[1-9]\d{6,14}$/.test(v.replace(/[\s\-()]/g, "")), "Invalid phone number");
109
+ var sixDigitPin = import_zod.z.string().trim().regex(/^\d{6}$/, "must be a 6-digit code");
110
+ var registerSchema = import_zod.z.union([
111
+ import_zod.z.object({
112
+ email,
113
+ password,
114
+ firstName: import_zod.z.string().max(100).nullish(),
115
+ lastName: import_zod.z.string().max(100).nullish()
116
+ }),
117
+ import_zod.z.object({ phone })
118
+ ]);
119
+ var loginSchema = import_zod.z.union([
120
+ import_zod.z.object({ email, password: import_zod.z.string().min(1).max(128) }),
121
+ import_zod.z.object({ phone })
122
+ ]);
123
+ var refreshSchema = import_zod.z.object({ refreshToken: import_zod.z.string().min(1).optional() });
124
+ var forgotPasswordSchema = import_zod.z.object({ email });
125
+ var resetPasswordSchema = import_zod.z.object({ pin: sixDigitPin, password });
126
+ var verifySchema = import_zod.z.object({ token: sixDigitPin });
127
+ var updateProfileSchema = import_zod.z.object({
128
+ firstName: import_zod.z.string().max(100).nullable().optional(),
129
+ lastName: import_zod.z.string().max(100).nullable().optional(),
130
+ avatarUrl: import_zod.z.string().trim().pipe(import_zod.z.url()).nullable().optional()
131
+ }).refine(
132
+ (o) => Object.values(o).some((v) => v !== void 0),
133
+ "Provide at least one of: firstName, lastName, avatarUrl"
134
+ );
135
+ var updatePreferencesSchema = import_zod.z.object({
136
+ locale: import_zod.z.string().max(35).optional(),
137
+ timezone: import_zod.z.string().max(64).optional(),
138
+ notifications: import_zod.z.unknown().optional(),
139
+ emailDigest: import_zod.z.unknown().optional(),
140
+ dateFormat: import_zod.z.unknown().optional(),
141
+ timeFormat: import_zod.z.unknown().optional()
142
+ }).refine(
143
+ (o) => Object.values(o).some((v) => v !== void 0),
144
+ "Provide at least one preference field"
145
+ );
146
+ var updateEmailSchema = import_zod.z.object({ email });
147
+ var updatePhoneSchema = import_zod.z.object({ phone });
148
+ var changePasswordSchema = import_zod.z.object({
149
+ currentPassword: import_zod.z.string().min(1).max(128),
150
+ newPassword: password
151
+ });
152
+ var mfaTokenSchema = import_zod.z.object({ token: import_zod.z.string().trim().min(6).max(64) });
153
+
154
+ // src/services/cookies.ts
155
+ function secureAttr(config) {
156
+ const secure = config.secureCookies ?? process.env["NODE_ENV"] === "production";
157
+ return secure ? "; Secure" : "";
158
+ }
159
+ function tokenPairCookies(accessToken, refreshToken, config) {
160
+ const secure = secureAttr(config);
161
+ return [
162
+ `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/${secure}`,
163
+ `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh${secure}`
164
+ ].join(", ");
165
+ }
166
+ function clearedTokenCookies(config) {
167
+ const secure = secureAttr(config);
168
+ return [
169
+ `access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`,
170
+ `refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0${secure}`
171
+ ].join(", ");
172
+ }
173
+
84
174
  // src/controllers/mfa.controller.ts
85
175
  var import_qrcode = __toESM(require("qrcode"), 1);
86
176
  var import_core3 = require("@fonderie/core");
@@ -217,7 +307,7 @@ function base32Encode(buf) {
217
307
  function generateTotpSecret() {
218
308
  return base32Encode((0, import_node_crypto.randomBytes)(20));
219
309
  }
220
- function generateTotpUri(email, secret, issuer) {
310
+ function generateTotpUri(email2, secret, issuer) {
221
311
  const params = new URLSearchParams({
222
312
  secret,
223
313
  issuer,
@@ -225,7 +315,7 @@ function generateTotpUri(email, secret, issuer) {
225
315
  digits: String(DIGITS),
226
316
  period: String(STEP)
227
317
  });
228
- return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email)}?${params}`;
318
+ return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email2)}?${params}`;
229
319
  }
230
320
  function verifyTotpToken(token, secret) {
231
321
  const t = timeCounter();
@@ -317,21 +407,21 @@ var UserModel = class {
317
407
  );
318
408
  return row ?? null;
319
409
  }
320
- async findByEmail(email) {
410
+ async findByEmail(email2) {
321
411
  const [row] = await this.store.query(
322
412
  `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,
323
- [email]
413
+ [email2]
324
414
  );
325
415
  return row ?? null;
326
416
  }
327
- async findByPhone(phone) {
417
+ async findByPhone(phone2) {
328
418
  const [row] = await this.store.query(
329
419
  `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
330
- [phone]
420
+ [phone2]
331
421
  );
332
422
  return row ?? null;
333
423
  }
334
- async findOrCreateByPhone(phone, firstName = null, lastName = null) {
424
+ async findOrCreateByPhone(phone2, firstName = null, lastName = null) {
335
425
  const [row] = await this.store.query(
336
426
  `INSERT INTO fonderie_users (phone, first_name, last_name)
337
427
  VALUES ($1, $2, $3)
@@ -340,16 +430,16 @@ var UserModel = class {
340
430
  last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),
341
431
  updated_at = now()
342
432
  RETURNING id`,
343
- [phone, firstName, lastName]
433
+ [phone2, firstName, lastName]
344
434
  );
345
435
  return row;
346
436
  }
347
- async create(email, passwordHash, firstName, lastName) {
437
+ async create(email2, passwordHash, firstName, lastName) {
348
438
  const [row] = await this.store.query(
349
439
  `INSERT INTO fonderie_users (email, password_hash, first_name, last_name)
350
440
  VALUES ($1, $2, $3, $4)
351
441
  RETURNING id`,
352
- [email.toLowerCase().trim(), passwordHash, firstName, lastName]
442
+ [email2.toLowerCase().trim(), passwordHash, firstName, lastName]
353
443
  );
354
444
  return row ?? null;
355
445
  }
@@ -436,16 +526,16 @@ var UserModel = class {
436
526
  [id]
437
527
  );
438
528
  }
439
- async updateEmail(id, email) {
529
+ async updateEmail(id, email2) {
440
530
  await this.store.query(
441
531
  `UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,
442
- [email.toLowerCase().trim(), id]
532
+ [email2.toLowerCase().trim(), id]
443
533
  );
444
534
  }
445
- async updatePhone(id, phone) {
535
+ async updatePhone(id, phone2) {
446
536
  await this.store.query(
447
537
  `UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,
448
- [phone, id]
538
+ [phone2, id]
449
539
  );
450
540
  }
451
541
  async updatePreferences(id, fields) {
@@ -478,14 +568,14 @@ var UserModel = class {
478
568
  );
479
569
  return row?.mfa_secret ?? null;
480
570
  }
481
- async upsertByProvider(email, provider, providerId) {
571
+ async upsertByProvider(email2, provider, providerId) {
482
572
  const [row] = await this.store.query(
483
573
  `INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
484
574
  VALUES ($1, now(), $2, $3)
485
575
  ON CONFLICT (email) DO UPDATE
486
576
  SET provider = $2, provider_id = $3
487
577
  RETURNING id`,
488
- [email, provider, providerId]
578
+ [email2, provider, providerId]
489
579
  );
490
580
  return row ?? null;
491
581
  }
@@ -665,10 +755,7 @@ function mfaController(store, config, issuer, bus) {
665
755
  {
666
756
  status: 200,
667
757
  headers: {
668
- "Set-Cookie": [
669
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
670
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
671
- ].join(", ")
758
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
672
759
  }
673
760
  }
674
761
  );
@@ -742,11 +829,11 @@ function checkCooldown(lastSentAt, cooldownMs) {
742
829
  init_password();
743
830
 
744
831
  // src/services/email.ts
745
- function normalizeEmail(email) {
746
- if (typeof email !== "string" || email.length === 0) {
832
+ function normalizeEmail(email2) {
833
+ if (typeof email2 !== "string" || email2.length === 0) {
747
834
  throw new Error("Invalid email");
748
835
  }
749
- const lower = email.trim().toLowerCase();
836
+ const lower = email2.trim().toLowerCase();
750
837
  if (lower.length === 0) {
751
838
  throw new Error("Email cannot be empty");
752
839
  }
@@ -766,9 +853,9 @@ function normalizeEmail(email) {
766
853
  }
767
854
  return `${normalizedLocal}@${domain}`;
768
855
  }
769
- function normalizeEmailSafe(email) {
856
+ function normalizeEmailSafe(email2) {
770
857
  try {
771
- return normalizeEmail(email);
858
+ return normalizeEmail(email2);
772
859
  } catch {
773
860
  return null;
774
861
  }
@@ -869,13 +956,13 @@ var PhoneVerificationModel = class {
869
956
  this.store = store;
870
957
  }
871
958
  store;
872
- async upsert(userId, phone, otp, expiresAt) {
959
+ async upsert(userId, phone2, otp, expiresAt) {
873
960
  await this.store.query(
874
961
  `INSERT INTO fonderie_phone_verifications (phone, user_id, otp, expires_at)
875
962
  VALUES ($1, $2, $3, $4)
876
963
  ON CONFLICT (phone) DO UPDATE
877
964
  SET user_id = $2, otp = $3, expires_at = $4, created_at = now()`,
878
- [phone, userId, otp, expiresAt]
965
+ [phone2, userId, otp, expiresAt]
879
966
  );
880
967
  }
881
968
  async findByUser(userId, otp) {
@@ -900,11 +987,11 @@ var PhoneVerificationModel = class {
900
987
  };
901
988
 
902
989
  // src/controllers/auth.controller.ts
903
- function normalizePhone(phone) {
904
- return phone.trim().replace(/[\s()\-\.]/g, "");
990
+ function normalizePhone(phone2) {
991
+ return phone2.trim().replace(/[\s()\-\.]/g, "");
905
992
  }
906
- function isValidPhone(phone) {
907
- return typeof phone === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone(phone));
993
+ function isValidPhone(phone2) {
994
+ return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone(phone2));
908
995
  }
909
996
  function extractRefreshToken(ctx) {
910
997
  const body = ctx.meta["body"];
@@ -925,13 +1012,13 @@ function authController(store, config, bus) {
925
1012
  return {
926
1013
  register: async (ctx) => {
927
1014
  const body = ctx.meta["body"];
928
- const { email, password, phone, firstName = null, lastName = null } = body ?? {};
929
- if (typeof email === "string" && typeof password === "string") {
930
- const normalizedEmail = normalizeEmailSafe(email);
1015
+ const { email: email2, password: password2, phone: phone2, firstName = null, lastName = null } = body ?? {};
1016
+ if (typeof email2 === "string" && typeof password2 === "string") {
1017
+ const normalizedEmail = normalizeEmailSafe(email2);
931
1018
  if (!normalizedEmail) {
932
1019
  return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
933
1020
  }
934
- if (password.length < 8) {
1021
+ if (password2.length < 8) {
935
1022
  return (0, import_core4.setApiResponse)(
936
1023
  import_core4.HTTP.UNPROCESSABLE,
937
1024
  "INVALID_PARAMETER",
@@ -942,7 +1029,7 @@ function authController(store, config, bus) {
942
1029
  if (existing) {
943
1030
  return (0, import_core4.setApiResponse)(import_core4.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Email already registered");
944
1031
  }
945
- const passwordHash = await hashPassword(password);
1032
+ const passwordHash = await hashPassword(password2);
946
1033
  const row = await users.create(
947
1034
  normalizedEmail,
948
1035
  passwordHash,
@@ -1002,27 +1089,24 @@ function authController(store, config, bus) {
1002
1089
  {
1003
1090
  status: 201,
1004
1091
  headers: {
1005
- "Set-Cookie": [
1006
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1007
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1008
- ].join(", ")
1092
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1009
1093
  }
1010
1094
  }
1011
1095
  );
1012
1096
  }
1013
- if (isValidPhone(phone)) {
1014
- const existing = await users.findByPhone(normalizePhone(phone));
1097
+ if (isValidPhone(phone2)) {
1098
+ const existing = await users.findByPhone(normalizePhone(phone2));
1015
1099
  if (existing) {
1016
1100
  return (0, import_core4.setApiResponse)(import_core4.HTTP.CONFLICT, "USER_ALREADY_EXISTS", "Phone already registered");
1017
1101
  }
1018
1102
  const { id } = await users.findOrCreateByPhone(
1019
- normalizePhone(phone),
1103
+ normalizePhone(phone2),
1020
1104
  firstName ?? null,
1021
1105
  lastName ?? null
1022
1106
  );
1023
1107
  const otp = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
1024
1108
  const expiresAt = new Date(Date.now() + OTP_TTL_MS);
1025
- await phoneVerif.upsert(id, normalizePhone(phone), otp, expiresAt);
1109
+ await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
1026
1110
  const user = await users.findById(id);
1027
1111
  if (!user) {
1028
1112
  return (0, import_core4.setApiResponse)(import_core4.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
@@ -1034,7 +1118,7 @@ function authController(store, config, bus) {
1034
1118
  {
1035
1119
  type: MESSAGE_KEYS.phoneOtp,
1036
1120
  data: { otp },
1037
- recipient: { email: null, phone: normalizePhone(phone), deviceToken: null }
1121
+ recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
1038
1122
  },
1039
1123
  reqOpts2
1040
1124
  ).catch(() => {
@@ -1067,10 +1151,7 @@ function authController(store, config, bus) {
1067
1151
  {
1068
1152
  status: 202,
1069
1153
  headers: {
1070
- "Set-Cookie": [
1071
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1072
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1073
- ].join(", ")
1154
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1074
1155
  }
1075
1156
  }
1076
1157
  );
@@ -1084,16 +1165,16 @@ function authController(store, config, bus) {
1084
1165
  login: async (ctx) => {
1085
1166
  const body = ctx.meta["body"];
1086
1167
  if (typeof body?.["email"] === "string" && typeof body?.["password"] === "string") {
1087
- const { email: rawEmail, password } = body;
1088
- const email = normalizeEmailSafe(rawEmail);
1089
- if (!email) {
1168
+ const { email: rawEmail, password: password2 } = body;
1169
+ const email2 = normalizeEmailSafe(rawEmail);
1170
+ if (!email2) {
1090
1171
  return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1091
1172
  }
1092
- const user = await users.findByEmail(email);
1173
+ const user = await users.findByEmail(email2);
1093
1174
  if (!user || !user.passwordHash) {
1094
1175
  return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1095
1176
  }
1096
- const valid = await verifyPassword(password, user.passwordHash);
1177
+ const valid = await verifyPassword(password2, user.passwordHash);
1097
1178
  if (!valid) {
1098
1179
  return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1099
1180
  }
@@ -1129,17 +1210,14 @@ function authController(store, config, bus) {
1129
1210
  {
1130
1211
  status: 200,
1131
1212
  headers: {
1132
- "Set-Cookie": [
1133
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1134
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1135
- ].join(", ")
1213
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1136
1214
  }
1137
1215
  }
1138
1216
  );
1139
1217
  }
1140
- const phone = body?.["phone"];
1141
- if (isValidPhone(phone)) {
1142
- const user = await users.findByPhone(normalizePhone(phone));
1218
+ const phone2 = body?.["phone"];
1219
+ if (isValidPhone(phone2)) {
1220
+ const user = await users.findByPhone(normalizePhone(phone2));
1143
1221
  if (!user) {
1144
1222
  return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
1145
1223
  }
@@ -1152,11 +1230,11 @@ function authController(store, config, bus) {
1152
1230
  }
1153
1231
  const otp = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
1154
1232
  const expiresAt = new Date(Date.now() + OTP_TTL_MS);
1155
- await phoneVerif.upsert(user.id, normalizePhone(phone), otp, expiresAt);
1233
+ await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
1156
1234
  bus?.emit(import_events2.NOTIFICATION_EVENT, {
1157
1235
  type: MESSAGE_KEYS.phoneOtp,
1158
1236
  data: { otp },
1159
- recipient: { email: null, phone: normalizePhone(phone), deviceToken: null }
1237
+ recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
1160
1238
  }).catch(() => {
1161
1239
  });
1162
1240
  const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
@@ -1175,10 +1253,7 @@ function authController(store, config, bus) {
1175
1253
  {
1176
1254
  status: 202,
1177
1255
  headers: {
1178
- "Set-Cookie": [
1179
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1180
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1181
- ].join(", ")
1256
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1182
1257
  }
1183
1258
  }
1184
1259
  );
@@ -1199,10 +1274,7 @@ function authController(store, config, bus) {
1199
1274
  {
1200
1275
  status: 200,
1201
1276
  headers: {
1202
- "Set-Cookie": [
1203
- "access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
1204
- "refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
1205
- ].join(", ")
1277
+ "Set-Cookie": clearedTokenCookies(config)
1206
1278
  }
1207
1279
  }
1208
1280
  );
@@ -1243,10 +1315,7 @@ function authController(store, config, bus) {
1243
1315
  {
1244
1316
  status: 200,
1245
1317
  headers: {
1246
- "Set-Cookie": [
1247
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1248
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1249
- ].join(", ")
1318
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1250
1319
  }
1251
1320
  }
1252
1321
  );
@@ -1257,11 +1326,11 @@ function authController(store, config, bus) {
1257
1326
  if (typeof rawEmail !== "string") {
1258
1327
  return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "email is required");
1259
1328
  }
1260
- const email = normalizeEmailSafe(rawEmail);
1261
- if (!email) {
1329
+ const email2 = normalizeEmailSafe(rawEmail);
1330
+ if (!email2) {
1262
1331
  return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid email address");
1263
1332
  }
1264
- const user = await users.findByEmail(email);
1333
+ const user = await users.findByEmail(email2);
1265
1334
  if (!user) {
1266
1335
  return (0, import_core4.setApiResponse)(
1267
1336
  import_core4.HTTP.OK,
@@ -1274,12 +1343,9 @@ function authController(store, config, bus) {
1274
1343
  const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
1275
1344
  if (remaining > 0) {
1276
1345
  return (0, import_core4.setApiResponse)(
1277
- import_core4.HTTP.TOO_MANY_REQUESTS,
1278
- "VERIFICATION_COOLDOWN",
1279
- "Please wait before requesting a new password reset code.",
1280
- {
1281
- retryAfter: Math.ceil(remaining / 1e3)
1282
- }
1346
+ import_core4.HTTP.OK,
1347
+ "PASSWORD_RESET_EMAIL_SENT",
1348
+ "Password reset email sent (if account exists)."
1283
1349
  );
1284
1350
  }
1285
1351
  const pin = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
@@ -1287,7 +1353,7 @@ function authController(store, config, bus) {
1287
1353
  await passwordReset.create(user.id, pin, expiresAt);
1288
1354
  bus?.emit(import_events2.NOTIFICATION_EVENT, {
1289
1355
  type: MESSAGE_KEYS.passwordReset,
1290
- recipient: { email, phone: null, deviceToken: null },
1356
+ recipient: { email: email2, phone: null, deviceToken: null },
1291
1357
  data: { pin }
1292
1358
  }).catch(() => {
1293
1359
  });
@@ -1300,8 +1366,8 @@ function authController(store, config, bus) {
1300
1366
  resetPassword: async (ctx) => {
1301
1367
  const body = ctx.meta["body"];
1302
1368
  const raw = body?.["pin"];
1303
- const password = body?.["password"];
1304
- if (typeof raw !== "string" || typeof password !== "string") {
1369
+ const password2 = body?.["password"];
1370
+ if (typeof raw !== "string" || typeof password2 !== "string") {
1305
1371
  return (0, import_core4.setApiResponse)(
1306
1372
  import_core4.HTTP.UNPROCESSABLE,
1307
1373
  "INVALID_PARAMETER",
@@ -1315,7 +1381,7 @@ function authController(store, config, bus) {
1315
1381
  "pin must be a 6-digit code"
1316
1382
  );
1317
1383
  }
1318
- if (password.length < 8) {
1384
+ if (password2.length < 8) {
1319
1385
  return (0, import_core4.setApiResponse)(
1320
1386
  import_core4.HTTP.UNPROCESSABLE,
1321
1387
  "INVALID_PARAMETER",
@@ -1327,7 +1393,7 @@ function authController(store, config, bus) {
1327
1393
  if (!row || /* @__PURE__ */ new Date() > row.expiresAt) {
1328
1394
  return (0, import_core4.setApiResponse)(import_core4.HTTP.BAD_REQUEST, "PASSWORD_RESET_FAILED", "Invalid or expired pin");
1329
1395
  }
1330
- const passwordHash = await hashPassword(password);
1396
+ const passwordHash = await hashPassword(password2);
1331
1397
  await store.transaction(async (tx) => {
1332
1398
  await Promise.all([
1333
1399
  tx.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
@@ -1398,10 +1464,7 @@ function authController(store, config, bus) {
1398
1464
  {
1399
1465
  status: 200,
1400
1466
  headers: {
1401
- "Set-Cookie": [
1402
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1403
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1404
- ].join(", ")
1467
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1405
1468
  }
1406
1469
  }
1407
1470
  );
@@ -1434,8 +1497,8 @@ function authController(store, config, bus) {
1434
1497
  const resolved = { ...config, ...config.resolve?.(ctx) };
1435
1498
  const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
1436
1499
  if (ctx.user.loginMethod === "phone") {
1437
- const phone = ctx.user.phone;
1438
- if (!phone) {
1500
+ const phone2 = ctx.user.phone;
1501
+ if (!phone2) {
1439
1502
  return (0, import_core4.setApiResponse)(
1440
1503
  import_core4.HTTP.BAD_REQUEST,
1441
1504
  "NO_PHONE_ON_ACCOUNT",
@@ -1455,11 +1518,11 @@ function authController(store, config, bus) {
1455
1518
  }
1456
1519
  const otp = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
1457
1520
  const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
1458
- await phoneVerif.upsert(ctx.user.id, phone, otp, expiresAt2);
1521
+ await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
1459
1522
  bus?.emit(import_events2.NOTIFICATION_EVENT, {
1460
1523
  type: MESSAGE_KEYS.phoneOtp,
1461
1524
  data: { otp },
1462
- recipient: { email: null, phone, deviceToken: null }
1525
+ recipient: { email: null, phone: phone2, deviceToken: null }
1463
1526
  }).catch(() => {
1464
1527
  });
1465
1528
  return (0, import_core4.setApiResponse)(
@@ -1512,13 +1575,13 @@ function authController(store, config, bus) {
1512
1575
  var import_node_crypto3 = require("crypto");
1513
1576
  var import_core5 = require("@fonderie/core");
1514
1577
  var import_events3 = require("@fonderie/events");
1515
- function normalizePhone2(phone) {
1516
- return phone.trim().replace(/[\s()\-\.]/g, "");
1578
+ function normalizePhone2(phone2) {
1579
+ return phone2.trim().replace(/[\s()\-\.]/g, "");
1517
1580
  }
1518
- function isValidPhone2(phone) {
1519
- return typeof phone === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone));
1581
+ function isValidPhone2(phone2) {
1582
+ return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone2));
1520
1583
  }
1521
- function userController(store, bus) {
1584
+ function userController(store, config, bus) {
1522
1585
  const users = new UserModel(store);
1523
1586
  const emailVerif = new EmailVerificationModel(store);
1524
1587
  const phoneVerif = new PhoneVerificationModel(store);
@@ -1707,10 +1770,7 @@ function userController(store, bus) {
1707
1770
  {
1708
1771
  status: 200,
1709
1772
  headers: {
1710
- "Set-Cookie": [
1711
- "access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
1712
- "refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
1713
- ].join(", ")
1773
+ "Set-Cookie": clearedTokenCookies(config)
1714
1774
  }
1715
1775
  }
1716
1776
  );
@@ -1814,10 +1874,7 @@ function oauthController(store, config) {
1814
1874
  {
1815
1875
  status: 200,
1816
1876
  headers: {
1817
- "Set-Cookie": [
1818
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1819
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1820
- ].join(", ")
1877
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1821
1878
  }
1822
1879
  }
1823
1880
  );
@@ -1827,46 +1884,47 @@ function oauthController(store, config) {
1827
1884
 
1828
1885
  // src/routes.ts
1829
1886
  function buildAuthRoutes(store, config, bus) {
1830
- const user = userController(store, bus);
1887
+ const user = userController(store, config, bus);
1831
1888
  const auth = authController(store, config, bus);
1832
1889
  const oauth = oauthController(store, config);
1833
1890
  const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
1834
- const verifyGate = config.requireVerification ? import_middlewares.requireVerified : (_ctx, next) => next();
1891
+ const verifyGate = config.requireVerification ? import_middlewares2.requireVerified : (_ctx, next) => next();
1835
1892
  const routes = [
1836
1893
  // Registration & Login (Public)
1837
- ["POST", "/auth/register", auth.register],
1838
- ["POST", "/auth/login", auth.login],
1894
+ ["POST", "/auth/register", (0, import_middlewares.validate)(registerSchema), auth.register],
1895
+ ["POST", "/auth/login", (0, import_middlewares.validate)(loginSchema), auth.login],
1839
1896
  // Token Management (Public)
1840
- ["POST", "/auth/refresh", auth.refresh],
1897
+ ["POST", "/auth/refresh", (0, import_middlewares.validate)(refreshSchema), auth.refresh],
1841
1898
  // Email — Password Recovery (Public)
1842
- ["POST", "/auth/email/forgot", auth.forgotPassword],
1843
- ["POST", "/auth/email/reset", auth.resetPassword],
1899
+ ["POST", "/auth/email/forgot", (0, import_middlewares.validate)(forgotPasswordSchema), auth.forgotPassword],
1900
+ ["POST", "/auth/email/reset", (0, import_middlewares.validate)(resetPasswordSchema), auth.resetPassword],
1844
1901
  // Verification (Protected — email or phone, determined by loginMethod)
1845
- ["POST", "/auth/verify", import_middlewares.requireAuth, auth.verify],
1846
- ["GET", "/auth/send-verification", import_middlewares.requireAuth, auth.sendVerification],
1902
+ ["POST", "/auth/verify", import_middlewares2.requireAuth, (0, import_middlewares.validate)(verifySchema), auth.verify],
1903
+ ["GET", "/auth/send-verification", import_middlewares2.requireAuth, auth.sendVerification],
1847
1904
  // Account Management (Protected)
1848
- ["POST", "/auth/logout", import_middlewares.requireAuth, auth.logout],
1905
+ ["POST", "/auth/logout", import_middlewares2.requireAuth, (0, import_middlewares.validate)(refreshSchema), auth.logout],
1849
1906
  // User Profile (Protected; writes also gate on requireVerification)
1850
- ["GET", "/users", import_middlewares.requireAuth, user.me],
1851
- ["PUT", "/users/profile", import_middlewares.requireAuth, verifyGate, user.updateProfile],
1852
- ["PUT", "/users/preferences", import_middlewares.requireAuth, verifyGate, user.updatePreferences],
1853
- ["PUT", "/users/email", import_middlewares.requireAuth, verifyGate, user.updateEmail],
1854
- ["PUT", "/users/phone", import_middlewares.requireAuth, verifyGate, user.updatePhone],
1855
- ["PUT", "/users/password", import_middlewares.requireAuth, user.changePassword],
1856
- ["DELETE", "/users", import_middlewares.requireAuth, verifyGate, user.deleteMe],
1907
+ ["GET", "/users", import_middlewares2.requireAuth, user.me],
1908
+ ["PUT", "/users/profile", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updateProfileSchema), user.updateProfile],
1909
+ ["PUT", "/users/preferences", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updatePreferencesSchema), user.updatePreferences],
1910
+ ["PUT", "/users/email", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updateEmailSchema), user.updateEmail],
1911
+ ["PUT", "/users/phone", import_middlewares2.requireAuth, verifyGate, (0, import_middlewares.validate)(updatePhoneSchema), user.updatePhone],
1912
+ ["PUT", "/users/password", import_middlewares2.requireAuth, (0, import_middlewares.validate)(changePasswordSchema), user.changePassword],
1913
+ ["DELETE", "/users", import_middlewares2.requireAuth, verifyGate, user.deleteMe],
1857
1914
  // MFA (email sessions only — requireVerified is always enforced here
1858
1915
  // because MFA is a security feature and email verification is meaningful)
1859
- ["POST", "/auth/mfa/setup", import_middlewares.requireAuth, requireEmailLogin, import_middlewares.requireVerified, mfa.setup],
1916
+ ["POST", "/auth/mfa/setup", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, mfa.setup],
1860
1917
  // /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
1861
1918
  // and full tokens (setup confirmation), so requireAnyAuth is used here.
1862
- ["POST", "/auth/mfa/verify", import_middlewares.requireAnyAuth, requireEmailLogin, import_middlewares.requireVerified, mfa.verify],
1863
- ["POST", "/auth/mfa/disable", import_middlewares.requireAuth, requireEmailLogin, import_middlewares.requireVerified, mfa.disable],
1919
+ ["POST", "/auth/mfa/verify", import_middlewares2.requireAnyAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.verify],
1920
+ ["POST", "/auth/mfa/disable", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.disable],
1864
1921
  [
1865
1922
  "POST",
1866
1923
  "/auth/mfa/backup-codes",
1867
- import_middlewares.requireAuth,
1924
+ import_middlewares2.requireAuth,
1868
1925
  requireEmailLogin,
1869
- import_middlewares.requireVerified,
1926
+ import_middlewares2.requireVerified,
1927
+ (0, import_middlewares.validate)(mfaTokenSchema),
1870
1928
  mfa.regenerateBackupCodes
1871
1929
  ]
1872
1930
  ];
@@ -1937,7 +1995,7 @@ var AuthModule = class {
1937
1995
  };
1938
1996
 
1939
1997
  // src/middlewares/require-auth.ts
1940
- var import_middlewares2 = require("@fonderie/core/middlewares");
1998
+ var import_middlewares3 = require("@fonderie/core/middlewares");
1941
1999
  // Annotate the CommonJS export names for ESM import in node:
1942
2000
  0 && (module.exports = {
1943
2001
  AUTH_CONFIG_KEYS,
@@ -1946,7 +2004,9 @@ var import_middlewares2 = require("@fonderie/core/middlewares");
1946
2004
  normalizeEmail,
1947
2005
  normalizeEmailSafe,
1948
2006
  requireAuth,
2007
+ schemas,
1949
2008
  toUserDTO,
2009
+ validate,
1950
2010
  withSession
1951
2011
  });
1952
2012
  //# sourceMappingURL=index.cjs.map