@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/dist/index.d.cts CHANGED
@@ -169,6 +169,9 @@ declare const AUTH_DEFAULTS: {
169
169
  readonly LOCKOUT_DURATION_MINUTES: 30;
170
170
  readonly LOCKOUT_MAX_ATTEMPTS: 5;
171
171
  readonly PASSWORD_RESET_TTL_MINUTES: 15;
172
+ readonly EMAIL_VERIFICATION_TTL_HOURS: 24;
173
+ readonly EMAIL_CHANGE_TTL_HOURS: 24;
174
+ readonly TWO_FACTOR_EMAIL_CODE_TTL_MINUTES: 5;
172
175
  readonly TWO_FACTOR_CHALLENGE_TTL_SECONDS: 300;
173
176
  readonly APP_NAME: "App";
174
177
  };
@@ -260,6 +263,11 @@ interface EmailBranding {
260
263
  footerColor?: string;
261
264
  /** Heading and welcome-line color. Default: '#1e1b4b' */
262
265
  headingColor?: string;
266
+ /**
267
+ * Marketing site the footer's Privacy Policy (`/privacy`) and Get in Touch (`/contact`)
268
+ * links point at. Default: unset, and the footer has no links.
269
+ */
270
+ marketingUrl?: string;
263
271
  }
264
272
 
265
273
  /**
package/dist/index.d.ts CHANGED
@@ -169,6 +169,9 @@ declare const AUTH_DEFAULTS: {
169
169
  readonly LOCKOUT_DURATION_MINUTES: 30;
170
170
  readonly LOCKOUT_MAX_ATTEMPTS: 5;
171
171
  readonly PASSWORD_RESET_TTL_MINUTES: 15;
172
+ readonly EMAIL_VERIFICATION_TTL_HOURS: 24;
173
+ readonly EMAIL_CHANGE_TTL_HOURS: 24;
174
+ readonly TWO_FACTOR_EMAIL_CODE_TTL_MINUTES: 5;
172
175
  readonly TWO_FACTOR_CHALLENGE_TTL_SECONDS: 300;
173
176
  readonly APP_NAME: "App";
174
177
  };
@@ -260,6 +263,11 @@ interface EmailBranding {
260
263
  footerColor?: string;
261
264
  /** Heading and welcome-line color. Default: '#1e1b4b' */
262
265
  headingColor?: string;
266
+ /**
267
+ * Marketing site the footer's Privacy Policy (`/privacy`) and Get in Touch (`/contact`)
268
+ * links point at. Default: unset, and the footer has no links.
269
+ */
270
+ marketingUrl?: string;
263
271
  }
264
272
 
