@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/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 =
|
|
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
|
|
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,
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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() +
|
|
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 =
|
|
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() +
|
|
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,
|