@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 +10 -0
- package/dist/index.cjs +31 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +31 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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
|
|
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,
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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() +
|
|
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 =
|
|
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() +
|
|
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,
|