@fonderie/auth 7.21.2 → 7.22.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/brain/outcomes.md CHANGED
@@ -142,7 +142,7 @@ Raw SQL ships in `node_modules/@fonderie/auth/dist/migrations/sql/` — read it
142
142
 
143
143
  | Method | Path | Middleware chain (auth / validation / handler) |
144
144
  |---|---|---|
145
- | GET | `/_admin/users` | `async (ctx) => { const params = new URL(ctx.request.url).searchParams; const email = params.get('email')?.trim().toLowerCase(); if (email) { const user = await users.findByEmail(email); return user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND(); } const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const cursorParam = params.get('cursor'); const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null; if (cursorParam && !cursor) return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor'); const deleted = params.get('deleted') === '1' || params.get('deleted') === 'true'; const page = await users.list({ limit, ...(cursor ? { cursor } : {}), ...(deleted ? { deleted } : {}), }); return setApiResponse(HTTP.OK, 'USERS', 'Users', toAdminUserPageDTO(page)); }` |
145
+ | GET | `/_admin/users` | `async (ctx) => { const params = new URL(ctx.request.url).searchParams; // Accounts are stored under normalizeEmail: a search for the address a // user typed ('Jane+work@x.com') finds the account ('jane@x.com'). const raw = params.get('email')?.trim(); const email = raw ? normalizeEmailSafe(raw) ?? raw.toLowerCase() : undefined; if (email) { const user = await users.findByEmail(email); return user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND(); } const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const cursorParam = params.get('cursor'); const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null; if (cursorParam && !cursor) return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor'); const deleted = params.get('deleted') === '1' || params.get('deleted') === 'true'; const page = await users.list({ limit, ...(cursor ? { cursor } : {}), ...(deleted ? { deleted } : {}), }); return setApiResponse(HTTP.OK, 'USERS', 'Users', toAdminUserPageDTO(page)); }` |
146
146
  | GET | `/_admin/users/:id` | `async (ctx) => { // Read-only view: a soft-deleted account resolves (deletedAt set), so // an operator arriving from billing sees what happened to it. const user = await users.findByIdIncludingDeleted(idOf(ctx)); return user ? setApiResponse(HTTP.OK, 'USER', 'User', toAdminUserDTO(user)) : NOT_FOUND(); }` |
147
147
  | GET | `/_admin/users/:id/login-history` | `async (ctx) => { if (!(await users.findById(idOf(ctx)))) return NOT_FOUND(); const params = new URL(ctx.request.url).searchParams; const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const cursorParam = params.get('cursor'); const cursor = cursorParam ? decodeLoginCursor(cursorParam) : null; if (cursorParam && !cursor) return setApiResponse(HTTP.UNPROCESSABLE, 'INVALID_PARAMETER', 'Invalid cursor'); const page = await events.listByUser({ userId: idOf(ctx), limit, ...(cursor ? { cursor } : {}), }); return setApiResponse( HTTP.OK, 'LOGIN_HISTORY', 'Login history', toLoginHistoryPageDTO(page), ); }` |
148
148
  | DELETE | `/_admin/users/:id/sessions` | `async (ctx) => { if (!(await users.findById(idOf(ctx)))) return NOT_FOUND(); await sessions.deleteByUser(idOf(ctx)); await background(bus?.emit(EVENT_KEYS.sessionRevoked, { userId: idOf(ctx), sids: null, reason: 'admin' } satisfies ISessionRevokedEvent)); return setApiResponse(HTTP.OK, 'SESSIONS_REVOKED', 'All sessions revoked'); }` |
package/dist/index.cjs CHANGED
@@ -125,6 +125,41 @@ var import_middlewares = require("@fonderie/core/middlewares");
125
125
 
126
126
  // src/services/rate-limit.ts
127
127
  var import_rate_limit = require("@fonderie/rate-limit");
