@fonderie/auth 7.11.1 → 7.13.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.js CHANGED
@@ -148,14 +148,16 @@ var email = z.string().trim().pipe(z.email());
148
148
  var password = z.string().min(8, "password must be at least 8 characters").max(128);
149
149
  var phone = z.string().refine((v) => /^\+?[1-9]\d{6,14}$/.test(v.replace(/[\s\-()]/g, "")), "Invalid phone number");
150
150
  var sixDigitPin = z.string().trim().regex(/^\d{6}$/, "must be a 6-digit code");
151
+ var signupLocale = z.string().max(35).nullish();
151
152
  var registerSchema = z.union([
152
153
  z.object({
153
154
  email,
154
155
  password,
155
156
  firstName: z.string().max(100).nullish(),
156
- lastName: z.string().max(100).nullish()
157
+ lastName: z.string().max(100).nullish(),
158
+ locale: signupLocale
157
159
  }),
158
- z.object({ phone })
160
+ z.object({ phone, locale: signupLocale })
159
161
  ]);
160
162
  var loginSchema = z.union([
161
163
  z.object({ email, password: z.string().min(1).max(128) }),
@@ -275,6 +277,8 @@ var MESSAGE_KEYS = {
275
277
  var EVENT_KEYS = {
276
278
  userRegistered: "fonderie.user.registered",
277
279
  userDeleted: "fonderie.user.deleted",
280
+ // Emitted by purgeSoftDeletedUsers (given a bus) once the row is hard-deleted.
281
+ userPurged: "fonderie.user.purged",
278
282
  emailVerified: "fonderie.user.email_verified",
279
283
  passwordChanged: "fonderie.user.password_changed"
280
284
  };
@@ -548,7 +552,7 @@ var UserModel = class {
548
552
  async list(query = {}) {
549
553
  const limit = Math.min(query.limit ?? 50, MAX_LIST_LIMIT);
550
554
  const params = [];
551
- const where = ["deleted_at IS NULL"];
555
+ const where = [query.deleted ? "deleted_at IS NOT NULL" : "deleted_at IS NULL"];
552
556
  if (query.cursor) {
553
557
  params.push(query.cursor.createdAt, query.cursor.id);
554
558
  where.push(`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`);
@@ -564,6 +568,16 @@ var UserModel = class {
564
568
  );
565
569
  return { users: rows.slice(0, limit), hasMore: rows.length > limit };
566
570
  }
571
+ // For operator views only: a soft-deleted account still resolves, with
572
+ // deletedAt set, so the console can say "deleted on …" instead of "no such
573
+ // user". Nothing that authenticates or acts on an account may use this.
574
+ async findByIdIncludingDeleted(id) {
575
+ const [row] = await this.store.query(
576
+ `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1`,
577
+ [id]
578
+ );
579
+ return row ?? null;
580
+ }
567
581
  async findById(id) {
568
582
  const [row] = await this.store.query(
569
583
  `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,
@@ -585,25 +599,25 @@ var UserModel = class {
585
599
  );
586
600
  return row ?? null;
587
601
  }
588
- async findOrCreateByPhone(phone2, firstName = null, lastName = null) {
602
+ async findOrCreateByPhone(phone2, firstName = null, lastName = null, locale = null) {
589
603
  const [row] = await this.store.query(
590
- `INSERT INTO fonderie_users (phone, first_name, last_name)
591
- VALUES ($1, $2, $3)
604
+ `INSERT INTO fonderie_users (phone, first_name, last_name, locale)
605
+ VALUES ($1, $2, $3, COALESCE($4, 'en-US'))
592
606
  ON CONFLICT (phone) DO UPDATE
593
607
  SET first_name = COALESCE(EXCLUDED.first_name, fonderie_users.first_name),
594
608
  last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),
595
609
  updated_at = now()
596
610
  RETURNING id`,
597
- [phone2, firstName, lastName]
611
+ [phone2, firstName, lastName, locale]
598
612
  );
599
613
  return row;
600
614
  }
601
- async create(email2, passwordHash, firstName, lastName) {
615
+ async create(email2, passwordHash, firstName, lastName, locale = null) {
602
616
  const [row] = await this.store.query(
603
- `INSERT INTO fonderie_users (email, password_hash, first_name, last_name)
604
- VALUES ($1, $2, $3, $4)
617
+ `INSERT INTO fonderie_users (email, password_hash, first_name, last_name, locale)
618
+ VALUES ($1, $2, $3, $4, COALESCE($5, 'en-US'))
605
619
  RETURNING id`,
606
- [email2.toLowerCase().trim(), passwordHash, firstName, lastName]
620
+ [email2.toLowerCase().trim(), passwordHash, firstName, lastName, locale]
607
621
  );
608
622
  return row ?? null;
609
623
  }
@@ -1362,7 +1376,7 @@ function mfaController(store, config, issuer, bus) {
1362
1376
  // src/controllers/auth.controller.ts
1363
1377
  import { randomBytes as randomBytes3, randomInt } from "crypto";
1364
1378
  import { NOTIFICATION_EVENT as NOTIFICATION_EVENT2 } from "@fonderie/events";
1365
- import { setApiResponse as setApiResponse3, HTTP as HTTP3, background as background3 } from "@fonderie/core";
1379
+ import { setApiResponse as setApiResponse3, HTTP as HTTP3, background as background3, canonicalLocale, defineLocales } from "@fonderie/core";
1366
1380
 
1367
1381
  // src/services/cooldown.ts
1368
1382
  function checkCooldown(lastSentAt, cooldownMs) {
@@ -1562,7 +1576,7 @@ function extractRefreshToken(ctx) {
1562
1576
  const match = cookie.match(/(?:^|;\s*)refresh_token=([^;]+)/);
1563
1577
  return match?.[1] ?? null;
1564
1578
  }
1565
- function authController(store, config, bus) {
1579
+ function authController(store, config, bus, locales = defineLocales()) {
1566
1580
  const users = new UserModel(store);
1567
1581
  const sessions = new SessionModel(store, config.location);
1568
1582
  const loginEvents = new LoginEventModel(store, config.location);
@@ -1574,6 +1588,7 @@ function authController(store, config, bus) {
1574
1588
  register: async (ctx) => {
1575
1589
  const body = ctx.meta["body"];
1576
1590
  const { email: email2, password: password2, phone: phone2, firstName = null, lastName = null } = body ?? {};
1591
+ const locale = canonicalLocale(body?.["locale"]) ?? locales.default;
1577
1592
  if (typeof email2 === "string" && typeof password2 === "string") {
1578
1593
  const normalizedEmail = normalizeEmailSafe(email2);
1579
1594
  if (!normalizedEmail) {
@@ -1595,7 +1610,8 @@ function authController(store, config, bus) {
1595
1610
  normalizedEmail,
1596
1611
  passwordHash,
1597
1612
  firstName,
1598
- lastName
1613
+ lastName,
1614
+ locale
1599
1615
  );
1600
1616
  if (!row) {
1601
1617
  return setApiResponse3(HTTP3.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
@@ -1668,7 +1684,8 @@ function authController(store, config, bus) {
1668
1684
  const { id } = await users.findOrCreateByPhone(
1669
1685
  normalizePhone(phone2),
1670
1686
  firstName ?? null,
1671
- lastName ?? null
1687
+ lastName ?? null,
1688
+ locale
1672
1689
  );
1673
1690
  const otp = randomInt(1e5, 1e6).toString();
1674
1691
  const expiresAt = new Date(Date.now() + OTP_TTL_MS);
@@ -3079,9 +3096,9 @@ function oauthController(store, config, bus) {
3079
3096
  }
3080
3097
 
3081
3098
  // src/routes.ts
3082
- function buildAuthRoutes(store, config, bus) {
3099
+ function buildAuthRoutes(store, config, bus, locales) {
3083
3100
  const user = userController(store, config, bus);
3084
- const auth = authController(store, config, bus);
3101
+ const auth = authController(store, config, bus, locales);
3085
3102
  const oauth = oauthController(store, config, bus);
3086
3103
  const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
3087
3104
  const verifyGate = config.requireVerification ? requireVerified : (_ctx, next) => next();
@@ -3381,7 +3398,12 @@ function adminRouteTable(store) {
3381
3398
  const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null;
3382
3399
  if (cursorParam && !cursor)
3383
3400
  return setApiResponse7(HTTP7.UNPROCESSABLE, "INVALID_PARAMETER", "Invalid cursor");
3384
- const page = await users.list({ limit, ...cursor ? { cursor } : {} });
3401
+ const deleted = params.get("deleted") === "1" || params.get("deleted") === "true";
3402
+ const page = await users.list({
3403
+ limit,
3404
+ ...cursor ? { cursor } : {},
3405
+ ...deleted ? { deleted } : {}
3406
+ });
3385
3407
  return setApiResponse7(HTTP7.OK, "USERS", "Users", toAdminUserPageDTO(page));
3386
3408
  }
3387
3409
  ],
@@ -3389,7 +3411,7 @@ function adminRouteTable(store) {
3389
3411
  "GET",
3390
3412
  "/_admin/users/:id",
3391
3413
  async (ctx) => {
3392
- const user = await users.findById(idOf(ctx));
3414
+ const user = await users.findByIdIncludingDeleted(idOf(ctx));
3393
3415
  return user ? setApiResponse7(HTTP7.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
3394
3416
  }
3395
3417
  ],
@@ -3468,7 +3490,7 @@ var AuthModule = class {
3468
3490
  name = "@fonderie/auth";
3469
3491
  // Baked in at build time by tsup.base, so the operator's Modules page can
3470
3492
  // say what is actually deployed rather than 'not reported'.
3471
- version = "7.11.1";
3493
+ version = "7.13.0";
3472
3494
  // Users, sessions, login history, suspend — only through @fonderie/admin.
3473
3495
  describeAdmin() {
3474
3496
  return { routes: describeAuthAdminRoutes(this.store) };
@@ -3479,7 +3501,7 @@ var AuthModule = class {
3479
3501
  }
3480
3502
  install(app) {
3481
3503
  app.use(withSession(this.store, this.config));
3482
- const routes = buildAuthRoutes(this.store, this.config, this.bus);
3504
+ const routes = buildAuthRoutes(this.store, this.config, this.bus, app.locales);
3483
3505
  for (const [method, path, ...handlers] of routes) {
3484
3506
  app.addRoute(method, path, ...handlers);
3485
3507
  }
@@ -3487,7 +3509,328 @@ var AuthModule = class {
3487
3509
  };
3488
3510
 
3489
3511
  // src/templates.ts
3490
- var DEFAULT_TEMPLATES = {
3512
+ import { withTranslations } from "@fonderie/core";
3513
+
3514
+ // src/templates.es.ts
3515
+ var ES_TEMPLATES = {
3516
+ [MESSAGE_KEYS.emailRegistration]: {
3517
+ subject: "Confirma tu cuenta",
3518
+ html: `<h1>Te damos la bienvenida</h1>
3519
+ <p>Hola {{firstName}}:</p>
3520
+ <p>Gracias por registrarte. Confirma tu cuenta con este c\xF3digo:</p>
3521
+ <p><span class="pin-code">{{pin}}</span></p>
3522
+ <p class="muted">Este c\xF3digo caduca en 24 horas. Si no creaste una cuenta, puedes ignorar este correo.</p>`,
3523
+ text: `Te damos la bienvenida
3524
+
3525
+ Hola {{firstName}}:
3526
+
3527
+ Gracias por registrarte. Confirma tu cuenta con este c\xF3digo: {{pin}}
3528
+
3529
+ Este c\xF3digo caduca en 24 horas. Si no creaste una cuenta, puedes ignorar este correo.`
3530
+ },
3531
+ [MESSAGE_KEYS.emailVerification]: {
3532
+ subject: "Tu c\xF3digo de verificaci\xF3n",
3533
+ html: `<h1>Verifica tu correo electr\xF3nico</h1>
3534
+ <p>Hola {{firstName}}:</p>
3535
+ <p>Usa este c\xF3digo para terminar de configurar tu cuenta:</p>
3536
+ <p><span class="pin-code">{{pin}}</span></p>
3537
+ <p class="muted">Este c\xF3digo caduca en 24 horas. Si no creaste una cuenta, puedes ignorar este correo.</p>`,
3538
+ text: `Verifica tu correo electr\xF3nico
3539
+
3540
+ Hola {{firstName}}:
3541
+
3542
+ Usa este c\xF3digo para terminar de configurar tu cuenta: {{pin}}
3543
+
3544
+ Este c\xF3digo caduca en 24 horas. Si no creaste una cuenta, puedes ignorar este correo.`
3545
+ },
3546
+ [MESSAGE_KEYS.passwordReset]: {
3547
+ subject: "Restablece tu contrase\xF1a",
3548
+ html: `<h1>Restablece tu contrase\xF1a</h1>
3549
+ <p>Recibimos una solicitud para restablecer tu contrase\xF1a. Usa este c\xF3digo para continuar:</p>
3550
+ <p><span class="pin-code">{{pin}}</span></p>
3551
+ <p class="muted">Este c\xF3digo caduca pronto. Si no lo solicitaste, no tienes que hacer nada &mdash; tu contrase\xF1a sigue siendo la misma.</p>`,
3552
+ text: `Restablece tu contrase\xF1a
3553
+
3554
+ Recibimos una solicitud para restablecer tu contrase\xF1a. Usa este c\xF3digo para continuar: {{pin}}
3555
+
3556
+ Este c\xF3digo caduca pronto. Si no lo solicitaste, no tienes que hacer nada \u2014 tu contrase\xF1a sigue siendo la misma.`
3557
+ },
3558
+ // SMS — text only.
3559
+ [MESSAGE_KEYS.phoneOtp]: {
3560
+ text: "{{otp}} es tu c\xF3digo de verificaci\xF3n. Caduca en 10 minutos."
3561
+ },
3562
+ [MESSAGE_KEYS.mfaEnabled]: {
3563
+ subject: "La verificaci\xF3n en dos pasos est\xE1 activada",
3564
+ html: `<h1>Verificaci\xF3n en dos pasos activada</h1>
3565
+ <p>Se acaba de activar la verificaci\xF3n en dos pasos en tu cuenta. A partir de ahora, introducir\xE1s un c\xF3digo de tu aplicaci\xF3n de autenticaci\xF3n al iniciar sesi\xF3n.</p>
3566
+ <p class="muted">Si no fuiste t\xFA, contacta con soporte de inmediato &mdash; alguien podr\xEDa tener acceso a tu cuenta.</p>`,
3567
+ text: `Verificaci\xF3n en dos pasos activada
3568
+
3569
+ Se acaba de activar la verificaci\xF3n en dos pasos en tu cuenta. A partir de ahora, introducir\xE1s un c\xF3digo de tu aplicaci\xF3n de autenticaci\xF3n al iniciar sesi\xF3n.
3570
+
3571
+ Si no fuiste t\xFA, contacta con soporte de inmediato \u2014 alguien podr\xEDa tener acceso a tu cuenta.`
3572
+ },
3573
+ [MESSAGE_KEYS.mfaDisabled]: {
3574
+ subject: "La verificaci\xF3n en dos pasos est\xE1 desactivada",
3575
+ html: `<h1>Verificaci\xF3n en dos pasos desactivada</h1>
3576
+ <p>Se acaba de desactivar la verificaci\xF3n en dos pasos en tu cuenta. Ahora tu cuenta est\xE1 protegida solo por tu contrase\xF1a.</p>
3577
+ <p class="muted">Si no fuiste t\xFA, contacta con soporte de inmediato y vuelve a activar la verificaci\xF3n en dos pasos &mdash; alguien podr\xEDa tener acceso a tu cuenta.</p>`,
3578
+ text: `Verificaci\xF3n en dos pasos desactivada
3579
+
3580
+ Se acaba de desactivar la verificaci\xF3n en dos pasos en tu cuenta. Ahora tu cuenta est\xE1 protegida solo por tu contrase\xF1a.
3581
+
3582
+ Si no fuiste t\xFA, contacta con soporte de inmediato y vuelve a activar la verificaci\xF3n en dos pasos \u2014 alguien podr\xEDa tener acceso a tu cuenta.`
3583
+ },
3584
+ [MESSAGE_KEYS.mfaBackupCodesRegenerated]: {
3585
+ subject: "Se regeneraron tus c\xF3digos de respaldo",
3586
+ html: `<h1>Nuevos c\xF3digos de respaldo generados</h1>
3587
+ <p>Se acaba de generar un nuevo conjunto de c\xF3digos de respaldo de verificaci\xF3n en dos pasos para tu cuenta. Tus c\xF3digos anteriores ya no funcionan.</p>
3588
+ <p class="muted">Si no fuiste t\xFA, contacta con soporte de inmediato &mdash; alguien podr\xEDa tener acceso a tu cuenta.</p>`,
3589
+ text: `Nuevos c\xF3digos de respaldo generados
3590
+
3591
+ Se acaba de generar un nuevo conjunto de c\xF3digos de respaldo de verificaci\xF3n en dos pasos para tu cuenta. Tus c\xF3digos anteriores ya no funcionan.
3592
+
3593
+ Si no fuiste t\xFA, contacta con soporte de inmediato \u2014 alguien podr\xEDa tener acceso a tu cuenta.`
3594
+ },
3595
+ [MESSAGE_KEYS.emailChanged]: {
3596
+ subject: "Se cambi\xF3 tu direcci\xF3n de correo electr\xF3nico",
3597
+ html: `<h1>Se cambi\xF3 tu correo electr\xF3nico</h1>
3598
+ <p>La direcci\xF3n de correo electr\xF3nico de tu cuenta se acaba de cambiar a <strong>{{newEmail}}</strong>.</p>
3599
+ <p class="muted">Si hiciste este cambio, todo est\xE1 en orden. Si no, contacta con soporte de inmediato &mdash; alguien podr\xEDa tener acceso a tu cuenta.</p>`,
3600
+ text: `Se cambi\xF3 tu correo electr\xF3nico
3601
+
3602
+ La direcci\xF3n de correo electr\xF3nico de tu cuenta se acaba de cambiar a {{newEmail}}.
3603
+
3604
+ Si hiciste este cambio, todo est\xE1 en orden. Si no, contacta con soporte de inmediato \u2014 alguien podr\xEDa tener acceso a tu cuenta.`
3605
+ },
3606
+ [MESSAGE_KEYS.phoneChanged]: {
3607
+ subject: "Se cambi\xF3 tu n\xFAmero de tel\xE9fono",
3608
+ html: `<h1>Se cambi\xF3 tu n\xFAmero de tel\xE9fono</h1>
3609
+ <p>El n\xFAmero de tel\xE9fono de tu cuenta se acaba de actualizar.</p>
3610
+ <p class="muted">Si hiciste este cambio, todo est\xE1 en orden. Si no, contacta con soporte de inmediato &mdash; alguien podr\xEDa tener acceso a tu cuenta.</p>`,
3611
+ text: `Se cambi\xF3 tu n\xFAmero de tel\xE9fono
3612
+
3613
+ El n\xFAmero de tel\xE9fono de tu cuenta se acaba de actualizar.
3614
+
3615
+ Si hiciste este cambio, todo est\xE1 en orden. Si no, contacta con soporte de inmediato \u2014 alguien podr\xEDa tener acceso a tu cuenta.`
3616
+ },
3617
+ [MESSAGE_KEYS.passwordRevoked]: {
3618
+ subject: "Se elimin\xF3 la contrase\xF1a de tu cuenta",
3619
+ html: `<h1>Se elimin\xF3 tu contrase\xF1a</h1>
3620
+ <p>Acabas de iniciar sesi\xF3n con <strong>{{provider}}</strong>. Esta cuenta ten\xEDa una contrase\xF1a, pero esta direcci\xF3n de correo nunca se hab\xEDa confirmado &mdash; as\xED que no pod\xEDamos saber qui\xE9n la estableci\xF3, y se ha eliminado.</p>
3621
+ <p>Iniciar sesi\xF3n con {{provider}} funciona como antes. Si tambi\xE9n quieres una contrase\xF1a, puedes crearla en la configuraci\xF3n de tu cuenta.</p>
3622
+ <p class="muted">Si nunca estableciste una contrase\xF1a aqu\xED, es posible que alguien intentara registrarse con tu direcci\xF3n. No tienes que hacer nada m\xE1s &mdash; la cuenta es tuya.</p>`,
3623
+ text: `Se elimin\xF3 tu contrase\xF1a
3624
+
3625
+ Acabas de iniciar sesi\xF3n con {{provider}}. Esta cuenta ten\xEDa una contrase\xF1a, pero
3626
+ esta direcci\xF3n de correo nunca se hab\xEDa confirmado \u2014 as\xED que no pod\xEDamos saber
3627
+ qui\xE9n la estableci\xF3, y se ha eliminado.
3628
+
3629
+ Iniciar sesi\xF3n con {{provider}} funciona como antes. Si tambi\xE9n quieres una
3630
+ contrase\xF1a, puedes crearla en la configuraci\xF3n de tu cuenta.
3631
+
3632
+ Si nunca estableciste una contrase\xF1a aqu\xED, es posible que alguien intentara
3633
+ registrarse con tu direcci\xF3n. No tienes que hacer nada m\xE1s \u2014 la cuenta es tuya.`
3634
+ },
3635
+ [MESSAGE_KEYS.oauthRegistration]: {
3636
+ subject: "Te damos la bienvenida a {{appName}}",
3637
+ html: `<h1>Te damos la bienvenida a {{appName}}</h1>
3638
+ <p>Tu cuenta se cre\xF3 con <strong>{{provider}}</strong>. Inicia sesi\xF3n cuando quieras con la misma cuenta de {{provider}} &mdash; no hay ninguna contrase\xF1a que recordar.</p>
3639
+ <p class="muted">Si no creaste esta cuenta, contacta con soporte.</p>`,
3640
+ text: `Te damos la bienvenida
3641
+
3642
+ Tu cuenta se cre\xF3 con {{provider}}. Inicia sesi\xF3n cuando quieras con la misma
3643
+ cuenta de {{provider}} \u2014 no hay ninguna contrase\xF1a que recordar.
3644
+
3645
+ Si no creaste esta cuenta, contacta con soporte.`
3646
+ },
3647
+ [MESSAGE_KEYS.oauthLinked]: {
3648
+ subject: "Se a\xF1adi\xF3 un nuevo m\xE9todo de inicio de sesi\xF3n a tu cuenta",
3649
+ html: `<h1>Se a\xF1adi\xF3 el inicio de sesi\xF3n con {{provider}}</h1>
3650
+ <p>Ahora tambi\xE9n puedes iniciar sesi\xF3n en tu cuenta con <strong>{{provider}}</strong>.</p>
3651
+ <p class="muted">Si fuiste t\xFA, no tienes que hacer nada m\xE1s. Si no, contacta con soporte de inmediato y cambia tu contrase\xF1a &mdash; otra persona podr\xEDa iniciar sesi\xF3n como t\xFA.</p>`,
3652
+ text: `Se a\xF1adi\xF3 el inicio de sesi\xF3n con {{provider}}
3653
+
3654
+ Ahora tambi\xE9n puedes iniciar sesi\xF3n en tu cuenta con {{provider}}.
3655
+
3656
+ Si fuiste t\xFA, no tienes que hacer nada m\xE1s. Si no, contacta con soporte de
3657
+ inmediato y cambia tu contrase\xF1a \u2014 otra persona podr\xEDa iniciar sesi\xF3n como t\xFA.`
3658
+ },
3659
+ [MESSAGE_KEYS.oauthUnlinked]: {
3660
+ subject: "Se elimin\xF3 un m\xE9todo de inicio de sesi\xF3n de tu cuenta",
3661
+ html: `<h1>Se elimin\xF3 el inicio de sesi\xF3n con {{provider}}</h1>
3662
+ <p><strong>{{provider}}</strong> ya no se puede usar para iniciar sesi\xF3n. Tu correo electr\xF3nico y tu contrase\xF1a siguen funcionando.</p>
3663
+ <p class="muted">Si fuiste t\xFA, todo est\xE1 en orden. Si no, contacta con soporte de inmediato.</p>`,
3664
+ text: `Se elimin\xF3 el inicio de sesi\xF3n con {{provider}}
3665
+
3666
+ {{provider}} ya no se puede usar para iniciar sesi\xF3n. Tu correo electr\xF3nico y tu contrase\xF1a siguen funcionando.
3667
+
3668
+ Si fuiste t\xFA, todo est\xE1 en orden. Si no, contacta con soporte de inmediato.`
3669
+ }
3670
+ };
3671
+
3672
+ // src/templates.fr.ts
3673
+ var FR_TEMPLATES = {
3674
+ [MESSAGE_KEYS.emailRegistration]: {
3675
+ subject: "Confirmez votre compte",
3676
+ html: `<h1>Bienvenue</h1>
3677
+ <p>Bonjour {{firstName}},</p>
3678
+ <p>Merci pour votre inscription. Confirmez votre compte avec ce code :</p>
3679
+ <p><span class="pin-code">{{pin}}</span></p>
3680
+ <p class="muted">Ce code expire dans 24 heures. Si vous n&rsquo;avez pas cr\xE9\xE9 de compte, vous pouvez ignorer cet e-mail.</p>`,
3681
+ text: `Bienvenue
3682
+
3683
+ Bonjour {{firstName}},
3684
+
3685
+ Merci pour votre inscription. Confirmez votre compte avec ce code : {{pin}}
3686
+
3687
+ Ce code expire dans 24 heures. Si vous n'avez pas cr\xE9\xE9 de compte, vous pouvez ignorer cet e-mail.`
3688
+ },
3689
+ [MESSAGE_KEYS.emailVerification]: {
3690
+ subject: "Votre code de v\xE9rification",
3691
+ html: `<h1>V\xE9rifiez votre adresse e-mail</h1>
3692
+ <p>Bonjour {{firstName}},</p>
3693
+ <p>Utilisez ce code pour terminer la cr\xE9ation de votre compte :</p>
3694
+ <p><span class="pin-code">{{pin}}</span></p>
3695
+ <p class="muted">Ce code expire dans 24 heures. Si vous n&rsquo;avez pas cr\xE9\xE9 de compte, vous pouvez ignorer cet e-mail.</p>`,
3696
+ text: `V\xE9rifiez votre adresse e-mail
3697
+
3698
+ Bonjour {{firstName}},
3699
+
3700
+ Utilisez ce code pour terminer la cr\xE9ation de votre compte : {{pin}}
3701
+
3702
+ Ce code expire dans 24 heures. Si vous n'avez pas cr\xE9\xE9 de compte, vous pouvez ignorer cet e-mail.`
3703
+ },
3704
+ [MESSAGE_KEYS.passwordReset]: {
3705
+ subject: "R\xE9initialisez votre mot de passe",
3706
+ html: `<h1>R\xE9initialisez votre mot de passe</h1>
3707
+ <p>Nous avons re\xE7u une demande de r\xE9initialisation de votre mot de passe. Utilisez ce code pour continuer :</p>
3708
+ <p><span class="pin-code">{{pin}}</span></p>
3709
+ <p class="muted">Ce code expire bient\xF4t. Si vous n&rsquo;avez rien demand\xE9, aucune action n&rsquo;est n\xE9cessaire &mdash; votre mot de passe reste inchang\xE9.</p>`,
3710
+ text: `R\xE9initialisez votre mot de passe
3711
+
3712
+ Nous avons re\xE7u une demande de r\xE9initialisation de votre mot de passe. Utilisez ce code pour continuer : {{pin}}
3713
+
3714
+ Ce code expire bient\xF4t. Si vous n'avez rien demand\xE9, aucune action n'est n\xE9cessaire \u2014 votre mot de passe reste inchang\xE9.`
3715
+ },
3716
+ // SMS — text only.
3717
+ [MESSAGE_KEYS.phoneOtp]: {
3718
+ text: "{{otp}} est votre code de v\xE9rification. Il expire dans 10 minutes."
3719
+ },
3720
+ [MESSAGE_KEYS.mfaEnabled]: {
3721
+ subject: "La double authentification est activ\xE9e",
3722
+ html: `<h1>Double authentification activ\xE9e</h1>
3723
+ <p>La double authentification vient d&rsquo;\xEAtre activ\xE9e sur votre compte. D\xE9sormais, vous saisirez un code de votre application d&rsquo;authentification \xE0 chaque connexion.</p>
3724
+ <p class="muted">Si ce n&rsquo;est pas vous, contactez imm\xE9diatement le support &mdash; quelqu&rsquo;un pourrait avoir acc\xE8s \xE0 votre compte.</p>`,
3725
+ text: `Double authentification activ\xE9e
3726
+
3727
+ La double authentification vient d'\xEAtre activ\xE9e sur votre compte. D\xE9sormais, vous saisirez un code de votre application d'authentification \xE0 chaque connexion.
3728
+
3729
+ Si ce n'est pas vous, contactez imm\xE9diatement le support \u2014 quelqu'un pourrait avoir acc\xE8s \xE0 votre compte.`
3730
+ },
3731
+ [MESSAGE_KEYS.mfaDisabled]: {
3732
+ subject: "La double authentification est d\xE9sactiv\xE9e",
3733
+ html: `<h1>Double authentification d\xE9sactiv\xE9e</h1>
3734
+ <p>La double authentification vient d&rsquo;\xEAtre d\xE9sactiv\xE9e sur votre compte. Votre compte n&rsquo;est d\xE9sormais prot\xE9g\xE9 que par votre mot de passe.</p>
3735
+ <p class="muted">Si ce n&rsquo;est pas vous, contactez imm\xE9diatement le support et r\xE9activez la double authentification &mdash; quelqu&rsquo;un pourrait avoir acc\xE8s \xE0 votre compte.</p>`,
3736
+ text: `Double authentification d\xE9sactiv\xE9e
3737
+
3738
+ La double authentification vient d'\xEAtre d\xE9sactiv\xE9e sur votre compte. Votre compte n'est d\xE9sormais prot\xE9g\xE9 que par votre mot de passe.
3739
+
3740
+ Si ce n'est pas vous, contactez imm\xE9diatement le support et r\xE9activez la double authentification \u2014 quelqu'un pourrait avoir acc\xE8s \xE0 votre compte.`
3741
+ },
3742
+ [MESSAGE_KEYS.mfaBackupCodesRegenerated]: {
3743
+ subject: "Vos codes de secours ont \xE9t\xE9 r\xE9g\xE9n\xE9r\xE9s",
3744
+ html: `<h1>Nouveaux codes de secours g\xE9n\xE9r\xE9s</h1>
3745
+ <p>Une nouvelle s\xE9rie de codes de secours pour la double authentification vient d&rsquo;\xEAtre g\xE9n\xE9r\xE9e pour votre compte. Vos anciens codes ne fonctionnent plus.</p>
3746
+ <p class="muted">Si ce n&rsquo;est pas vous, contactez imm\xE9diatement le support &mdash; quelqu&rsquo;un pourrait avoir acc\xE8s \xE0 votre compte.</p>`,
3747
+ text: `Nouveaux codes de secours g\xE9n\xE9r\xE9s
3748
+
3749
+ Une nouvelle s\xE9rie de codes de secours pour la double authentification vient d'\xEAtre g\xE9n\xE9r\xE9e pour votre compte. Vos anciens codes ne fonctionnent plus.
3750
+
3751
+ Si ce n'est pas vous, contactez imm\xE9diatement le support \u2014 quelqu'un pourrait avoir acc\xE8s \xE0 votre compte.`
3752
+ },
3753
+ [MESSAGE_KEYS.emailChanged]: {
3754
+ subject: "Votre adresse e-mail a \xE9t\xE9 modifi\xE9e",
3755
+ html: `<h1>Votre adresse e-mail a \xE9t\xE9 modifi\xE9e</h1>
3756
+ <p>L&rsquo;adresse e-mail de votre compte vient d&rsquo;\xEAtre remplac\xE9e par <strong>{{newEmail}}</strong>.</p>
3757
+ <p class="muted">Si vous \xEAtes \xE0 l&rsquo;origine de ce changement, tout est en ordre. Sinon, contactez imm\xE9diatement le support &mdash; quelqu&rsquo;un pourrait avoir acc\xE8s \xE0 votre compte.</p>`,
3758
+ text: `Votre adresse e-mail a \xE9t\xE9 modifi\xE9e
3759
+
3760
+ L'adresse e-mail de votre compte vient d'\xEAtre remplac\xE9e par {{newEmail}}.
3761
+
3762
+ Si vous \xEAtes \xE0 l'origine de ce changement, tout est en ordre. Sinon, contactez imm\xE9diatement le support \u2014 quelqu'un pourrait avoir acc\xE8s \xE0 votre compte.`
3763
+ },
3764
+ [MESSAGE_KEYS.phoneChanged]: {
3765
+ subject: "Votre num\xE9ro de t\xE9l\xE9phone a \xE9t\xE9 modifi\xE9",
3766
+ html: `<h1>Votre num\xE9ro de t\xE9l\xE9phone a \xE9t\xE9 modifi\xE9</h1>
3767
+ <p>Le num\xE9ro de t\xE9l\xE9phone de votre compte vient d&rsquo;\xEAtre mis \xE0 jour.</p>
3768
+ <p class="muted">Si vous \xEAtes \xE0 l&rsquo;origine de ce changement, tout est en ordre. Sinon, contactez imm\xE9diatement le support &mdash; quelqu&rsquo;un pourrait avoir acc\xE8s \xE0 votre compte.</p>`,
3769
+ text: `Votre num\xE9ro de t\xE9l\xE9phone a \xE9t\xE9 modifi\xE9
3770
+
3771
+ Le num\xE9ro de t\xE9l\xE9phone de votre compte vient d'\xEAtre mis \xE0 jour.
3772
+
3773
+ Si vous \xEAtes \xE0 l'origine de ce changement, tout est en ordre. Sinon, contactez imm\xE9diatement le support \u2014 quelqu'un pourrait avoir acc\xE8s \xE0 votre compte.`
3774
+ },
3775
+ [MESSAGE_KEYS.passwordRevoked]: {
3776
+ subject: "Le mot de passe de votre compte a \xE9t\xE9 supprim\xE9",
3777
+ html: `<h1>Votre mot de passe a \xE9t\xE9 supprim\xE9</h1>
3778
+ <p>Vous venez de vous connecter avec <strong>{{provider}}</strong>. Un mot de passe avait \xE9t\xE9 d\xE9fini sur ce compte, mais cette adresse e-mail n&rsquo;avait jamais \xE9t\xE9 confirm\xE9e &mdash; nous ne pouvions donc pas savoir qui l&rsquo;avait d\xE9fini, et il a \xE9t\xE9 supprim\xE9.</p>
3779
+ <p>La connexion avec {{provider}} fonctionne comme avant. Si vous souhaitez aussi un mot de passe, d\xE9finissez-en un dans les param\xE8tres de votre compte.</p>
3780
+ <p class="muted">Si vous n&rsquo;avez jamais d\xE9fini de mot de passe ici, quelqu&rsquo;un a peut-\xEAtre tent\xE9 de s&rsquo;inscrire avec votre adresse. Vous n&rsquo;avez rien d&rsquo;autre \xE0 faire &mdash; le compte vous appartient.</p>`,
3781
+ text: `Votre mot de passe a \xE9t\xE9 supprim\xE9
3782
+
3783
+ Vous venez de vous connecter avec {{provider}}. Un mot de passe avait \xE9t\xE9 d\xE9fini
3784
+ sur ce compte, mais cette adresse e-mail n'avait jamais \xE9t\xE9 confirm\xE9e \u2014 nous ne
3785
+ pouvions donc pas savoir qui l'avait d\xE9fini, et il a \xE9t\xE9 supprim\xE9.
3786
+
3787
+ La connexion avec {{provider}} fonctionne comme avant. Si vous souhaitez aussi un
3788
+ mot de passe, d\xE9finissez-en un dans les param\xE8tres de votre compte.
3789
+
3790
+ Si vous n'avez jamais d\xE9fini de mot de passe ici, quelqu'un a peut-\xEAtre tent\xE9 de
3791
+ s'inscrire avec votre adresse. Vous n'avez rien d'autre \xE0 faire \u2014 le compte vous
3792
+ appartient.`
3793
+ },
3794
+ [MESSAGE_KEYS.oauthRegistration]: {
3795
+ subject: "Bienvenue sur {{appName}}",
3796
+ html: `<h1>Bienvenue sur {{appName}}</h1>
3797
+ <p>Votre compte a \xE9t\xE9 cr\xE9\xE9 avec <strong>{{provider}}</strong>. Connectez-vous \xE0 tout moment avec ce m\xEAme compte {{provider}} &mdash; aucun mot de passe \xE0 retenir.</p>
3798
+ <p class="muted">Si vous n&rsquo;avez pas cr\xE9\xE9 ce compte, contactez le support.</p>`,
3799
+ text: `Bienvenue
3800
+
3801
+ Votre compte a \xE9t\xE9 cr\xE9\xE9 avec {{provider}}. Connectez-vous \xE0 tout moment avec ce
3802
+ m\xEAme compte {{provider}} \u2014 aucun mot de passe \xE0 retenir.
3803
+
3804
+ Si vous n'avez pas cr\xE9\xE9 ce compte, contactez le support.`
3805
+ },
3806
+ [MESSAGE_KEYS.oauthLinked]: {
3807
+ subject: "Une nouvelle m\xE9thode de connexion a \xE9t\xE9 ajout\xE9e \xE0 votre compte",
3808
+ html: `<h1>Connexion avec {{provider}} ajout\xE9e</h1>
3809
+ <p>Vous pouvez d\xE9sormais aussi vous connecter \xE0 votre compte avec <strong>{{provider}}</strong>.</p>
3810
+ <p class="muted">Si c&rsquo;est bien vous, vous n&rsquo;avez rien d&rsquo;autre \xE0 faire. Sinon, contactez imm\xE9diatement le support et changez votre mot de passe &mdash; quelqu&rsquo;un d&rsquo;autre pourrait se connecter \xE0 votre place.</p>`,
3811
+ text: `Connexion avec {{provider}} ajout\xE9e
3812
+
3813
+ Vous pouvez d\xE9sormais aussi vous connecter \xE0 votre compte avec {{provider}}.
3814
+
3815
+ Si c'est bien vous, vous n'avez rien d'autre \xE0 faire. Sinon, contactez
3816
+ imm\xE9diatement le support et changez votre mot de passe \u2014 quelqu'un d'autre
3817
+ pourrait se connecter \xE0 votre place.`
3818
+ },
3819
+ [MESSAGE_KEYS.oauthUnlinked]: {
3820
+ subject: "Une m\xE9thode de connexion a \xE9t\xE9 retir\xE9e de votre compte",
3821
+ html: `<h1>Connexion avec {{provider}} retir\xE9e</h1>
3822
+ <p><strong>{{provider}}</strong> ne peut plus \xEAtre utilis\xE9 pour vous connecter. Votre adresse e-mail et votre mot de passe fonctionnent toujours.</p>
3823
+ <p class="muted">Si c&rsquo;est bien vous, tout est en ordre. Sinon, contactez imm\xE9diatement le support.</p>`,
3824
+ text: `Connexion avec {{provider}} retir\xE9e
3825
+
3826
+ {{provider}} ne peut plus \xEAtre utilis\xE9 pour vous connecter. Votre adresse e-mail et votre mot de passe fonctionnent toujours.
3827
+
3828
+ Si c'est bien vous, tout est en ordre. Sinon, contactez imm\xE9diatement le support.`
3829
+ }
3830
+ };
3831
+
3832
+ // src/templates.ts
3833
+ var EN_TEMPLATES = {
3491
3834
  [MESSAGE_KEYS.emailRegistration]: {
3492
3835
  subject: "Confirm your account",
3493
3836
  html: `<h1>Welcome aboard</h1>
@@ -3643,6 +3986,7 @@ change your password \u2014 someone else may be able to sign in as you.`
3643
3986
  If you did this, you're all set. If not, contact support right away.`
3644
3987
  }
3645
3988
  };
3989
+ var DEFAULT_TEMPLATES = withTranslations(EN_TEMPLATES, { fr: FR_TEMPLATES, es: ES_TEMPLATES });
3646
3990
  var SAMPLE_PAYLOADS = {
3647
3991
  [MESSAGE_KEYS.passwordRevoked]: { provider: "Google" },
3648
3992
  [MESSAGE_KEYS.oauthRegistration]: { provider: "Google", appName: "Fonderie" },
@@ -3699,7 +4043,7 @@ async function importUser(store, user) {
3699
4043
  }
3700
4044
 
3701
4045
  // src/services/retention.ts
3702
- async function purgeSoftDeletedUsers(store, { olderThanDays }) {
4046
+ async function purgeSoftDeletedUsers(store, { olderThanDays, bus }) {
3703
4047
  if (!Number.isFinite(olderThanDays) || olderThanDays < 0) {
3704
4048
  throw new Error("[auth] purgeSoftDeletedUsers: olderThanDays must be a non-negative number");
3705
4049
  }
@@ -3710,6 +4054,9 @@ async function purgeSoftDeletedUsers(store, { olderThanDays }) {
3710
4054
  RETURNING id`,
3711
4055
  [olderThanDays]
3712
4056
  );
4057
+ if (bus) {
4058
+ for (const { id } of rows) await bus.emit("fonderie.user.purged", { userId: id });
4059
+ }
3713
4060
  return rows.length;
3714
4061
  }
3715
4062
  function startUserRetention(store, options) {
@@ -3718,7 +4065,10 @@ function startUserRetention(store, options) {
3718
4065
  const run = async () => {
3719
4066
  if (stopped) return;
3720
4067
  try {
3721
- const deleted = await purgeSoftDeletedUsers(store, { olderThanDays: options.olderThanDays });
4068
+ const deleted = await purgeSoftDeletedUsers(store, {
4069
+ olderThanDays: options.olderThanDays,
4070
+ ...options.bus ? { bus: options.bus } : {}
4071
+ });
3722
4072
  options.onPurge?.(deleted);
3723
4073
  } catch (err) {
3724
4074
  console.error("[auth] scheduled user retention purge failed:", err);