265
273
  /**
package/dist/index.js CHANGED
@@ -260,6 +260,9 @@ var AUTH_DEFAULTS = {
260
260
  LOCKOUT_DURATION_MINUTES: 30,
261
261
  LOCKOUT_MAX_ATTEMPTS: 5,
262
262
  PASSWORD_RESET_TTL_MINUTES: 15,
263
+ EMAIL_VERIFICATION_TTL_HOURS: 24,
264
+ EMAIL_CHANGE_TTL_HOURS: 24,
265
+ TWO_FACTOR_EMAIL_CODE_TTL_MINUTES: 5,
263
266
  TWO_FACTOR_CHALLENGE_TTL_SECONDS: 300,
264
267
  // 5 minutes
265
268
  APP_NAME: "App"
@@ -749,7 +752,7 @@ async function verifyTurnstileToken(token, secretKey, remoteIp, environment) {
749
752
 
750
753
  // src/core/email-change.ts
751
754
  import { eq as eq3 } from "drizzle-orm";
752
- var TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1e3;
755
+ var TOKEN_EXPIRY_MS = AUTH_DEFAULTS.EMAIL_CHANGE_TTL_HOURS * 60 * 60 * 1e3;
753
756
  async function requestEmailChange(params) {
754
757
  const { userId, password, newEmail, pepper, db, tables: tables2 } = params;
755
758
  const { users, emailChangeTokens } = tables2;
@@ -1004,8 +1007,7 @@ function generateEmailTemplate(options) {
1004
1007
  securityWarning,
1005
1008
  footerNote,
1006
1009
  appUrl,
1007
- marketingUrl = appUrl,
1008
- // Default to appUrl if no marketing site
1010
+ marketingUrl,
1009
1011
  appName = "App",
1010
1012
  buttonBgColor = "#00fe9a",
1011
1013
  buttonTextColor = "#000004",
@@ -1018,7 +1020,7 @@ function generateEmailTemplate(options) {
1018
1020
  const safeAppName = escapeHtml(appName);
1019
1021
  const safeButtonUrl = buttonUrl ? sanitizeEmailUrl(buttonUrl, appUrl) : "";
1020
1022
  const safeAppUrl = appUrl ? sanitizeEmailUrl(appUrl, appUrl) : "";
1021
- const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, appUrl) : "";
1023
+ const safeMarketingUrl = marketingUrl ? sanitizeEmailUrl(marketingUrl, marketingUrl).replace(/\/$/, "") : "";
1022
1024
  const safeLogoUrl = logoUrl ? sanitizeEmailUrl(logoUrl, appUrl) : `${safeAppUrl.replace(/\/$/, "")}/email-header-image.png`;
1023
1025
  return `
1024
1026
  <!DOCTYPE html>
@@ -1312,6 +1314,7 @@ function generateEmailTemplate(options) {
1312
1314
  <tr>
1313
1315
  <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;">
1314
1316
 
1317
+ ${safeMarketingUrl ? `
1315
1318
  <!-- Policy Links -->
1316
1319
  <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;">
1317
1320
  <tr>
@@ -1324,6 +1327,7 @@ function generateEmailTemplate(options) {
1324
1327
  </td>
1325
1328
  </tr>
1326
1329
  </table>
1330
+ ` : ""}
1327
1331
 
1328
1332
  <!-- Copyright & Disclaimer -->
1329
1333
  <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;">
@@ -1450,13 +1454,13 @@ async function cleanupExpiredPasswordlessChallenges(db, tables2) {
1450
1454
  function passwordlessEmail(input) {
1451
1455
  const { appName, appUrl, linkUrl, code } = input;
1452
1456
  const minutes = Math.max(1, Math.round((input.ttlMs ?? PASSWORDLESS_TTL_MS) / 6e4));
1453
- const lifetime = `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
1457
+ const lifetime2 = `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
1454
1458
  const safeLink = sanitizeEmailUrl(linkUrl, appUrl);
1455
1459
  const html = generateEmailTemplate({
1456
1460
  heading: `Sign in to ${escapeHtml(appName)}`,
1457
1461
  body: `<p>Use this code, or the button below, to sign in:</p>
1458
1462
  <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
1459
- <p>The code and the link expire in ${lifetime}.</p>`,
1463
+ <p>The code and the link expire in ${lifetime2}.</p>`,
1460
1464
  buttonText: "Sign in",
1461
1465
  buttonUrl: safeLink,
1462
1466
  footerNote: "If you didn't ask to sign in, you can ignore this email.",
@@ -1471,7 +1475,7 @@ function passwordlessEmail(input) {
1471
1475
  Or sign in with this link:
1472
1476
  ${safeLink}
1473
1477
 
1474
- The code and the link expire in ${lifetime}.
1478
+ The code and the link expire in ${lifetime2}.
1475
1479
 
1476
1480
  If you didn't ask to sign in, you can ignore this email.`
1477
1481
  };
@@ -2021,6 +2025,15 @@ import { eq as eq9 } from "drizzle-orm";
2021
2025
  import { eq as eq7 } from "drizzle-orm";
2022
2026
 
2023
2027
  // src/lib/email/templates.ts
2028
+ function lifetime(minutes) {
2029
+ if (minutes % 60 !== 0) return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
2030
+ const hours = minutes / 60;
2031
+ return `${hours} ${hours === 1 ? "hour" : "hours"}`;
2032
+ }
2033
+ var VERIFICATION_LIFETIME = lifetime(AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60);
2034
+ var PASSWORD_RESET_LIFETIME = lifetime(AUTH_DEFAULTS.PASSWORD_RESET_TTL_MINUTES);
2035
+ var EMAIL_CHANGE_LIFETIME = lifetime(AUTH_DEFAULTS.EMAIL_CHANGE_TTL_HOURS * 60);
2036
+ var TWO_FACTOR_CODE_LIFETIME = lifetime(AUTH_DEFAULTS.TWO_FACTOR_EMAIL_CODE_TTL_MINUTES);
2024
2037
  function extractAppUrl(url) {
2025
2038
  try {
2026
2039
  const parsed = new URL(url);
@@ -2036,7 +2049,7 @@ function generateVerificationEmail(data, appName = "Your App", configuredAppUrl
2036
2049
  const safeFirstName = firstName ? escapeHtml(firstName) : null;
2037
2050
  const html = generateEmailTemplate({
2038
2051
  heading: "",
2039
- body: `Please verify that your email address is <strong>${escapeHtml(email)}</strong>, and that you entered it when signing up for ${escapeHtml(appName)}.`,
2052
+ 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}.`,
2040
2053
  buttonText: "Verify Email",
2041
2054
  buttonUrl: safeUrl,
2042
2055
  welcomeMessage: safeFirstName ? `Hi ${safeFirstName}, Welcome to ${escapeHtml(appName)}!` : void 0,
@@ -2055,7 +2068,7 @@ Thank you for signing up for ${appName}! Please verify your email address by cli
2055
2068
 
2056
2069
  ${safeUrl}
2057
2070
 
2058
- This link expires in 48 hours.
2071
+ This link expires in ${VERIFICATION_LIFETIME}.
2059
2072
 
2060
2073
  If you didn't create an account with ${appName}, you can safely ignore this email.`,
2061
2074
  tags: {
@@ -2073,7 +2086,7 @@ function generatePasswordResetEmail(data, appName = "Your App", configuredAppUrl
2073
2086
  buttonText: "Reset Password",
2074
2087
  buttonUrl: safeUrl,
2075
2088
  securityWarning: `
2076
- <strong>Link expires in 1 hour.</strong><br>
2089
+ <strong>Link expires in ${PASSWORD_RESET_LIFETIME}.</strong><br>
2077
2090
  For security, this link can only be used once.
2078
2091
  `,
2079
2092
  footerNote: "If you didn't request this change, no action is needed. Your password will remain unchanged.",
@@ -2091,7 +2104,7 @@ We received a request to reset your password. Click the link below to create a n
2091
2104
 
2092
2105
  ${safeUrl}
2093
2106
 
2094
- This link will expire in 1 hour.
2107
+ This link expires in ${PASSWORD_RESET_LIFETIME}.
2095
2108
 
2096
2109
  If you didn't request a password reset, you can safely ignore this email.`,
2097
2110
  tags: {
@@ -2109,7 +2122,7 @@ function generateEmailChangeConfirmation(data, appName = "Your App", configuredA
2109
2122
  buttonText: "Confirm Email Change",
2110
2123
  buttonUrl: safeUrl,
2111
2124
  securityWarning: `
2112
- <strong>This link expires in 24 hours.</strong><br>
2125
+ <strong>This link expires in ${EMAIL_CHANGE_LIFETIME}.</strong><br>
2113
2126
  For security, this link can only be used once.
2114
2127
  `,
2115
2128
  footerNote: "If you didn't request this change, you can safely ignore this email.",
@@ -2129,7 +2142,7 @@ Click the link below to confirm this change:
2129
2142
 
2130
2143
  ${safeUrl}
2131
2144
 
2132
- This link expires in 24 hours.
2145
+ This link expires in ${EMAIL_CHANGE_LIFETIME}.
2133
2146
 
2134
2147
  If you didn't request this change, you can safely ignore this email.`,
2135
2148
  tags: {
@@ -2183,7 +2196,7 @@ function generate2faCodeEmail(data, appName = "Your App", appUrl = "", branding)
2183
2196
  <p>Hi ${safeFirstName},</p>
2184
2197
  <p>Your verification code is:</p>
2185
2198
  <p style="font-size: 32px; font-weight: bold; letter-spacing: 4px; text-align: center; margin: 24px 0; font-family: monospace;">${escapeHtml(code)}</p>
2186
- <p>This code expires in 5 minutes.</p>
2199
+ <p>This code expires in ${TWO_FACTOR_CODE_LIFETIME}.</p>
2187
2200
  <p>If you didn't request this code, you can safely ignore this email.</p>
2188
2201
  `,
2189
2202
  buttonText: "",
@@ -2203,7 +2216,7 @@ Your verification code is:
2203
2216
 
2204
2217
  ${code}
2205
2218
 
2206
- This code expires in 5 minutes.
2219
+ This code expires in ${TWO_FACTOR_CODE_LIFETIME}.
2207
2220
 
2208
2221
  If you didn't request this code, you can safely ignore this email.
2209
2222
 
@@ -3282,7 +3295,7 @@ var signupHandler = async (c) => {
3282
3295
  if (!invitedVerified) {
3283
3296
  const emailToken = generateSecureToken(32);
3284
3297
  const tokenHash = await hashToken(emailToken);
3285
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
3298
+ const expiresAt = Date.now() + AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60 * 60 * 1e3;
3286
3299
  await database.insert(schema.emailVerificationTokens).values({
3287
3300
  tokenHash,
3288
3301
  userId: newUser.id,
@@ -3361,7 +3374,7 @@ function generateEmailOtp() {
3361
3374
  const num = (bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]) >>> 0;
3362
3375
  return String(num % 1e6).padStart(6, "0");
3363
3376
  }
3364
- var EMAIL_CODE_TTL_SECONDS = 300;
3377
+ var EMAIL_CODE_TTL_SECONDS = AUTH_DEFAULTS.TWO_FACTOR_EMAIL_CODE_TTL_MINUTES * 60;
3365
3378
  async function sendEmailChallengeCode(env, database, usersTable, user, emailBranding) {
3366
3379
  const code = generateEmailOtp();
3367
3380
  await env.OAUTH_STATES.put(`email_2fa_challenge:${user.id}`, await hashToken(code), {
@@ -4419,7 +4432,7 @@ var resendVerificationHandler = async (c) => {
4419
4432
  await db.delete(schema.emailVerificationTokens).where(eq19(schema.emailVerificationTokens.userId, userId));
4420
4433
  const emailToken = generateSecureToken(32);
4421
4434
  const tokenHash = await hashToken(emailToken);
4422
- const expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
4435
+ const expiresAt = Date.now() + AUTH_DEFAULTS.EMAIL_VERIFICATION_TTL_HOURS * 60 * 60 * 1e3;
4423
4436
  await db.insert(schema.emailVerificationTokens).values({
4424
4437
  tokenHash,
4425
4438
  userId: user.id,