128
+
129
+ // src/services/email.ts
130
+ function normalizeEmail(email2) {
131
+ if (typeof email2 !== "string" || email2.length === 0) {
132
+ throw new Error("Invalid email");
133
+ }
134
+ const lower = email2.trim().toLowerCase();
135
+ if (lower.length === 0) {
136
+ throw new Error("Email cannot be empty");
137
+ }
138
+ const atIndex = lower.indexOf("@");
139
+ if (atIndex === -1 || lower.lastIndexOf("@") !== atIndex) {
140
+ throw new Error("Invalid email format");
141
+ }
142
+ const local = lower.substring(0, atIndex);
143
+ const domain = lower.substring(atIndex + 1);
144
+ if (!local || !domain) {
145
+ throw new Error("Invalid email format");
146
+ }
147
+ const plusIndex = local.indexOf("+");
148
+ const normalizedLocal = plusIndex === -1 ? local : local.substring(0, plusIndex);
149
+ if (!normalizedLocal) {
150
+ throw new Error("Invalid email format");
151
+ }
152
+ return `${normalizedLocal}@${domain}`;
153
+ }
154
+ function normalizeEmailSafe(email2) {
155
+ try {
156
+ return normalizeEmail(email2);
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ // src/services/rate-limit.ts
128
163
  var min = (n) => n * 60;
129
164
  var DEFAULTS = {
130
165
  // login: 10/15min per IP AND 5/15min per account (credential stuffing
@@ -177,7 +212,9 @@ function buildAuthAccountLimiter(route, store, config) {
177
212
  return (0, import_rate_limit.rateLimit)({
178
213
  store: r.store,
179
214
  rule: r.account.rule,
180
- key: (0, import_rate_limit.byBodyField)(`auth:${route}`, r.account.field)
215
+ // The account bucket is keyed the way accounts are stored, so a rotating
216
+ // '+tag' on the same address does not buy fresh attempts.
217
+ key: (0, import_rate_limit.byBodyField)(`auth:${route}`, r.account.field, r.account.field === "email" ? normalizeEmailSafe : void 0)
181
218
  });
182
219
  }
183
220
 
@@ -1633,39 +1670,6 @@ function checkCooldown(lastSentAt, cooldownMs) {
1633
1670
  // src/controllers/auth.controller.ts
1634
1671
  init_password();
1635
1672
 
1636
- // src/services/email.ts
1637
- function normalizeEmail(email2) {
1638
- if (typeof email2 !== "string" || email2.length === 0) {
1639
- throw new Error("Invalid email");
1640
- }
1641
- const lower = email2.trim().toLowerCase();
1642
- if (lower.length === 0) {
1643
- throw new Error("Email cannot be empty");
1644
- }
1645
- const atIndex = lower.indexOf("@");
1646
- if (atIndex === -1 || lower.lastIndexOf("@") !== atIndex) {
1647
- throw new Error("Invalid email format");
1648
- }
1649
- const local = lower.substring(0, atIndex);
1650
- const domain = lower.substring(atIndex + 1);
1651
- if (!local || !domain) {
1652
- throw new Error("Invalid email format");
1653
- }
1654
- const plusIndex = local.indexOf("+");
1655
- const normalizedLocal = plusIndex === -1 ? local : local.substring(0, plusIndex);
1656
- if (!normalizedLocal) {
1657
- throw new Error("Invalid email format");
1658
- }
1659
- return `${normalizedLocal}@${domain}`;
1660
- }
1661
- function normalizeEmailSafe(email2) {
1662
- try {
1663
- return normalizeEmail(email2);
1664
- } catch {
1665
- return null;
1666
- }
1667
- }
1668
-
1669
1673
  // src/models/password-reset.model.ts
1670
1674
  var import_node_crypto5 = require("crypto");
1671
1675
  var hashSecret = (value) => (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
@@ -3355,7 +3359,7 @@ function oauthController(store, config, bus) {
3355
3359
  const meta = requestMeta(ctx);
3356
3360
  await loginEvents.recordSafe({
3357
3361
  userId: null,
3358
- emailAttempted: claims.email ?? null,
3362
+ emailAttempted: claims.email ? normalizeEmailSafe(claims.email) ?? claims.email : null,
3359
3363
  method: "oauth-apple",
3360
3364
  outcome: "failed",
3361
3365
  failureReason: "token_replayed",
@@ -3403,7 +3407,7 @@ function oauthController(store, config, bus) {
3403
3407
  if (!await consumedTokens.consumeOnce(tokenHash, new Date(expMs))) {
3404
3408
  await loginEvents.recordSafe({
3405
3409
  userId: null,
3406
- emailAttempted: claims.email ?? null,
3410
+ emailAttempted: claims.email ? normalizeEmailSafe(claims.email) ?? claims.email : null,
3407
3411
  method: "oauth-google",
3408
3412
  outcome: "failed",
3409
3413
  failureReason: "token_replayed",
@@ -3761,7 +3765,8 @@ function adminRouteTable(store, bus) {
3761
3765
  // see anything cannot discover who signed up this morning.
3762
3766
  async (ctx) => {
3763
3767
  const params = new URL(ctx.request.url).searchParams;
3764
- const email2 = params.get("email")?.trim().toLowerCase();
3768
+ const raw = params.get("email")?.trim();
3769
+ const email2 = raw ? normalizeEmailSafe(raw) ?? raw.toLowerCase() : void 0;
3765
3770
  if (email2) {
3766
3771
  const user = await users.findByEmail(email2);
3767
3772
  return user ? (0, import_core12.setApiResponse)(import_core12.HTTP.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
@@ -3865,7 +3870,7 @@ var AuthModule = class {
3865
3870
  name = "@fonderie/auth";
3866
3871
  // Baked in at build time by tsup.base, so the operator's Modules page can
3867
3872
  // say what is actually deployed rather than 'not reported'.
3868
- version = "7.21.2";
3873
+ version = "7.22.1";
3869
3874
  // Users, sessions, login history, suspend — only through @fonderie/admin.
3870
3875
  describeAdmin() {
3871
3876
  return { routes: describeAuthAdminRoutes(this.store, this.bus) };
@@ -4216,6 +4221,314 @@ Si c'est bien vous, tout est en ordre. Sinon, contactez imm\xE9diatement le supp
4216
4221
  }
4217
4222
  };
4218
4223
 
4224
+ // src/templates.zh-Hans.ts
4225
+ var ZH_HANS_TEMPLATES = {
4226
+ [MESSAGE_KEYS.emailRegistration]: {
4227
+ subject: "\u786E\u8BA4\u60A8\u7684\u8D26\u6237",
4228
+ html: `<h1>\u6B22\u8FCE\u52A0\u5165</h1>
4229
+ <p>{{firstName}}\uFF0C\u60A8\u597D\uFF1A</p>
4230
+ <p>\u611F\u8C22\u60A8\u7684\u6CE8\u518C\u3002\u8BF7\u4F7F\u7528\u4EE5\u4E0B\u9A8C\u8BC1\u7801\u786E\u8BA4\u60A8\u7684\u8D26\u6237\uFF1A</p>
4231
+ <p><span class="pin-code">{{pin}}</span></p>
4232
+ <p class="muted">\u6B64\u9A8C\u8BC1\u7801\u5C06\u5728 24 \u5C0F\u65F6\u540E\u5931\u6548\u3002\u5982\u679C\u60A8\u6CA1\u6709\u521B\u5EFA\u8D26\u6237\uFF0C\u8BF7\u5FFD\u7565\u6B64\u90AE\u4EF6\u3002</p>`,
4233
+ text: `\u6B22\u8FCE\u52A0\u5165
4234
+
4235
+ {{firstName}}\uFF0C\u60A8\u597D\uFF1A
4236
+
4237
+ \u611F\u8C22\u60A8\u7684\u6CE8\u518C\u3002\u8BF7\u4F7F\u7528\u4EE5\u4E0B\u9A8C\u8BC1\u7801\u786E\u8BA4\u60A8\u7684\u8D26\u6237\uFF1A{{pin}}
4238
+
4239
+ \u6B64\u9A8C\u8BC1\u7801\u5C06\u5728 24 \u5C0F\u65F6\u540E\u5931\u6548\u3002\u5982\u679C\u60A8\u6CA1\u6709\u521B\u5EFA\u8D26\u6237\uFF0C\u8BF7\u5FFD\u7565\u6B64\u90AE\u4EF6\u3002`
4240
+ },
4241
+ [MESSAGE_KEYS.emailVerification]: {
4242
+ subject: "\u60A8\u7684\u9A8C\u8BC1\u7801",
4243
+ html: `<h1>\u9A8C\u8BC1\u60A8\u7684\u7535\u5B50\u90AE\u7BB1</h1>
4244
+ <p>{{firstName}}\uFF0C\u60A8\u597D\uFF1A</p>
4245
+ <p>\u8BF7\u4F7F\u7528\u4EE5\u4E0B\u9A8C\u8BC1\u7801\u5B8C\u6210\u8D26\u6237\u8BBE\u7F6E\uFF1A</p>
4246
+ <p><span class="pin-code">{{pin}}</span></p>
4247
+ <p class="muted">\u6B64\u9A8C\u8BC1\u7801\u5C06\u5728 24 \u5C0F\u65F6\u540E\u5931\u6548\u3002\u5982\u679C\u60A8\u6CA1\u6709\u521B\u5EFA\u8D26\u6237\uFF0C\u8BF7\u5FFD\u7565\u6B64\u90AE\u4EF6\u3002</p>`,
4248
+ text: `\u9A8C\u8BC1\u60A8\u7684\u7535\u5B50\u90AE\u7BB1
4249
+
4250
+ {{firstName}}\uFF0C\u60A8\u597D\uFF1A
4251
+
4252
+ \u8BF7\u4F7F\u7528\u4EE5\u4E0B\u9A8C\u8BC1\u7801\u5B8C\u6210\u8D26\u6237\u8BBE\u7F6E\uFF1A{{pin}}
4253
+
4254
+ \u6B64\u9A8C\u8BC1\u7801\u5C06\u5728 24 \u5C0F\u65F6\u540E\u5931\u6548\u3002\u5982\u679C\u60A8\u6CA1\u6709\u521B\u5EFA\u8D26\u6237\uFF0C\u8BF7\u5FFD\u7565\u6B64\u90AE\u4EF6\u3002`
4255
+ },
4256
+ [MESSAGE_KEYS.passwordReset]: {
4257
+ subject: "\u91CD\u7F6E\u60A8\u7684\u5BC6\u7801",
4258
+ html: `<h1>\u91CD\u7F6E\u60A8\u7684\u5BC6\u7801</h1>
4259
+ <p>\u6211\u4EEC\u6536\u5230\u4E86\u91CD\u7F6E\u60A8\u5BC6\u7801\u7684\u8BF7\u6C42\u3002\u8BF7\u4F7F\u7528\u4EE5\u4E0B\u9A8C\u8BC1\u7801\u7EE7\u7EED\uFF1A</p>
4260
+ <p><span class="pin-code">{{pin}}</span></p>
4261
+ <p class="muted">\u6B64\u9A8C\u8BC1\u7801\u5F88\u5FEB\u5C31\u4F1A\u5931\u6548\u3002\u5982\u679C\u60A8\u6CA1\u6709\u7533\u8BF7\u91CD\u7F6E\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\uFF0C\u60A8\u7684\u5BC6\u7801\u4FDD\u6301\u4E0D\u53D8\u3002</p>`,
4262
+ text: `\u91CD\u7F6E\u60A8\u7684\u5BC6\u7801
4263
+
4264
+ \u6211\u4EEC\u6536\u5230\u4E86\u91CD\u7F6E\u60A8\u5BC6\u7801\u7684\u8BF7\u6C42\u3002\u8BF7\u4F7F\u7528\u4EE5\u4E0B\u9A8C\u8BC1\u7801\u7EE7\u7EED\uFF1A{{pin}}
4265
+
4266
+ \u6B64\u9A8C\u8BC1\u7801\u5F88\u5FEB\u5C31\u4F1A\u5931\u6548\u3002\u5982\u679C\u60A8\u6CA1\u6709\u7533\u8BF7\u91CD\u7F6E\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\uFF0C\u60A8\u7684\u5BC6\u7801\u4FDD\u6301\u4E0D\u53D8\u3002`
4267
+ },
4268
+ [MESSAGE_KEYS.phoneOtp]: {
4269
+ text: "\u60A8\u7684\u9A8C\u8BC1\u7801\u662F {{otp}}\uFF0C10 \u5206\u949F\u5185\u6709\u6548\u3002"
4270
+ },
4271
+ [MESSAGE_KEYS.mfaEnabled]: {
4272
+ subject: "\u53CC\u91CD\u9A8C\u8BC1\u5DF2\u5F00\u542F",
4273
+ html: `<h1>\u53CC\u91CD\u9A8C\u8BC1\u5DF2\u5F00\u542F</h1>
4274
+ <p>\u60A8\u7684\u8D26\u6237\u521A\u521A\u5F00\u542F\u4E86\u53CC\u91CD\u9A8C\u8BC1\u3002\u4ECA\u540E\u767B\u5F55\u65F6\uFF0C\u60A8\u9700\u8981\u8F93\u5165\u8EAB\u4EFD\u9A8C\u8BC1\u5668\u5E94\u7528\u4E2D\u7684\u9A8C\u8BC1\u7801\u3002</p>
4275
+ <p class="muted">\u5982\u679C\u8FD9\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002</p>`,
4276
+ text: `\u53CC\u91CD\u9A8C\u8BC1\u5DF2\u5F00\u542F
4277
+
4278
+ \u60A8\u7684\u8D26\u6237\u521A\u521A\u5F00\u542F\u4E86\u53CC\u91CD\u9A8C\u8BC1\u3002\u4ECA\u540E\u767B\u5F55\u65F6\uFF0C\u60A8\u9700\u8981\u8F93\u5165\u8EAB\u4EFD\u9A8C\u8BC1\u5668\u5E94\u7528\u4E2D\u7684\u9A8C\u8BC1\u7801\u3002
4279
+
4280
+ \u5982\u679C\u8FD9\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002`
4281
+ },
4282
+ [MESSAGE_KEYS.mfaDisabled]: {
4283
+ subject: "\u53CC\u91CD\u9A8C\u8BC1\u5DF2\u5173\u95ED",
4284
+ html: `<h1>\u53CC\u91CD\u9A8C\u8BC1\u5DF2\u5173\u95ED</h1>
4285
+ <p>\u60A8\u7684\u8D26\u6237\u521A\u521A\u5173\u95ED\u4E86\u53CC\u91CD\u9A8C\u8BC1\u3002\u73B0\u5728\u60A8\u7684\u8D26\u6237\u4EC5\u7531\u5BC6\u7801\u4FDD\u62A4\u3002</p>
4286
+ <p class="muted">\u5982\u679C\u8FD9\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\u5E76\u91CD\u65B0\u5F00\u542F\u53CC\u91CD\u9A8C\u8BC1\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002</p>`,
4287
+ text: `\u53CC\u91CD\u9A8C\u8BC1\u5DF2\u5173\u95ED
4288
+
4289
+ \u60A8\u7684\u8D26\u6237\u521A\u521A\u5173\u95ED\u4E86\u53CC\u91CD\u9A8C\u8BC1\u3002\u73B0\u5728\u60A8\u7684\u8D26\u6237\u4EC5\u7531\u5BC6\u7801\u4FDD\u62A4\u3002
4290
+
4291
+ \u5982\u679C\u8FD9\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\u5E76\u91CD\u65B0\u5F00\u542F\u53CC\u91CD\u9A8C\u8BC1\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002`
4292
+ },
4293
+ [MESSAGE_KEYS.mfaBackupCodesRegenerated]: {
4294
+ subject: "\u60A8\u7684\u5907\u7528\u9A8C\u8BC1\u7801\u5DF2\u91CD\u65B0\u751F\u6210",
4295
+ html: `<h1>\u5DF2\u751F\u6210\u65B0\u7684\u5907\u7528\u9A8C\u8BC1\u7801</h1>
4296
+ <p>\u60A8\u7684\u8D26\u6237\u521A\u521A\u751F\u6210\u4E86\u4E00\u7EC4\u65B0\u7684\u53CC\u91CD\u9A8C\u8BC1\u5907\u7528\u7801\u3002\u4E4B\u524D\u7684\u5907\u7528\u7801\u5DF2\u5168\u90E8\u5931\u6548\u3002</p>
4297
+ <p class="muted">\u5982\u679C\u8FD9\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002</p>`,
4298
+ text: `\u5DF2\u751F\u6210\u65B0\u7684\u5907\u7528\u9A8C\u8BC1\u7801
4299
+
4300
+ \u60A8\u7684\u8D26\u6237\u521A\u521A\u751F\u6210\u4E86\u4E00\u7EC4\u65B0\u7684\u53CC\u91CD\u9A8C\u8BC1\u5907\u7528\u7801\u3002\u4E4B\u524D\u7684\u5907\u7528\u7801\u5DF2\u5168\u90E8\u5931\u6548\u3002
4301
+
4302
+ \u5982\u679C\u8FD9\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002`
4303
+ },
4304
+ [MESSAGE_KEYS.emailChanged]: {
4305
+ subject: "\u60A8\u7684\u7535\u5B50\u90AE\u7BB1\u5730\u5740\u5DF2\u66F4\u6539",
4306
+ html: `<h1>\u60A8\u7684\u7535\u5B50\u90AE\u7BB1\u5DF2\u66F4\u6539</h1>
4307
+ <p>\u60A8\u8D26\u6237\u7684\u7535\u5B50\u90AE\u7BB1\u5730\u5740\u521A\u521A\u66F4\u6539\u4E3A <strong>{{newEmail}}</strong>\u3002</p>
4308
+ <p class="muted">\u5982\u679C\u662F\u60A8\u672C\u4EBA\u66F4\u6539\u7684\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002</p>`,
4309
+ text: `\u60A8\u7684\u7535\u5B50\u90AE\u7BB1\u5DF2\u66F4\u6539
4310
+
4311
+ \u60A8\u8D26\u6237\u7684\u7535\u5B50\u90AE\u7BB1\u5730\u5740\u521A\u521A\u66F4\u6539\u4E3A {{newEmail}}\u3002
4312
+
4313
+ \u5982\u679C\u662F\u60A8\u672C\u4EBA\u66F4\u6539\u7684\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002`
4314
+ },
4315
+ [MESSAGE_KEYS.phoneChanged]: {
4316
+ subject: "\u60A8\u7684\u624B\u673A\u53F7\u7801\u5DF2\u66F4\u6539",
4317
+ html: `<h1>\u60A8\u7684\u624B\u673A\u53F7\u7801\u5DF2\u66F4\u6539</h1>
4318
+ <p>\u60A8\u8D26\u6237\u7684\u624B\u673A\u53F7\u7801\u521A\u521A\u5B8C\u6210\u66F4\u65B0\u3002</p>
4319
+ <p class="muted">\u5982\u679C\u662F\u60A8\u672C\u4EBA\u66F4\u6539\u7684\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002</p>`,
4320
+ text: `\u60A8\u7684\u624B\u673A\u53F7\u7801\u5DF2\u66F4\u6539
4321
+
4322
+ \u60A8\u8D26\u6237\u7684\u624B\u673A\u53F7\u7801\u521A\u521A\u5B8C\u6210\u66F4\u65B0\u3002
4323
+
4324
+ \u5982\u679C\u662F\u60A8\u672C\u4EBA\u66F4\u6539\u7684\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u7ECF\u80FD\u591F\u8BBF\u95EE\u60A8\u7684\u8D26\u6237\u3002`
4325
+ },
4326
+ [MESSAGE_KEYS.passwordRevoked]: {
4327
+ subject: "\u60A8\u8D26\u6237\u7684\u5BC6\u7801\u5DF2\u88AB\u79FB\u9664",
4328
+ html: `<h1>\u60A8\u7684\u5BC6\u7801\u5DF2\u88AB\u79FB\u9664</h1>
4329
+ <p>\u60A8\u521A\u521A\u4F7F\u7528 <strong>{{provider}}</strong> \u767B\u5F55\u3002\u6B64\u8D26\u6237\u4E4B\u524D\u8BBE\u7F6E\u8FC7\u5BC6\u7801\uFF0C\u4F46\u8BE5\u7535\u5B50\u90AE\u7BB1\u5730\u5740\u4ECE\u672A\u7ECF\u8FC7\u786E\u8BA4\uFF0C\u56E0\u6B64\u6211\u4EEC\u65E0\u6CD5\u786E\u5B9A\u5BC6\u7801\u662F\u8C01\u8BBE\u7F6E\u7684\uFF0C\u5DF2\u5C06\u5176\u79FB\u9664\u3002</p>
4330
+ <p>\u4F7F\u7528 {{provider}} \u767B\u5F55\u4E0D\u53D7\u5F71\u54CD\u3002\u5982\u679C\u60A8\u8FD8\u60F3\u8BBE\u7F6E\u5BC6\u7801\uFF0C\u53EF\u4EE5\u5728\u8D26\u6237\u8BBE\u7F6E\u4E2D\u8FDB\u884C\u8BBE\u7F6E\u3002</p>
4331
+ <p class="muted">\u5982\u679C\u60A8\u4ECE\u672A\u5728\u6B64\u8BBE\u7F6E\u8FC7\u5BC6\u7801\uFF0C\u53EF\u80FD\u6709\u5176\u4ED6\u4EBA\u66FE\u8BD5\u56FE\u7528\u60A8\u7684\u5730\u5740\u6CE8\u518C\u3002\u60A8\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\uFF0C\u6B64\u8D26\u6237\u5C5E\u4E8E\u60A8\u3002</p>`,
4332
+ text: `\u60A8\u7684\u5BC6\u7801\u5DF2\u88AB\u79FB\u9664
4333
+
4334
+ \u60A8\u521A\u521A\u4F7F\u7528 {{provider}} \u767B\u5F55\u3002\u6B64\u8D26\u6237\u4E4B\u524D\u8BBE\u7F6E\u8FC7\u5BC6\u7801\uFF0C\u4F46\u8BE5\u7535\u5B50\u90AE\u7BB1\u5730\u5740\u4ECE\u672A\u7ECF\u8FC7\u786E\u8BA4\uFF0C
4335
+ \u56E0\u6B64\u6211\u4EEC\u65E0\u6CD5\u786E\u5B9A\u5BC6\u7801\u662F\u8C01\u8BBE\u7F6E\u7684\uFF0C\u5DF2\u5C06\u5176\u79FB\u9664\u3002
4336
+
4337
+ \u4F7F\u7528 {{provider}} \u767B\u5F55\u4E0D\u53D7\u5F71\u54CD\u3002\u5982\u679C\u60A8\u8FD8\u60F3\u8BBE\u7F6E\u5BC6\u7801\uFF0C\u53EF\u4EE5\u5728\u8D26\u6237\u8BBE\u7F6E\u4E2D\u8FDB\u884C\u8BBE\u7F6E\u3002
4338
+
4339
+ \u5982\u679C\u60A8\u4ECE\u672A\u5728\u6B64\u8BBE\u7F6E\u8FC7\u5BC6\u7801\uFF0C\u53EF\u80FD\u6709\u5176\u4ED6\u4EBA\u66FE\u8BD5\u56FE\u7528\u60A8\u7684\u5730\u5740\u6CE8\u518C\u3002\u60A8\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\uFF0C\u6B64\u8D26\u6237\u5C5E\u4E8E\u60A8\u3002`
4340
+ },
4341
+ [MESSAGE_KEYS.oauthRegistration]: {
4342
+ subject: "\u6B22\u8FCE\u4F7F\u7528 {{appName}}",
4343
+ html: `<h1>\u6B22\u8FCE\u4F7F\u7528 {{appName}}</h1>
4344
+ <p>\u60A8\u7684\u8D26\u6237\u5DF2\u901A\u8FC7 <strong>{{provider}}</strong> \u521B\u5EFA\u3002\u4ECA\u540E\u968F\u65F6\u53EF\u4EE5\u4F7F\u7528\u540C\u4E00\u4E2A {{provider}} \u8D26\u6237\u767B\u5F55\uFF0C\u65E0\u9700\u8BB0\u4F4F\u5BC6\u7801\u3002</p>
4345
+ <p class="muted">\u5982\u679C\u60A8\u6CA1\u6709\u521B\u5EFA\u6B64\u8D26\u6237\uFF0C\u8BF7\u8054\u7CFB\u5BA2\u670D\u3002</p>`,
4346
+ text: `\u6B22\u8FCE
4347
+
4348
+ \u60A8\u7684\u8D26\u6237\u5DF2\u901A\u8FC7 {{provider}} \u521B\u5EFA\u3002\u4ECA\u540E\u968F\u65F6\u53EF\u4EE5\u4F7F\u7528\u540C\u4E00\u4E2A
4349
+ {{provider}} \u8D26\u6237\u767B\u5F55\uFF0C\u65E0\u9700\u8BB0\u4F4F\u5BC6\u7801\u3002
4350
+
4351
+ \u5982\u679C\u60A8\u6CA1\u6709\u521B\u5EFA\u6B64\u8D26\u6237\uFF0C\u8BF7\u8054\u7CFB\u5BA2\u670D\u3002`
4352
+ },
4353
+ [MESSAGE_KEYS.oauthLinked]: {
4354
+ subject: "\u60A8\u7684\u8D26\u6237\u65B0\u589E\u4E86\u4E00\u79CD\u767B\u5F55\u65B9\u5F0F",
4355
+ html: `<h1>\u5DF2\u6DFB\u52A0 {{provider}} \u767B\u5F55</h1>
4356
+ <p>\u73B0\u5728\u4E5F\u53EF\u4EE5\u4F7F\u7528 <strong>{{provider}}</strong> \u767B\u5F55\u60A8\u7684\u8D26\u6237\u3002</p>
4357
+ <p class="muted">\u5982\u679C\u662F\u60A8\u672C\u4EBA\u6DFB\u52A0\u7684\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\u5E76\u66F4\u6539\u5BC6\u7801\uFF0C\u53EF\u80FD\u6709\u5176\u4ED6\u4EBA\u80FD\u591F\u4EE5\u60A8\u7684\u8EAB\u4EFD\u767B\u5F55\u3002</p>`,
4358
+ text: `\u5DF2\u6DFB\u52A0 {{provider}} \u767B\u5F55
4359
+
4360
+ \u73B0\u5728\u4E5F\u53EF\u4EE5\u4F7F\u7528 {{provider}} \u767B\u5F55\u60A8\u7684\u8D26\u6237\u3002
4361
+
4362
+ \u5982\u679C\u662F\u60A8\u672C\u4EBA\u6DFB\u52A0\u7684\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\u5E76\u66F4\u6539\u5BC6\u7801\uFF0C
4363
+ \u53EF\u80FD\u6709\u5176\u4ED6\u4EBA\u80FD\u591F\u4EE5\u60A8\u7684\u8EAB\u4EFD\u767B\u5F55\u3002`
4364
+ },
4365
+ [MESSAGE_KEYS.oauthUnlinked]: {
4366
+ subject: "\u60A8\u7684\u8D26\u6237\u79FB\u9664\u4E86\u4E00\u79CD\u767B\u5F55\u65B9\u5F0F",
4367
+ html: `<h1>\u5DF2\u79FB\u9664 {{provider}} \u767B\u5F55</h1>
4368
+ <p>\u5DF2\u65E0\u6CD5\u518D\u4F7F\u7528 <strong>{{provider}}</strong> \u767B\u5F55\u3002\u60A8\u7684\u7535\u5B50\u90AE\u7BB1\u548C\u5BC6\u7801\u4ECD\u7136\u53EF\u7528\u3002</p>
4369
+ <p class="muted">\u5982\u679C\u662F\u60A8\u672C\u4EBA\u79FB\u9664\u7684\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\u3002</p>`,
4370
+ text: `\u5DF2\u79FB\u9664 {{provider}} \u767B\u5F55
4371
+
4372
+ \u5DF2\u65E0\u6CD5\u518D\u4F7F\u7528 {{provider}} \u767B\u5F55\u3002\u60A8\u7684\u7535\u5B50\u90AE\u7BB1\u548C\u5BC6\u7801\u4ECD\u7136\u53EF\u7528\u3002
4373
+
4374
+ \u5982\u679C\u662F\u60A8\u672C\u4EBA\u79FB\u9664\u7684\uFF0C\u65E0\u9700\u4EFB\u4F55\u64CD\u4F5C\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8BF7\u7ACB\u5373\u8054\u7CFB\u5BA2\u670D\u3002`
4375
+ }
4376
+ };
4377
+
4378
+ // src/templates.zh-Hant.ts
4379
+ var ZH_HANT_TEMPLATES = {
4380
+ [MESSAGE_KEYS.emailRegistration]: {
4381
+ subject: "\u78BA\u8A8D\u60A8\u7684\u5E33\u6236",
4382
+ html: `<h1>\u6B61\u8FCE\u52A0\u5165</h1>
4383
+ <p>{{firstName}}\uFF0C\u60A8\u597D\uFF1A</p>
4384
+ <p>\u611F\u8B1D\u60A8\u7684\u8A3B\u518A\u3002\u8ACB\u4F7F\u7528\u4EE5\u4E0B\u9A57\u8B49\u78BC\u78BA\u8A8D\u60A8\u7684\u5E33\u6236\uFF1A</p>
4385
+ <p><span class="pin-code">{{pin}}</span></p>
4386
+ <p class="muted">\u6B64\u9A57\u8B49\u78BC\u5C07\u65BC 24 \u5C0F\u6642\u5F8C\u5931\u6548\u3002\u5982\u679C\u60A8\u6C92\u6709\u5EFA\u7ACB\u5E33\u6236\uFF0C\u8ACB\u7565\u904E\u9019\u5C01\u90F5\u4EF6\u3002</p>`,
4387
+ text: `\u6B61\u8FCE\u52A0\u5165
4388
+
4389
+ {{firstName}}\uFF0C\u60A8\u597D\uFF1A
4390
+
4391
+ \u611F\u8B1D\u60A8\u7684\u8A3B\u518A\u3002\u8ACB\u4F7F\u7528\u4EE5\u4E0B\u9A57\u8B49\u78BC\u78BA\u8A8D\u60A8\u7684\u5E33\u6236\uFF1A{{pin}}
4392
+
4393
+ \u6B64\u9A57\u8B49\u78BC\u5C07\u65BC 24 \u5C0F\u6642\u5F8C\u5931\u6548\u3002\u5982\u679C\u60A8\u6C92\u6709\u5EFA\u7ACB\u5E33\u6236\uFF0C\u8ACB\u7565\u904E\u9019\u5C01\u90F5\u4EF6\u3002`
4394
+ },
4395
+ [MESSAGE_KEYS.emailVerification]: {
4396
+ subject: "\u60A8\u7684\u9A57\u8B49\u78BC",
4397
+ html: `<h1>\u9A57\u8B49\u60A8\u7684\u96FB\u5B50\u90F5\u4EF6</h1>
4398
+ <p>{{firstName}}\uFF0C\u60A8\u597D\uFF1A</p>
4399
+ <p>\u8ACB\u4F7F\u7528\u4EE5\u4E0B\u9A57\u8B49\u78BC\u5B8C\u6210\u5E33\u6236\u8A2D\u5B9A\uFF1A</p>
4400
+ <p><span class="pin-code">{{pin}}</span></p>
4401
+ <p class="muted">\u6B64\u9A57\u8B49\u78BC\u5C07\u65BC 24 \u5C0F\u6642\u5F8C\u5931\u6548\u3002\u5982\u679C\u60A8\u6C92\u6709\u5EFA\u7ACB\u5E33\u6236\uFF0C\u8ACB\u7565\u904E\u9019\u5C01\u90F5\u4EF6\u3002</p>`,
4402
+ text: `\u9A57\u8B49\u60A8\u7684\u96FB\u5B50\u90F5\u4EF6
4403
+
4404
+ {{firstName}}\uFF0C\u60A8\u597D\uFF1A
4405
+
4406
+ \u8ACB\u4F7F\u7528\u4EE5\u4E0B\u9A57\u8B49\u78BC\u5B8C\u6210\u5E33\u6236\u8A2D\u5B9A\uFF1A{{pin}}
4407
+
4408
+ \u6B64\u9A57\u8B49\u78BC\u5C07\u65BC 24 \u5C0F\u6642\u5F8C\u5931\u6548\u3002\u5982\u679C\u60A8\u6C92\u6709\u5EFA\u7ACB\u5E33\u6236\uFF0C\u8ACB\u7565\u904E\u9019\u5C01\u90F5\u4EF6\u3002`
4409
+ },
4410
+ [MESSAGE_KEYS.passwordReset]: {
4411
+ subject: "\u91CD\u8A2D\u60A8\u7684\u5BC6\u78BC",
4412
+ html: `<h1>\u91CD\u8A2D\u60A8\u7684\u5BC6\u78BC</h1>
4413
+ <p>\u6211\u5011\u6536\u5230\u91CD\u8A2D\u60A8\u5BC6\u78BC\u7684\u8981\u6C42\u3002\u8ACB\u4F7F\u7528\u4EE5\u4E0B\u9A57\u8B49\u78BC\u7E7C\u7E8C\uFF1A</p>
4414
+ <p><span class="pin-code">{{pin}}</span></p>
4415
+ <p class="muted">\u6B64\u9A57\u8B49\u78BC\u5373\u5C07\u5931\u6548\u3002\u5982\u679C\u60A8\u6C92\u6709\u63D0\u51FA\u91CD\u8A2D\u8981\u6C42\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\uFF0C\u60A8\u7684\u5BC6\u78BC\u7DAD\u6301\u4E0D\u8B8A\u3002</p>`,
4416
+ text: `\u91CD\u8A2D\u60A8\u7684\u5BC6\u78BC
4417
+
4418
+ \u6211\u5011\u6536\u5230\u91CD\u8A2D\u60A8\u5BC6\u78BC\u7684\u8981\u6C42\u3002\u8ACB\u4F7F\u7528\u4EE5\u4E0B\u9A57\u8B49\u78BC\u7E7C\u7E8C\uFF1A{{pin}}
4419
+
4420
+ \u6B64\u9A57\u8B49\u78BC\u5373\u5C07\u5931\u6548\u3002\u5982\u679C\u60A8\u6C92\u6709\u63D0\u51FA\u91CD\u8A2D\u8981\u6C42\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\uFF0C\u60A8\u7684\u5BC6\u78BC\u7DAD\u6301\u4E0D\u8B8A\u3002`
4421
+ },
4422
+ [MESSAGE_KEYS.phoneOtp]: {
4423
+ text: "\u60A8\u7684\u9A57\u8B49\u78BC\u662F {{otp}}\uFF0C10 \u5206\u9418\u5167\u6709\u6548\u3002"
4424
+ },
4425
+ [MESSAGE_KEYS.mfaEnabled]: {
4426
+ subject: "\u96D9\u91CD\u9A57\u8B49\u5DF2\u555F\u7528",
4427
+ html: `<h1>\u96D9\u91CD\u9A57\u8B49\u5DF2\u555F\u7528</h1>
4428
+ <p>\u60A8\u7684\u5E33\u6236\u525B\u525B\u555F\u7528\u4E86\u96D9\u91CD\u9A57\u8B49\u3002\u5F80\u5F8C\u767B\u5165\u6642\uFF0C\u60A8\u9700\u8981\u8F38\u5165\u9A57\u8B49\u5668\u61C9\u7528\u7A0B\u5F0F\u4E2D\u7684\u9A57\u8B49\u78BC\u3002</p>
4429
+ <p class="muted">\u5982\u679C\u9019\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002</p>`,
4430
+ text: `\u96D9\u91CD\u9A57\u8B49\u5DF2\u555F\u7528
4431
+
4432
+ \u60A8\u7684\u5E33\u6236\u525B\u525B\u555F\u7528\u4E86\u96D9\u91CD\u9A57\u8B49\u3002\u5F80\u5F8C\u767B\u5165\u6642\uFF0C\u60A8\u9700\u8981\u8F38\u5165\u9A57\u8B49\u5668\u61C9\u7528\u7A0B\u5F0F\u4E2D\u7684\u9A57\u8B49\u78BC\u3002
4433
+
4434
+ \u5982\u679C\u9019\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002`
4435
+ },
4436
+ [MESSAGE_KEYS.mfaDisabled]: {
4437
+ subject: "\u96D9\u91CD\u9A57\u8B49\u5DF2\u505C\u7528",
4438
+ html: `<h1>\u96D9\u91CD\u9A57\u8B49\u5DF2\u505C\u7528</h1>
4439
+ <p>\u60A8\u7684\u5E33\u6236\u525B\u525B\u505C\u7528\u4E86\u96D9\u91CD\u9A57\u8B49\u3002\u73FE\u5728\u60A8\u7684\u5E33\u6236\u50C5\u53D7\u5BC6\u78BC\u4FDD\u8B77\u3002</p>
4440
+ <p class="muted">\u5982\u679C\u9019\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\u4E26\u91CD\u65B0\u555F\u7528\u96D9\u91CD\u9A57\u8B49\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002</p>`,
4441
+ text: `\u96D9\u91CD\u9A57\u8B49\u5DF2\u505C\u7528
4442
+
4443
+ \u60A8\u7684\u5E33\u6236\u525B\u525B\u505C\u7528\u4E86\u96D9\u91CD\u9A57\u8B49\u3002\u73FE\u5728\u60A8\u7684\u5E33\u6236\u50C5\u53D7\u5BC6\u78BC\u4FDD\u8B77\u3002
4444
+
4445
+ \u5982\u679C\u9019\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\u4E26\u91CD\u65B0\u555F\u7528\u96D9\u91CD\u9A57\u8B49\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002`
4446
+ },
4447
+ [MESSAGE_KEYS.mfaBackupCodesRegenerated]: {
4448
+ subject: "\u60A8\u7684\u5099\u7528\u9A57\u8B49\u78BC\u5DF2\u91CD\u65B0\u7522\u751F",
4449
+ html: `<h1>\u5DF2\u7522\u751F\u65B0\u7684\u5099\u7528\u9A57\u8B49\u78BC</h1>
4450
+ <p>\u60A8\u7684\u5E33\u6236\u525B\u525B\u7522\u751F\u4E86\u4E00\u7D44\u65B0\u7684\u96D9\u91CD\u9A57\u8B49\u5099\u7528\u78BC\u3002\u5148\u524D\u7684\u5099\u7528\u78BC\u5DF2\u5168\u90E8\u5931\u6548\u3002</p>
4451
+ <p class="muted">\u5982\u679C\u9019\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002</p>`,
4452
+ text: `\u5DF2\u7522\u751F\u65B0\u7684\u5099\u7528\u9A57\u8B49\u78BC
4453
+
4454
+ \u60A8\u7684\u5E33\u6236\u525B\u525B\u7522\u751F\u4E86\u4E00\u7D44\u65B0\u7684\u96D9\u91CD\u9A57\u8B49\u5099\u7528\u78BC\u3002\u5148\u524D\u7684\u5099\u7528\u78BC\u5DF2\u5168\u90E8\u5931\u6548\u3002
4455
+
4456
+ \u5982\u679C\u9019\u4E0D\u662F\u60A8\u672C\u4EBA\u7684\u64CD\u4F5C\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002`
4457
+ },
4458
+ [MESSAGE_KEYS.emailChanged]: {
4459
+ subject: "\u60A8\u7684\u96FB\u5B50\u90F5\u4EF6\u5730\u5740\u5DF2\u8B8A\u66F4",
4460
+ html: `<h1>\u60A8\u7684\u96FB\u5B50\u90F5\u4EF6\u5DF2\u8B8A\u66F4</h1>
4461
+ <p>\u60A8\u5E33\u6236\u7684\u96FB\u5B50\u90F5\u4EF6\u5730\u5740\u525B\u525B\u8B8A\u66F4\u70BA <strong>{{newEmail}}</strong>\u3002</p>
4462
+ <p class="muted">\u5982\u679C\u662F\u60A8\u672C\u4EBA\u8B8A\u66F4\u7684\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002</p>`,
4463
+ text: `\u60A8\u7684\u96FB\u5B50\u90F5\u4EF6\u5DF2\u8B8A\u66F4
4464
+
4465
+ \u60A8\u5E33\u6236\u7684\u96FB\u5B50\u90F5\u4EF6\u5730\u5740\u525B\u525B\u8B8A\u66F4\u70BA {{newEmail}}\u3002
4466
+
4467
+ \u5982\u679C\u662F\u60A8\u672C\u4EBA\u8B8A\u66F4\u7684\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002`
4468
+ },
4469
+ [MESSAGE_KEYS.phoneChanged]: {
4470
+ subject: "\u60A8\u7684\u96FB\u8A71\u865F\u78BC\u5DF2\u8B8A\u66F4",
4471
+ html: `<h1>\u60A8\u7684\u96FB\u8A71\u865F\u78BC\u5DF2\u8B8A\u66F4</h1>
4472
+ <p>\u60A8\u5E33\u6236\u7684\u96FB\u8A71\u865F\u78BC\u525B\u525B\u5B8C\u6210\u66F4\u65B0\u3002</p>
4473
+ <p class="muted">\u5982\u679C\u662F\u60A8\u672C\u4EBA\u8B8A\u66F4\u7684\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002</p>`,
4474
+ text: `\u60A8\u7684\u96FB\u8A71\u865F\u78BC\u5DF2\u8B8A\u66F4
4475
+
4476
+ \u60A8\u5E33\u6236\u7684\u96FB\u8A71\u865F\u78BC\u525B\u525B\u5B8C\u6210\u66F4\u65B0\u3002
4477
+
4478
+ \u5982\u679C\u662F\u60A8\u672C\u4EBA\u8B8A\u66F4\u7684\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\uFF0C\u53EF\u80FD\u6709\u4EBA\u5DF2\u80FD\u5B58\u53D6\u60A8\u7684\u5E33\u6236\u3002`
4479
+ },
4480
+ [MESSAGE_KEYS.passwordRevoked]: {
4481
+ subject: "\u60A8\u5E33\u6236\u7684\u5BC6\u78BC\u5DF2\u88AB\u79FB\u9664",
4482
+ html: `<h1>\u60A8\u7684\u5BC6\u78BC\u5DF2\u88AB\u79FB\u9664</h1>
4483
+ <p>\u60A8\u525B\u525B\u4F7F\u7528 <strong>{{provider}}</strong> \u767B\u5165\u3002\u6B64\u5E33\u6236\u5148\u524D\u8A2D\u5B9A\u904E\u5BC6\u78BC\uFF0C\u4F46\u9019\u500B\u96FB\u5B50\u90F5\u4EF6\u5730\u5740\u5F9E\u672A\u7D93\u904E\u78BA\u8A8D\uFF0C\u56E0\u6B64\u6211\u5011\u7121\u6CD5\u5224\u65B7\u5BC6\u78BC\u662F\u8AB0\u8A2D\u5B9A\u7684\uFF0C\u5DF2\u5C07\u5176\u79FB\u9664\u3002</p>
4484
+ <p>\u4F7F\u7528 {{provider}} \u767B\u5165\u4E0D\u53D7\u5F71\u97FF\u3002\u5982\u679C\u60A8\u4E5F\u60F3\u8A2D\u5B9A\u5BC6\u78BC\uFF0C\u53EF\u4EE5\u5728\u5E33\u6236\u8A2D\u5B9A\u4E2D\u9032\u884C\u8A2D\u5B9A\u3002</p>
4485
+ <p class="muted">\u5982\u679C\u60A8\u5F9E\u672A\u5728\u6B64\u8A2D\u5B9A\u904E\u5BC6\u78BC\uFF0C\u53EF\u80FD\u6709\u5176\u4ED6\u4EBA\u66FE\u8A66\u5716\u7528\u60A8\u7684\u5730\u5740\u8A3B\u518A\u3002\u60A8\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\uFF0C\u6B64\u5E33\u6236\u5C6C\u65BC\u60A8\u3002</p>`,
4486
+ text: `\u60A8\u7684\u5BC6\u78BC\u5DF2\u88AB\u79FB\u9664
4487
+
4488
+ \u60A8\u525B\u525B\u4F7F\u7528 {{provider}} \u767B\u5165\u3002\u6B64\u5E33\u6236\u5148\u524D\u8A2D\u5B9A\u904E\u5BC6\u78BC\uFF0C\u4F46\u9019\u500B\u96FB\u5B50\u90F5\u4EF6\u5730\u5740\u5F9E\u672A\u7D93\u904E\u78BA\u8A8D\uFF0C
4489
+ \u56E0\u6B64\u6211\u5011\u7121\u6CD5\u5224\u65B7\u5BC6\u78BC\u662F\u8AB0\u8A2D\u5B9A\u7684\uFF0C\u5DF2\u5C07\u5176\u79FB\u9664\u3002
4490
+
4491
+ \u4F7F\u7528 {{provider}} \u767B\u5165\u4E0D\u53D7\u5F71\u97FF\u3002\u5982\u679C\u60A8\u4E5F\u60F3\u8A2D\u5B9A\u5BC6\u78BC\uFF0C\u53EF\u4EE5\u5728\u5E33\u6236\u8A2D\u5B9A\u4E2D\u9032\u884C\u8A2D\u5B9A\u3002
4492
+
4493
+ \u5982\u679C\u60A8\u5F9E\u672A\u5728\u6B64\u8A2D\u5B9A\u904E\u5BC6\u78BC\uFF0C\u53EF\u80FD\u6709\u5176\u4ED6\u4EBA\u66FE\u8A66\u5716\u7528\u60A8\u7684\u5730\u5740\u8A3B\u518A\u3002\u60A8\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\uFF0C\u6B64\u5E33\u6236\u5C6C\u65BC\u60A8\u3002`
4494
+ },
4495
+ [MESSAGE_KEYS.oauthRegistration]: {
4496
+ subject: "\u6B61\u8FCE\u4F7F\u7528 {{appName}}",
4497
+ html: `<h1>\u6B61\u8FCE\u4F7F\u7528 {{appName}}</h1>
4498
+ <p>\u60A8\u7684\u5E33\u6236\u5DF2\u900F\u904E <strong>{{provider}}</strong> \u5EFA\u7ACB\u3002\u5F80\u5F8C\u96A8\u6642\u90FD\u80FD\u4F7F\u7528\u540C\u4E00\u500B {{provider}} \u5E33\u6236\u767B\u5165\uFF0C\u4E0D\u5FC5\u8A18\u4F4F\u5BC6\u78BC\u3002</p>
4499
+ <p class="muted">\u5982\u679C\u60A8\u6C92\u6709\u5EFA\u7ACB\u6B64\u5E33\u6236\uFF0C\u8ACB\u806F\u7D61\u5BA2\u670D\u3002</p>`,
4500
+ text: `\u6B61\u8FCE
4501
+
4502
+ \u60A8\u7684\u5E33\u6236\u5DF2\u900F\u904E {{provider}} \u5EFA\u7ACB\u3002\u5F80\u5F8C\u96A8\u6642\u90FD\u80FD\u4F7F\u7528\u540C\u4E00\u500B
4503
+ {{provider}} \u5E33\u6236\u767B\u5165\uFF0C\u4E0D\u5FC5\u8A18\u4F4F\u5BC6\u78BC\u3002
4504
+
4505
+ \u5982\u679C\u60A8\u6C92\u6709\u5EFA\u7ACB\u6B64\u5E33\u6236\uFF0C\u8ACB\u806F\u7D61\u5BA2\u670D\u3002`
4506
+ },
4507
+ [MESSAGE_KEYS.oauthLinked]: {
4508
+ subject: "\u60A8\u7684\u5E33\u6236\u65B0\u589E\u4E86\u4E00\u7A2E\u767B\u5165\u65B9\u5F0F",
4509
+ html: `<h1>\u5DF2\u65B0\u589E {{provider}} \u767B\u5165</h1>
4510
+ <p>\u73FE\u5728\u4E5F\u53EF\u4EE5\u4F7F\u7528 <strong>{{provider}}</strong> \u767B\u5165\u60A8\u7684\u5E33\u6236\u3002</p>
4511
+ <p class="muted">\u5982\u679C\u662F\u60A8\u672C\u4EBA\u65B0\u589E\u7684\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\u4E26\u8B8A\u66F4\u5BC6\u78BC\uFF0C\u53EF\u80FD\u6709\u5176\u4ED6\u4EBA\u80FD\u4EE5\u60A8\u7684\u8EAB\u5206\u767B\u5165\u3002</p>`,
4512
+ text: `\u5DF2\u65B0\u589E {{provider}} \u767B\u5165
4513
+
4514
+ \u73FE\u5728\u4E5F\u53EF\u4EE5\u4F7F\u7528 {{provider}} \u767B\u5165\u60A8\u7684\u5E33\u6236\u3002
4515
+
4516
+ \u5982\u679C\u662F\u60A8\u672C\u4EBA\u65B0\u589E\u7684\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\u4E26\u8B8A\u66F4\u5BC6\u78BC\uFF0C
4517
+ \u53EF\u80FD\u6709\u5176\u4ED6\u4EBA\u80FD\u4EE5\u60A8\u7684\u8EAB\u5206\u767B\u5165\u3002`
4518
+ },
4519
+ [MESSAGE_KEYS.oauthUnlinked]: {
4520
+ subject: "\u60A8\u7684\u5E33\u6236\u79FB\u9664\u4E86\u4E00\u7A2E\u767B\u5165\u65B9\u5F0F",
4521
+ html: `<h1>\u5DF2\u79FB\u9664 {{provider}} \u767B\u5165</h1>
4522
+ <p>\u5DF2\u7121\u6CD5\u518D\u4F7F\u7528 <strong>{{provider}}</strong> \u767B\u5165\u3002\u60A8\u7684\u96FB\u5B50\u90F5\u4EF6\u548C\u5BC6\u78BC\u4ECD\u53EF\u4F7F\u7528\u3002</p>
4523
+ <p class="muted">\u5982\u679C\u662F\u60A8\u672C\u4EBA\u79FB\u9664\u7684\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\u3002</p>`,
4524
+ text: `\u5DF2\u79FB\u9664 {{provider}} \u767B\u5165
4525
+
4526
+ \u5DF2\u7121\u6CD5\u518D\u4F7F\u7528 {{provider}} \u767B\u5165\u3002\u60A8\u7684\u96FB\u5B50\u90F5\u4EF6\u548C\u5BC6\u78BC\u4ECD\u53EF\u4F7F\u7528\u3002
4527
+
4528
+ \u5982\u679C\u662F\u60A8\u672C\u4EBA\u79FB\u9664\u7684\uFF0C\u7121\u9808\u63A1\u53D6\u4EFB\u4F55\u884C\u52D5\u3002\u5982\u679C\u4E0D\u662F\uFF0C\u8ACB\u7ACB\u5373\u806F\u7D61\u5BA2\u670D\u3002`
4529
+ }
4530
+ };
4531
+
4219
4532
  // src/templates.ts
4220
4533
  var EN_TEMPLATES = {
4221
4534
  [MESSAGE_KEYS.emailRegistration]: {
@@ -4373,7 +4686,12 @@ change your password \u2014 someone else may be able to sign in as you.`
4373
4686
  If you did this, you're all set. If not, contact support right away.`
4374
4687
  }
4375
4688
  };
4376
- var DEFAULT_TEMPLATES = (0, import_core14.withTranslations)(EN_TEMPLATES, { fr: FR_TEMPLATES, es: ES_TEMPLATES });
4689
+ var DEFAULT_TEMPLATES = (0, import_core14.withTranslations)(EN_TEMPLATES, {
4690
+ fr: FR_TEMPLATES,
4691
+ es: ES_TEMPLATES,
4692
+ "zh-Hans": ZH_HANS_TEMPLATES,
4693
+ "zh-Hant": ZH_HANT_TEMPLATES
4694
+ });
4377
4695
  var SAMPLE_PAYLOADS = {
4378
4696
  [MESSAGE_KEYS.passwordRevoked]: { provider: "Google" },
4379
4697
  [MESSAGE_KEYS.oauthRegistration]: { provider: "Google", appName: "Fonderie" },
@@ -4408,7 +4726,9 @@ async function importUser(store, user) {
4408
4726
  placeholders.push(`$${vals.length}`);
4409
4727
  };
4410
4728
  if (user.id !== void 0) add("id", user.id);
4411
- add("email", user.email.toLowerCase().trim());
4729
+ const email2 = normalizeEmailSafe(user.email);
4730
+ if (!email2) throw new Error(`[auth] importUser: '${user.email}' is not an email address`);
4731
+ add("email", email2);
4412
4732
  if (user.passwordHash !== void 0) add("password_hash", user.passwordHash);
4413
4733
  if (user.firstName !== void 0) add("first_name", user.firstName);
4414
4734
  if (user.lastName !== void 0) add("last_name", user.lastName);