@syncello/auth 4.0.0 → 4.1.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/README.md CHANGED
@@ -319,6 +319,16 @@ import {
319
319
 
320
320
  ## Release Notes
321
321
 
322
+ ### 4.1.0
323
+
324
+ - `emailBranding.marketingUrl`: the site the email footer's Privacy Policy (`/privacy`) and Get
325
+ in Touch (`/contact`) links point at. Without it the footer has no links; they used to point
326
+ at the app, where those pages don't exist.
327
+ - Emails state each link's or code's real lifetime from `AUTH_DEFAULTS`: the password reset
328
+ email said 1 hour for a 15-minute token, and the verification email 48 hours for a 24-hour
329
+ one. `AUTH_DEFAULTS` gains `EMAIL_VERIFICATION_TTL_HOURS`, `EMAIL_CHANGE_TTL_HOURS` and
330
+ `TWO_FACTOR_EMAIL_CODE_TTL_MINUTES`, which the token code now reads too (values unchanged).
331
+
322
332
  ### 4.0.0
323
333
 
324
334
  **Breaking:** `verifyWebhookSignature` takes all three svix headers. Svix signs
package/dist/index.cjs CHANGED
@@ -680,6 +680,9 @@ var AUTH_DEFAULTS = {
680
680
  LOCKOUT_DURATION_MINUTES: 30,
681
681
  LOCKOUT_MAX_ATTEMPTS: 5,
682
682
  PASSWORD_RESET_TTL_MINUTES: 15,
683
+ EMAIL_VERIFICATION_TTL_HOURS: 24,
684
+ EMAIL_CHANGE_TTL_HOURS: 24,
685
+ TWO_FACTOR_EMAIL_CODE_TTL_MINUTES: 5,
683
686
  TWO_FACTOR_CHALLENGE_TTL_SECONDS: 300,
684
687
  // 5 minutes
685
688
  APP_NAME: "App"
@@ -1169,7 +1172,7 @@ async function verifyTurnstileToken(token, secretKey, remoteIp, environment) {
1169
1172
 
1170
1173
  // src/core/email-change.ts
1171
1174
  var import_drizzle_orm3 = require("drizzle-orm");
1172
- var TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1e3;
1175
+ var TOKEN_EXPIRY_MS = AUTH_DEFAULTS.EMAIL_CHANGE_TTL_HOURS * 60 * 60 * 1e3;
1173
1176
  async function requestEmailChange(params) {
1174
1177
  const { userId, password, newEmail, pepper, db, tables: tables2 } = params;
1175
1178
  const { users, emailChangeTokens } = tables2;
@@ -1424,8 +1427,7 @@ function generateEmailTemplate(options) {
1424
1427
  securityWarning,
1425
1428
  footerNote,
1426
1429
  appUrl,
1427
- marketingUrl = appUrl,
1428
- // Default to appUrl if no marketing site
1430
+ marketingUrl,
1429
1431
  appName = "App",
1430
1432
  buttonBgColor = "#00fe9a",
1431
1433
  buttonTextColor = "#000004",
@@ -1438,7 +1440,7 @@ function generateEmailTemplate(options) {
1438
1440
  const safeAppName = escapeHtml(appName);
1439
1441
  const safeButtonUrl = buttonUrl ? sanitizeEmailUrl(buttonUrl, appUrl) : "";
1440
1442
  const safeAppUrl = appUrl ? sanitizeEmailUrl(appUrl, appUrl) : "";
1441
- const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, appUrl) : "";
1443
+ const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, marketingUrl).replace(/\/$/, "") : "";
1442
1444
  const safeLogoUrl = logoUrl ? sanitizeEmailUrl(logoUrl, appUrl) : `${safeAppUrl.replace(/\/$/, "")}/email-header-image.png`;
1443
1445
  return `
1444
1446
  <!DOCTYPE html>
@@ -1732,6 +1734,7 @@ function generateEmailTemplate(options) {
1732
1734
  <tr>
1733
1735
  <td class="column column-1" width="100%" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; vertical-align: top; padding: 20px;">
1734
1736
 
1737
+ ${safeMarketingUrl ? `
1735
1738
  <!-- Policy Links -->
1736
1739
  <table class="paragraph_block" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
1737
1740
  <tr>
@@ -1744,6 +1747,7 @@ function generateEmailTemplate(options) {
1744
1747
  </td>
1745
1748
  </tr>
1746
1749
  </table>
1750
+ ` : ""}
1747
1751
 
1748
1752
  <!-- Copyright & Disclaimer -->
1749
1753
  <table class="paragraph_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
@@ -1870,13 +1874,13 @@ async function cleanupExpiredPasswordlessChallenges(db, tables2) {
1870
1874
  function passwordlessEmail(input) {
1871
1875
  const { appName, appUrl, linkUrl, code } = input;
1872
1876
  const minutes = Math.max(1, Math.round((input.ttlMs ?? PASSWORDLESS_TTL_MS) / 6e4));
1873
- const lifetime = `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
1877
+ const lifetime2 = `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
1874
1878
  const safeLink = sanitizeEmailUrl(linkUrl, appUrl);
1875
1879
  const html = generateEmailTemplate({
1876
1880
  heading: `Sign in to ${escapeHtml(appName)}`,
1877
1881
  body: `<p>Use this code, or the button below, to sign in:</p>
1878
1882
  <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
1879
- <p>The code and the link expire in ${lifetime}.</p>`,
1883
+ <p>The code and the link expire in ${lifetime2}.</p>`,
1880
1884
  buttonText: "Sign in",
1881
1885
  buttonUrl: safeLink,
1882
1886
  footerNote: "If you didn't ask to sign in, you can ignore this email.",
@@ -1891,7 +1895,7 @@ function passwordlessEmail(input) {
1891
1895
  Or sign in with this link:
1892
1896
  ${safeLink}
1893
1897
 
1894
- The code and the link expire in ${lifetime}.
1898
+ The code and the link expire in ${lifetime2}.
1895
1899
 
1896
1900
  If you didn't ask to sign in, you can ignore this email.`
1897
1901
  };
@@ -2441,6 +2445,15 @@ var import_drizzle_orm10 = require("drizzle-orm");
2441
2445
  var import_drizzle_orm7 = require("drizzle-orm");
2442
2446
 
2443
2447
  // src/lib/email/templates.ts
2448
+ function lifetime(minutes) {
2449
+ if (minutes % 60 !== 0) return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
2450
+ const hours = minutes / 60;
2451
+ return `${hours} ${hours === 1 ? "hour" : "hours"}`;
2452
+ }
2453
+ var VERIFICATION_LIFETIME = lifetime(AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60);
2454
+ var PASSWORD_RESET_LIFETIME = lifetime(AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES);
2455
+ var EMAIL_CHANGE_LIFETIME = lifetime(AUTH_DEFAULTS.EMAIL_CHANGE_TTL_HOURS * 60);
2456
+ var TWO_FACTOR_CODE_LIFETIME = lifetime(AUTH_DEFAULTS.TWO_FACTOR_EMAIL_CODE_TTL_MINUTES);
2444
2457
  function extractAppUrl(url) {
2445
2458
  try {
2446
2459
  const parsed = new URL(url);
@@ -2456,7 +2469,7 @@ function generateVerificationEmail(data, appName = "Your App", configuredAppUrl
2456
2469
  const safeFirstName = firstName ? escapeHtml(firstName) : null;
2457
2470
  const html = generateEmailTemplate({
2458
2471
  heading: "",
2459
- body: `Please verify that your email address is <strong>${escapeHtml(email)}</strong>, and that you entered it when signing up for ${escapeHtml(appName)}.`,
2472
+ body: `Please verify that your email address is <strong>${escapeHtml(email)}</strong>, and that you entered it when signing up for ${escapeHtml(appName)}. This link expires in ${VERIFICATION_LIFETIME}.`,
2460
2473
  buttonText: "Verify Email",
2461
2474
  buttonUrl: safeUrl,
2462
2475
  welcomeMessage: safeFirstName ? `Hi ${safeFirstName}, Welcome to ${escapeHtml(appName)}!` : void 0,
@@ -2475,7 +2488,7 @@ Thank you for signing up for ${appName}! Please verify your email address by cli
2475
2488
 
2476
2489
  ${safeUrl}
2477
2490
 
2478
- This link expires in 48 hours.
2491
+ This link expires in ${VERIFICATION_LIFETIME}.
2479
2492
 
2480
2493
  If you didn't create an account with ${appName}, you can safely ignore this email.`,
2481
2494
  tags: {
@@ -2493,7 +2506,7 @@ function generatePasswordResetEmail(data, appName = "Your App", configuredAppUrl
2493
2506
  buttonText: "Reset Password",
2494
2507
  buttonUrl: safeUrl,
2495
2508
  securityWarning: `
2496
- <strong>Link expires in 1 hour.</strong><br>
2509
+ <strong>Link expires in ${PASSWORD_RESET_LIFETIME}.</strong><br>
2497
2510
  For security, this link can only be used once.
2498
2511
  `,
2499
2512
  footerNote: "If you didn't request this change, no action is needed. Your password will remain unchanged.",
@@ -2511,7 +2524,7 @@ We received a request to reset your password. Click the link below to create a n
2511
2524
 
2512
2525
  ${safeUrl}
2513
2526
 
2514
- This link will expire in 1 hour.
2527
+ This link expires in ${PASSWORD_RESET_LIFETIME}.
2515
2528
 
2516
2529
  If you didn't request a password reset, you can safely ignore this email.`,
2517
2530
  tags: {
@@ -2529,7 +2542,7 @@ function generateEmailChangeConfirmation(data, appName = "Your App", configuredA
2529
2542
  buttonText: "Confirm Email Change",
2530
2543
  buttonUrl: safeUrl,
2531
2544
  securityWarning: `
2532
- <strong>This link expires in 24 hours.</strong><br>
2545
+ <strong>This link expires in ${EMAIL_CHANGE_LIFETIME}.</strong><br>
2533
2546
  For security, this link can only be used once.
2534
2547
  `,
2535
2548
  footerNote: "If you didn't request this change, you can safely ignore this email.",
@@ -2549,7 +2562,7 @@ Click the link below to confirm this change:
2549
2562
 
2550
2563
  ${safeUrl}
2551
2564
 
2552
- This link expires in 24 hours.
2565
+ This link expires in ${EMAIL_CHANGE_LIFETIME}.
2553
2566
 
2554
2567
  If you didn't request this change, you can safely ignore this email.`,
2555
2568
  tags: {
@@ -2603,7 +2616,7 @@ function generate2faCodeEmail(data, appName = "Your App", appUrl = "", branding)
2603
2616
  <p>Hi ${safeFirstName},</p>
2604
2617
  <p>Your verification code is:</p>
2605
2618
  <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
2606
- <p>This code expires in 5 minutes.</p>
2619
+ <p>This code expires in ${TWO_FACTOR_CODE_LIFETIME}.</p>
2607
2620
  <p>If you didn't request this code, you can safely ignore this email.</p>
2608
2621
  `,
2609
2622
  buttonText: "",
@@ -2623,7 +2636,7 @@ Your verification code is:
2623
2636
 
2624
2637
  ${code}
2625
2638
 
2626
- This code expires in 5 minutes.
2639
+ This code expires in ${TWO_FACTOR_CODE_LIFETIME}.
2627
2640
 
2628
2641
  If you didn't request this code, you can safely ignore this email.
2629
2642
 
@@ -3702,7 +3715,7 @@ var signupHandler = async (c) => {
3702
3715
  if (!invitedVerified) {
3703
3716
  const emailToken = generateSecureToken(32);
3704
3717
  const tokenHash = await hashToken(emailToken);
3705
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
3718
+ const expiresAt = Date.now() + AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60 * 60 * 1e3;
3706
3719
  await database.insert(schema.emailVerificationTokens).values({
3707
3720
  tokenHash,
3708
3721
  userId: newUser.id,
@@ -3781,7 +3794,7 @@ function generateEmailOtp() {
3781
3794
  const num = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
3782
3795
  return String(num % 1e6).padStart(6, "0");
3783
3796
  }
3784
- var EMAIL_CODE_TTL_SECONDS = 300;
3797
+ var EMAIL_CODE_TTL_SECONDS = AUTH_DEFAULTS.TWO_FACTOR_EMAIL_CODE_TTL_MINUTES * 60;
3785
3798
  async function sendEmailChallengeCode(env, database, usersTable, user, emailBranding) {
3786
3799
  const code = generateEmailOtp();
3787
3800
  await env.OAUTH_STATES.put(`email_2fa_challenge:${user.id}`, await hashToken(code), {
@@ -4839,7 +4852,7 @@ var resendVerificationHandler = async (c) => {
4839
4852
  await db.delete(schema.emailVerificationTokens).where((0, import_drizzle_orm20.eq)(schema.emailVerificationTokens.userId, userId));
4840
4853
  const emailToken = generateSecureToken(32);
4841
4854
  const tokenHash = await hashToken(emailToken);
4842
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
4855
+ const expiresAt = Date.now() + AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60 * 60 * 1e3;
4843
4856
  await db.insert(schema.emailVerificationTokens).values({
4844
4857
  tokenHash,
4845
4858
  userId: user.id,