@syncello/auth 4.1.0 → 4.2.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 +37 -0
- package/dist/index.cjs +210 -276
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +209 -276
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -297,6 +297,29 @@ A `verify` hook that throws is logged and treated as "not invited": invitation m
|
|
|
297
297
|
OAuth sign-up in open mode goes ahead unlinked, and a password signup with the token answers
|
|
298
298
|
`400`.
|
|
299
299
|
|
|
300
|
+
## Custom Emails
|
|
301
|
+
|
|
302
|
+
`renderEmail` puts an app's own emails in the same template as the auth emails. Every value is
|
|
303
|
+
plain text and escaped; every URL must be http(s) on `appUrl`'s host or a subdomain.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import { renderEmail } from '@syncello/auth';
|
|
307
|
+
|
|
308
|
+
const html = renderEmail({
|
|
309
|
+
heading: 'Your daily digest',
|
|
310
|
+
paragraphs: ['Here is what changed today.'],
|
|
311
|
+
sections: [
|
|
312
|
+
{ title: 'Acme', url: `${appUrl}/accounts/1`, rows: [{ text: 'PO 1001 shipped', url: `${appUrl}/orders/1001` }] },
|
|
313
|
+
],
|
|
314
|
+
button: { label: 'Open the portal', url: `${appUrl}/orders` },
|
|
315
|
+
notice: 'This link expires in 7 days.',
|
|
316
|
+
footerNote: 'You get this because you follow Acme.',
|
|
317
|
+
appUrl,
|
|
318
|
+
appName: 'My App',
|
|
319
|
+
branding: emailBranding,
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
300
323
|
## Direct Imports
|
|
301
324
|
|
|
302
325
|
For advanced usage, you can import utilities directly:
|
|
@@ -319,6 +342,20 @@ import {
|
|
|
319
342
|
|
|
320
343
|
## Release Notes
|
|
321
344
|
|
|
345
|
+
### 4.2.0
|
|
346
|
+
|
|
347
|
+
- `renderEmail` renders an app's own emails (invitations, notifications, digests) in the
|
|
348
|
+
template the auth emails use, from plain text: a heading, paragraphs, optional sections of
|
|
349
|
+
rows, a button, a notice, a footer note and `EmailBranding`. It escapes every value and
|
|
350
|
+
refuses links off the app's host (see Custom Emails).
|
|
351
|
+
- `emailBranding.logoWidth` and `logoHeight` (CSS px, positive integers, given together): the
|
|
352
|
+
header logo renders at that fixed size, with `width` and `height` attributes so mail clients
|
|
353
|
+
reserve its space and Outlook sizes it right, instead of filling half the email's width.
|
|
354
|
+
- A cleaner template for every email, with no new options needed: a white card on a light grey
|
|
355
|
+
page, a left-aligned heading and body, the notice as a grey panel edged in `headingColor`
|
|
356
|
+
instead of the cyan box, and the link to copy small and grey under a thin rule. URLs in the
|
|
357
|
+
markup are now HTML-escaped (`&` is written `&`), which browsers read as the same URL.
|
|
358
|
+
|
|
322
359
|
### 4.1.0
|
|
323
360
|
|
|
324
361
|
- `emailBranding.marketingUrl`: the site the email footer's Privacy Policy (`/privacy`) and Get
|
package/dist/index.cjs
CHANGED
|
@@ -121,6 +121,7 @@ __export(src_exports, {
|
|
|
121
121
|
rateLimit: () => rateLimit,
|
|
122
122
|
recentPasswordlessFailures: () => recentPasswordlessFailures,
|
|
123
123
|
refreshSession: () => refreshSession,
|
|
124
|
+
renderEmail: () => renderEmail,
|
|
124
125
|
requestEmailChange: () => requestEmailChange,
|
|
125
126
|
requireAuth: () => requireAuth,
|
|
126
127
|
requireVerifiedEmail: () => requireVerifiedEmail,
|
|
@@ -1417,36 +1418,140 @@ function escapeHtml(text) {
|
|
|
1417
1418
|
}
|
|
1418
1419
|
|
|
1419
1420
|
// src/core/email-template.ts
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1421
|
+
var FONT = "'Montserrat',Arial,sans-serif";
|
|
1422
|
+
var PAGE = "#f4f5f7";
|
|
1423
|
+
var BODY_TEXT = "#404040";
|
|
1424
|
+
var MUTED = "#737373";
|
|
1425
|
+
var RULE = "#e5e7eb";
|
|
1426
|
+
var TABLE = 'width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;"';
|
|
1427
|
+
function logoDimensions(width, height) {
|
|
1428
|
+
if (width === void 0 && height === void 0) return null;
|
|
1429
|
+
if (width === void 0 || height === void 0) {
|
|
1430
|
+
throw new Error("logoWidth and logoHeight must be given together");
|
|
1431
|
+
}
|
|
1432
|
+
return { width: pixels("logoWidth", width), height: pixels("logoHeight", height) };
|
|
1433
|
+
}
|
|
1434
|
+
function pixels(name, value) {
|
|
1435
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
1436
|
+
throw new Error(`${name} must be a positive integer`);
|
|
1437
|
+
}
|
|
1438
|
+
return escapeHtml(String(value));
|
|
1439
|
+
}
|
|
1440
|
+
function brandColours(branding) {
|
|
1441
|
+
return {
|
|
1442
|
+
buttonBgColor: escapeHtml(branding.buttonBgColor ?? "#00fe9a"),
|
|
1443
|
+
buttonTextColor: escapeHtml(branding.buttonTextColor ?? "#000004"),
|
|
1444
|
+
buttonFontWeight: escapeHtml(String(branding.buttonFontWeight ?? 500)),
|
|
1445
|
+
buttonRadius: escapeHtml(branding.buttonRadius ?? "6px"),
|
|
1446
|
+
footerColor: escapeHtml(branding.footerColor ?? "#1e1b4b"),
|
|
1447
|
+
headingColor: escapeHtml(branding.headingColor ?? "#1e1b4b")
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function logoUrlFor(logoUrl, appUrl) {
|
|
1451
|
+
if (logoUrl) return escapeHtml(sanitizeEmailUrl(logoUrl, appUrl));
|
|
1452
|
+
const base = appUrl ? sanitizeEmailUrl(appUrl, appUrl) : "";
|
|
1453
|
+
return escapeHtml(`${base.replace(/\/$/, "")}/email-header-image.png`);
|
|
1454
|
+
}
|
|
1455
|
+
function marketingUrlFor(marketingUrl) {
|
|
1456
|
+
return marketingUrl ? escapeHtml(sanitizeEmailUrl(marketingUrl, marketingUrl).replace(/\/$/, "")) : "";
|
|
1457
|
+
}
|
|
1458
|
+
var cardRow = (padding, content) => `
|
|
1459
|
+
<table ${TABLE}>
|
|
1460
|
+
<tr>
|
|
1461
|
+
<td class="pad" style="${padding}">
|
|
1462
|
+
${content}
|
|
1463
|
+
</td>
|
|
1464
|
+
</tr>
|
|
1465
|
+
</table>`;
|
|
1466
|
+
function logoBlock({ logoUrl, logoSize, appName }) {
|
|
1467
|
+
const img = logoSize ? `<img src="${logoUrl}" width="${logoSize.width}" height="${logoSize.height}" style="width: ${logoSize.width}px; max-width: 100%; height: auto; display: block; border: 0;" alt="${appName}" title="${appName}">` : `<div class="fullWidth" style="max-width: 50%;">
|
|
1468
|
+
<img src="${logoUrl}" style="display: block; height: auto; border: 0; width: 100%;" alt="${appName}" title="${appName}" height="auto">
|
|
1469
|
+
</div>`;
|
|
1470
|
+
return `
|
|
1471
|
+
<!-- Header Image -->
|
|
1472
|
+
<table class="image_block" ${TABLE}>
|
|
1473
|
+
<tr>
|
|
1474
|
+
<td class="pad" style="padding-top:40px;padding-left:40px;padding-right:40px;padding-bottom:8px;">
|
|
1475
|
+
<div class="alignment" align="center">
|
|
1476
|
+
${img}
|
|
1477
|
+
</div>
|
|
1478
|
+
</td>
|
|
1479
|
+
</tr>
|
|
1480
|
+
</table>`;
|
|
1481
|
+
}
|
|
1482
|
+
function buttonBlock({ label, url }, brand) {
|
|
1483
|
+
const font = `font-family: ${FONT}; font-size: 16px; font-weight: ${brand.buttonFontWeight};`;
|
|
1484
|
+
return `
|
|
1485
|
+
<!-- Button -->${cardRow(
|
|
1486
|
+
"padding-top:8px;padding-bottom:32px;padding-left:40px;padding-right:40px;text-align:center;",
|
|
1487
|
+
`<div class="alignment" align="center">
|
|
1488
|
+
<!--[if mso]>
|
|
1489
|
+
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${url}" style="height:48px;width:auto;v-text-anchor:middle;" arcsize="12%" fillcolor="${brand.buttonBgColor}">
|
|
1490
|
+
<v:stroke dashstyle="Solid" weight="0px" color="${brand.buttonBgColor}"/>
|
|
1491
|
+
<w:anchorlock/>
|
|
1492
|
+
<v:textbox inset="0px,0px,0px,0px">
|
|
1493
|
+
<center dir="false" style="color:${brand.buttonTextColor};${font}">
|
|
1494
|
+
<![endif]-->
|
|
1495
|
+
<a href="${url}" style="background-color: ${brand.buttonBgColor}; border: 0px solid transparent; border-radius: ${brand.buttonRadius}; color: ${brand.buttonTextColor}; display: inline-block; ${font} line-height: 24px; mso-border-alt: none; padding: 12px 28px; text-align: center; text-decoration: none; word-break: keep-all;">${label}</a>
|
|
1496
|
+
<!--[if mso]>
|
|
1497
|
+
</center>
|
|
1498
|
+
</v:textbox>
|
|
1499
|
+
</v:roundrect>
|
|
1500
|
+
<![endif]-->
|
|
1501
|
+
</div>`
|
|
1502
|
+
)}
|
|
1503
|
+
|
|
1504
|
+
<!-- Copy Link -->${cardRow(
|
|
1505
|
+
"padding-bottom:24px;padding-left:40px;padding-right:40px;",
|
|
1506
|
+
`<div style="border-top: 1px solid ${RULE}; padding-top: 16px; color:${MUTED};font-family:${FONT};font-size:12px;line-height:1.5;text-align:left;mso-line-height-alt:18px;">
|
|
1507
|
+
<p style="margin: 0 0 4px 0; word-break: break-word;">Or copy and paste this link into your browser:</p>
|
|
1508
|
+
<a href="${url}" style="color:${MUTED};text-decoration:underline;word-break:break-all;">${url}</a>
|
|
1509
|
+
</div>`
|
|
1510
|
+
)}`;
|
|
1511
|
+
}
|
|
1512
|
+
function renderLayout(parts) {
|
|
1513
|
+
const { brand } = parts;
|
|
1514
|
+
const welcome = parts.welcome ? `
|
|
1515
|
+
<!-- Welcome Message -->${cardRow(
|
|
1516
|
+
"padding-top:24px;padding-left:40px;padding-right:40px;",
|
|
1517
|
+
`<div style="color:${brand.headingColor};font-family:${FONT};font-size:20px;font-weight:600;line-height:1.4;text-align:left;mso-line-height-alt:28px;">
|
|
1518
|
+
<p style="margin: 0; word-break: break-word;">${parts.welcome}</p>
|
|
1519
|
+
</div>`
|
|
1520
|
+
)}` : "";
|
|
1521
|
+
const heading = parts.heading ? `
|
|
1522
|
+
<!-- Heading -->${cardRow(
|
|
1523
|
+
"padding-top:24px;padding-left:40px;padding-right:40px;",
|
|
1524
|
+
`<div style="color:${brand.headingColor};font-family:${FONT};font-size:24px;font-weight:700;line-height:1.3;text-align:left;mso-line-height-alt:31px;">
|
|
1525
|
+
<p style="margin: 0; word-break: break-word;">${parts.heading}</p>
|
|
1526
|
+
</div>`
|
|
1527
|
+
)}` : "";
|
|
1528
|
+
const notice = parts.notice ? `
|
|
1529
|
+
<!-- Notice -->${cardRow(
|
|
1530
|
+
"padding-bottom:24px;padding-left:40px;padding-right:40px;",
|
|
1531
|
+
`<table ${TABLE.replace('style="', `style="background-color: ${PAGE}; border-left: 3px solid ${brand.headingColor}; border-radius: 4px; `)}>
|
|
1532
|
+
<tr>
|
|
1533
|
+
<td style="padding: 14px 16px; color: ${BODY_TEXT}; font-family: ${FONT}; font-size: 13px; line-height: 1.5; text-align: left;">
|
|
1534
|
+
${parts.notice}
|
|
1535
|
+
</td>
|
|
1536
|
+
</tr>
|
|
1537
|
+
</table>`
|
|
1538
|
+
)}` : "";
|
|
1539
|
+
const footerNote = parts.footerNote ? `
|
|
1540
|
+
<!-- Footer Note -->${cardRow(
|
|
1541
|
+
"padding-bottom:32px;padding-left:40px;padding-right:40px;",
|
|
1542
|
+
`<div style="color:${MUTED};font-family:${FONT};font-size:13px;line-height:1.5;text-align:left;mso-line-height-alt:20px;">
|
|
1543
|
+
<p style="margin: 0; word-break: break-word;">${parts.footerNote}</p>
|
|
1544
|
+
</div>`
|
|
1545
|
+
)}` : "";
|
|
1546
|
+
const links = parts.marketingUrl ? `
|
|
1547
|
+
<p style="margin: 0 0 12px 0; word-break: break-word;">
|
|
1548
|
+
<a href="${parts.marketingUrl}/privacy" style="color: #ffffff; text-decoration: underline;">Privacy Policy</a> | <a href="${parts.marketingUrl}/contact" style="color: #ffffff; text-decoration: underline;">Get in Touch</a>
|
|
1549
|
+
</p>` : "";
|
|
1445
1550
|
return `
|
|
1446
1551
|
<!DOCTYPE html>
|
|
1447
1552
|
<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" lang="en">
|
|
1448
1553
|
<head>
|
|
1449
|
-
<title>${
|
|
1554
|
+
<title>${parts.title}</title>
|
|
1450
1555
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
1451
1556
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1452
1557
|
<!--[if mso]>
|
|
@@ -1461,7 +1566,7 @@ function generateEmailTemplate(options) {
|
|
|
1461
1566
|
</xml>
|
|
1462
1567
|
<![endif]-->
|
|
1463
1568
|
<!--[if !mso]><!-->
|
|
1464
|
-
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@
|
|
1569
|
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet" type="text/css">
|
|
1465
1570
|
<!--<![endif]-->
|
|
1466
1571
|
<style>
|
|
1467
1572
|
* {
|
|
@@ -1482,13 +1587,6 @@ function generateEmailTemplate(options) {
|
|
|
1482
1587
|
p {
|
|
1483
1588
|
line-height: inherit;
|
|
1484
1589
|
}
|
|
1485
|
-
.desktop_hide,
|
|
1486
|
-
.desktop_hide table {
|
|
1487
|
-
mso-hide: all;
|
|
1488
|
-
display: none;
|
|
1489
|
-
max-height: 0px;
|
|
1490
|
-
overflow: hidden;
|
|
1491
|
-
}
|
|
1492
1590
|
.image_block img+div {
|
|
1493
1591
|
display: none;
|
|
1494
1592
|
}
|
|
@@ -1497,41 +1595,16 @@ function generateEmailTemplate(options) {
|
|
|
1497
1595
|
line-height: 0;
|
|
1498
1596
|
}
|
|
1499
1597
|
@media (max-width:660px) {
|
|
1500
|
-
.
|
|
1501
|
-
|
|
1502
|
-
}
|
|
1503
|
-
.icons-inner {
|
|
1504
|
-
text-align: center;
|
|
1505
|
-
}
|
|
1506
|
-
.icons-inner td {
|
|
1507
|
-
margin: 0 auto;
|
|
1508
|
-
}
|
|
1509
|
-
.mobile_hide {
|
|
1510
|
-
display: none;
|
|
1598
|
+
.page {
|
|
1599
|
+
padding: 16px 8px !important;
|
|
1511
1600
|
}
|
|
1512
1601
|
.row-content {
|
|
1513
1602
|
width: 100% !important;
|
|
1514
1603
|
max-width: 100% !important;
|
|
1515
1604
|
}
|
|
1516
|
-
.stack .column {
|
|
1517
|
-
width: 100%;
|
|
1518
|
-
display: block;
|
|
1519
|
-
}
|
|
1520
|
-
.mobile_hide {
|
|
1521
|
-
min-height: 0;
|
|
1522
|
-
max-height: 0;
|
|
1523
|
-
max-width: 0;
|
|
1524
|
-
overflow: hidden;
|
|
1525
|
-
font-size: 0px;
|
|
1526
|
-
}
|
|
1527
|
-
.desktop_hide,
|
|
1528
|
-
.desktop_hide table {
|
|
1529
|
-
display: table !important;
|
|
1530
|
-
max-height: none !important;
|
|
1531
|
-
}
|
|
1532
1605
|
.pad {
|
|
1533
|
-
padding-left:
|
|
1534
|
-
padding-right:
|
|
1606
|
+
padding-left: 24px !important;
|
|
1607
|
+
padding-right: 24px !important;
|
|
1535
1608
|
}
|
|
1536
1609
|
}
|
|
1537
1610
|
</style>
|
|
@@ -1543,235 +1616,46 @@ function generateEmailTemplate(options) {
|
|
|
1543
1616
|
</style>
|
|
1544
1617
|
<![endif]-->
|
|
1545
1618
|
</head>
|
|
1546
|
-
<body class="body" style="background-color:
|
|
1547
|
-
<table class="nl-container"
|
|
1619
|
+
<body class="body" style="background-color: ${PAGE}; margin: 0; padding: 0; -webkit-text-size-adjust: none; text-size-adjust: none;">
|
|
1620
|
+
<table class="nl-container" ${TABLE.replace('style="', `style="background-color: ${PAGE}; `)}>
|
|
1548
1621
|
<tbody>
|
|
1549
1622
|
<tr>
|
|
1550
|
-
<td>
|
|
1551
|
-
<!--
|
|
1552
|
-
<table class="row
|
|
1623
|
+
<td class="page" style="padding: 32px 16px;">
|
|
1624
|
+
<!-- Card -->
|
|
1625
|
+
<table class="row-content" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #ffffff; border: 1px solid ${RULE}; border-radius: 12px; max-width: 640px; width: 100%; margin: 0 auto;">
|
|
1553
1626
|
<tbody>
|
|
1554
1627
|
<tr>
|
|
1555
|
-
<td>
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
<span style="word-break: break-word;"> </span>
|
|
1570
|
-
</td>
|
|
1571
|
-
</tr>
|
|
1572
|
-
</table>
|
|
1573
|
-
</div>
|
|
1574
|
-
</td>
|
|
1575
|
-
</tr>
|
|
1576
|
-
</table>
|
|
1628
|
+
<td width="100%" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; text-align: left; vertical-align: top;">
|
|
1629
|
+
${logoBlock(parts)}
|
|
1630
|
+
${welcome}
|
|
1631
|
+
${heading}
|
|
1632
|
+
|
|
1633
|
+
<!-- Body Text -->${cardRow(
|
|
1634
|
+
"padding-top:16px;padding-bottom:24px;padding-left:40px;padding-right:40px;",
|
|
1635
|
+
`<div style="color:${BODY_TEXT};font-family:${FONT};font-size:15px;line-height:1.6;text-align:left;mso-line-height-alt:24px;">
|
|
1636
|
+
${parts.body}
|
|
1637
|
+
</div>`
|
|
1638
|
+
)}
|
|
1639
|
+
${parts.button ? buttonBlock(parts.button, brand) : ""}
|
|
1640
|
+
${notice}
|
|
1641
|
+
${footerNote}
|
|
1577
1642
|
|
|
1578
|
-
<!-- Header Image -->
|
|
1579
|
-
<table class="image_block block-2" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
|
|
1580
|
-
<tr>
|
|
1581
|
-
<td class="pad" style="padding-left:40px;padding-right:40px;width:100%;">
|
|
1582
|
-
<div class="alignment" align="center">
|
|
1583
|
-
<div class="fullWidth" style="max-width: 50%;">
|
|
1584
|
-
<img src="${safeLogoUrl}" style="display: block; height: auto; border: 0; width: 100%;" alt="${safeAppName}" title="${safeAppName}" height="auto">
|
|
1585
|
-
</div>
|
|
1586
|
-
</div>
|
|
1587
|
-
</td>
|
|
1588
|
-
</tr>
|
|
1589
|
-
</table>
|
|
1590
|
-
|
|
1591
|
-
${welcomeMessage ? `
|
|
1592
|
-
<!-- Welcome Message -->
|
|
1593
|
-
<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;">
|
|
1594
|
-
<tr>
|
|
1595
|
-
<td class="pad" style="padding-top:30px;padding-left:40px;padding-right:40px;">
|
|
1596
|
-
<div style="color:${headingColor};font-family:'Montserrat',Arial,sans-serif;font-size:22px;font-weight:600;line-height:1.4;text-align:center;mso-line-height-alt:31px;">
|
|
1597
|
-
<p style="margin: 0; word-break: break-word;">${welcomeMessage}</p>
|
|
1598
|
-
</div>
|
|
1599
|
-
</td>
|
|
1600
|
-
</tr>
|
|
1601
|
-
</table>
|
|
1602
|
-
` : ""}
|
|
1603
|
-
|
|
1604
|
-
${heading ? `
|
|
1605
|
-
<!-- Heading -->
|
|
1606
|
-
<table class="paragraph_block block-4" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
|
|
1607
|
-
<tr>
|
|
1608
|
-
<td class="pad" style="padding-bottom:10px;padding-left:40px;padding-right:40px;padding-top:10px;">
|
|
1609
|
-
<div style="color:${headingColor};font-family:'Montserrat',Arial,sans-serif;font-size:30px;font-weight:700;line-height:1.2;text-align:center;mso-line-height-alt:36px;">
|
|
1610
|
-
<p style="margin: 0; word-break: break-word;">${heading}</p>
|
|
1611
|
-
</div>
|
|
1612
|
-
</td>
|
|
1613
|
-
</tr>
|
|
1614
|
-
</table>
|
|
1615
|
-
` : ""}
|
|
1616
|
-
|
|
1617
|
-
<!-- Body Text -->
|
|
1618
|
-
<table class="paragraph_block block-5" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; word-break: break-word;">
|
|
1619
|
-
<tr>
|
|
1620
|
-
<td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;padding-top:30px;">
|
|
1621
|
-
<div style="color:#404040;font-family:'Montserrat',Arial,sans-serif;font-size:15px;line-height:1.6;text-align:left;mso-line-height-alt:24px;">
|
|
1622
|
-
<p style="margin: 0; word-break: break-word;">${body}</p>
|
|
1623
|
-
</div>
|
|
1624
|
-
</td>
|
|
1625
|
-
</tr>
|
|
1626
|
-
</table>
|
|
1627
|
-
|
|
1628
|
-
${safeButtonUrl ? `
|
|
1629
|
-
<!-- Button -->
|
|
1630
|
-
<table class="button_block block-6" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
|
|
1631
|
-
<tr>
|
|
1632
|
-
<td class="pad" style="padding-left:10px;padding-right:10px;padding-bottom:10px;text-align:center;">
|
|
1633
|
-
<div class="alignment" align="center">
|
|
1634
|
-
<!--[if mso]>
|
|
1635
|
-
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${safeButtonUrl}" style="height:52px;width:auto;v-text-anchor:middle;" arcsize="12%" fillcolor="${buttonBgColor}">
|
|
1636
|
-
<v:stroke dashstyle="Solid" weight="0px" color="${buttonBgColor}"/>
|
|
1637
|
-
<w:anchorlock/>
|
|
1638
|
-
<v:textbox inset="0px,0px,0px,0px">
|
|
1639
|
-
<center dir="false" style="color:${buttonTextColor};font-family:'Montserrat',Arial,sans-serif;font-size:16px;font-weight:${buttonFontWeight};">
|
|
1640
|
-
<![endif]-->
|
|
1641
|
-
<a href="${safeButtonUrl}" style="background-color: ${buttonBgColor}; border: 0px solid transparent; border-radius: ${buttonRadius}; color: ${buttonTextColor}; display: inline-block; font-family: 'Montserrat', Arial, sans-serif; font-size: 16px; font-weight: ${buttonFontWeight}; mso-border-alt: none; padding: 10px 20px; text-align: center; text-decoration: none; text-transform: capitalize; word-break: keep-all;">
|
|
1642
|
-
${buttonText}
|
|
1643
|
-
</a>
|
|
1644
|
-
<!--[if mso]>
|
|
1645
|
-
</center>
|
|
1646
|
-
</v:textbox>
|
|
1647
|
-
</v:roundrect>
|
|
1648
|
-
<![endif]-->
|
|
1649
|
-
</div>
|
|
1650
|
-
</td>
|
|
1651
|
-
</tr>
|
|
1652
|
-
</table>
|
|
1653
|
-
|
|
1654
|
-
<!-- Copy Link -->
|
|
1655
|
-
<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;">
|
|
1656
|
-
<tr>
|
|
1657
|
-
<td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
|
|
1658
|
-
<div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:12px;line-height:1.5;text-align:center;mso-line-height-alt:18px;">
|
|
1659
|
-
<p style="margin: 0 0 5px 0; word-break: break-word;">or copy and paste this link into your browser:</p>
|
|
1660
|
-
<a href="${safeButtonUrl}" style="color:#4a90d9;text-decoration:underline;word-break:break-all;">${safeButtonUrl}</a>
|
|
1661
|
-
</div>
|
|
1662
|
-
</td>
|
|
1663
|
-
</tr>
|
|
1664
|
-
</table>
|
|
1665
|
-
` : ""}
|
|
1666
|
-
|
|
1667
|
-
${securityWarning ? `
|
|
1668
|
-
<!-- Security Warning -->
|
|
1669
|
-
<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;">
|
|
1670
|
-
<tr>
|
|
1671
|
-
<td class="pad" style="padding-left:40px;padding-right:40px;padding-bottom:20px;">
|
|
1672
|
-
<div style="background-color: #a1f1ff; border-radius: 8px; padding: 16px; display: flex; align-items: center; justify-content: center;">
|
|
1673
|
-
<p style="margin: 0; color: #01001b; font-family: 'Montserrat', Arial, sans-serif; font-size: 13px; line-height: 1.5; text-align: center;">
|
|
1674
|
-
${securityWarning}
|
|
1675
|
-
</p>
|
|
1676
|
-
</div>
|
|
1677
|
-
</td>
|
|
1678
|
-
</tr>
|
|
1679
|
-
</table>
|
|
1680
|
-
` : ""}
|
|
1681
|
-
|
|
1682
|
-
<!-- Footer Note -->
|
|
1683
|
-
<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;">
|
|
1684
|
-
<tr>
|
|
1685
|
-
<td class="pad" style="padding-bottom:30px;padding-left:40px;padding-right:40px;">
|
|
1686
|
-
<div style="color:#737373;font-family:'Montserrat',Arial,sans-serif;font-size:13px;line-height:1.5;text-align:center;mso-line-height-alt:20px;">
|
|
1687
|
-
<p style="margin: 0; word-break: break-word;">${footerNote}</p>
|
|
1688
|
-
</div>
|
|
1689
|
-
</td>
|
|
1690
|
-
</tr>
|
|
1691
|
-
</table>
|
|
1692
|
-
|
|
1693
|
-
</td>
|
|
1694
|
-
</tr>
|
|
1695
|
-
</tbody>
|
|
1696
|
-
</table>
|
|
1697
1643
|
</td>
|
|
1698
1644
|
</tr>
|
|
1699
1645
|
</tbody>
|
|
1700
1646
|
</table>
|
|
1701
1647
|
|
|
1702
|
-
<!--
|
|
1703
|
-
<table class="row
|
|
1648
|
+
<!-- Footer -->
|
|
1649
|
+
<table class="row-content" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: ${brand.footerColor}; border-radius: 12px; max-width: 640px; width: 100%; margin: 16px auto 0 auto;">
|
|
1704
1650
|
<tbody>
|
|
1705
1651
|
<tr>
|
|
1706
|
-
<td
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
<tr>
|
|
1710
|
-
<td class="column column-1" width="100%" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; font-weight: 400; text-align: left; padding-bottom: 5px; vertical-align: top;">
|
|
1711
|
-
<table class="empty_block block-1" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
|
|
1712
|
-
<tr>
|
|
1713
|
-
<td class="pad">
|
|
1714
|
-
<div></div>
|
|
1715
|
-
</td>
|
|
1716
|
-
</tr>
|
|
1717
|
-
</table>
|
|
1718
|
-
</td>
|
|
1719
|
-
</tr>
|
|
1720
|
-
</tbody>
|
|
1721
|
-
</table>
|
|
1722
|
-
</td>
|
|
1723
|
-
</tr>
|
|
1724
|
-
</tbody>
|
|
1725
|
-
</table>
|
|
1726
|
-
|
|
1727
|
-
<!-- Footer Row -->
|
|
1728
|
-
<table class="row row-3" align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt;">
|
|
1729
|
-
<tbody>
|
|
1730
|
-
<tr>
|
|
1731
|
-
<td>
|
|
1732
|
-
<table class="row-content stack" align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: ${footerColor}; color: #000000; max-width: 640px; width: 100%; margin: 0 auto;">
|
|
1733
|
-
<tbody>
|
|
1734
|
-
<tr>
|
|
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;">
|
|
1736
|
-
|
|
1737
|
-
${safeMarketingUrl ? `
|
|
1738
|
-
<!-- Policy Links -->
|
|
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;">
|
|
1740
|
-
<tr>
|
|
1741
|
-
<td class="pad" style="padding-bottom:20px;padding-left:40px;padding-right:40px;padding-top:20px;">
|
|
1742
|
-
<div style="color:#ffffff;font-family:'Montserrat',Arial,sans-serif;font-size:13px;line-height:1.5;text-align:center;mso-line-height-alt:20px;">
|
|
1743
|
-
<p style="margin: 0; word-break: break-word;">
|
|
1744
|
-
<a href="${safeMarketingUrl}/privacy" style="color: #ffffff; text-decoration: underline;">Privacy Policy</a> | <a href="${safeMarketingUrl}/contact" style="color: #ffffff; text-decoration: underline;">Get in Touch</a>
|
|
1745
|
-
</p>
|
|
1746
|
-
</div>
|
|
1747
|
-
</td>
|
|
1748
|
-
</tr>
|
|
1749
|
-
</table>
|
|
1750
|
-
` : ""}
|
|
1751
|
-
|
|
1752
|
-
<!-- Copyright & Disclaimer -->
|
|
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;">
|
|
1754
|
-
<tr>
|
|
1755
|
-
<td class="pad" style="padding-bottom:15px;padding-left:40px;padding-right:40px;">
|
|
1756
|
-
<div style="color:#a3a3a3;font-family:'Montserrat',Arial,sans-serif;font-size:12px;line-height:1.5;text-align:center;mso-line-height-alt:18px;">
|
|
1757
|
-
<p style="margin: 0 0 12px 0; word-break: break-word;">\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${safeAppName}. All rights reserved.</p>
|
|
1758
|
-
<p style="margin: 0; font-size: 11px; line-height: 1.5; word-break: break-word;">
|
|
1759
|
-
Please do not reply to this email as this address does not accept incoming messages. This service email contains essential information relating to your ${safeAppName} account.
|
|
1760
|
-
</p>
|
|
1761
|
-
</div>
|
|
1762
|
-
</td>
|
|
1763
|
-
</tr>
|
|
1764
|
-
</table>
|
|
1765
|
-
|
|
1766
|
-
</td>
|
|
1767
|
-
</tr>
|
|
1768
|
-
</tbody>
|
|
1769
|
-
</table>
|
|
1652
|
+
<td class="pad" style="padding: 24px 40px; color:#a3a3a3; font-family:${FONT}; font-size:12px; line-height:1.5; text-align:center; mso-line-height-alt:18px;">${links}
|
|
1653
|
+
<p style="margin: 0 0 8px 0; word-break: break-word;">\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${parts.appName}. All rights reserved.</p>
|
|
1654
|
+
<p style="margin: 0; font-size: 11px; word-break: break-word;">Please do not reply to this email as this address does not accept incoming messages. This service email contains essential information relating to your ${parts.appName} account.</p>
|
|
1770
1655
|
</td>
|
|
1771
1656
|
</tr>
|
|
1772
1657
|
</tbody>
|
|
1773
1658
|
</table>
|
|
1774
|
-
|
|
1775
1659
|
</td>
|
|
1776
1660
|
</tr>
|
|
1777
1661
|
</tbody>
|
|
@@ -1780,6 +1664,55 @@ function generateEmailTemplate(options) {
|
|
|
1780
1664
|
</html>
|
|
1781
1665
|
`;
|
|
1782
1666
|
}
|
|
1667
|
+
function generateEmailTemplate(options) {
|
|
1668
|
+
const { appUrl, buttonUrl } = options;
|
|
1669
|
+
const safeButtonUrl = buttonUrl ? escapeHtml(sanitizeEmailUrl(buttonUrl, appUrl)) : "";
|
|
1670
|
+
return renderLayout({
|
|
1671
|
+
title: options.heading,
|
|
1672
|
+
logoUrl: logoUrlFor(options.logoUrl, appUrl),
|
|
1673
|
+
logoSize: logoDimensions(options.logoWidth, options.logoHeight),
|
|
1674
|
+
appName: escapeHtml(options.appName ?? "App"),
|
|
1675
|
+
welcome: options.welcomeMessage ?? "",
|
|
1676
|
+
heading: options.heading,
|
|
1677
|
+
body: options.body,
|
|
1678
|
+
button: safeButtonUrl ? { label: options.buttonText, url: safeButtonUrl } : null,
|
|
1679
|
+
notice: options.securityWarning?.trim() ?? "",
|
|
1680
|
+
footerNote: options.footerNote,
|
|
1681
|
+
marketingUrl: marketingUrlFor(options.marketingUrl),
|
|
1682
|
+
brand: brandColours(options)
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
function renderEmail(content) {
|
|
1686
|
+
const { appUrl, branding = {} } = content;
|
|
1687
|
+
const link = (url) => escapeHtml(sanitizeEmailUrl(url, appUrl));
|
|
1688
|
+
const text = (value) => escapeHtml(value).replace(/\r?\n/g, "<br>");
|
|
1689
|
+
const brand = brandColours(branding);
|
|
1690
|
+
const paragraphs = content.paragraphs.map(
|
|
1691
|
+
(p) => `<p style="margin: 0 0 16px 0; word-break: break-word;">${text(p)}</p>`
|
|
1692
|
+
);
|
|
1693
|
+
const sections = (content.sections ?? []).map((section) => {
|
|
1694
|
+
const title = section.url ? `<a href="${link(section.url)}" style="color: ${brand.headingColor}; font-weight: 600; text-decoration: none;">${text(section.title)}</a>` : `<span style="color: ${brand.headingColor}; font-weight: 600;">${text(section.title)}</span>`;
|
|
1695
|
+
const rows = section.rows.map(
|
|
1696
|
+
(row) => `<li style="margin: 0 0 4px 0;">${row.url ? `<a href="${link(row.url)}" style="color: ${BODY_TEXT}; text-decoration: underline;">${text(row.text)}</a>` : text(row.text)}</li>`
|
|
1697
|
+
);
|
|
1698
|
+
return `<p style="margin: 8px 0 4px 0; word-break: break-word;">${title}</p>
|
|
1699
|
+
<ul style="margin: 0 0 16px 0; padding-left: 20px;">${rows.join("")}</ul>`;
|
|
1700
|
+
});
|
|
1701
|
+
return renderLayout({
|
|
1702
|
+
title: escapeHtml(content.heading),
|
|
1703
|
+
logoUrl: logoUrlFor(branding.logoUrl, appUrl),
|
|
1704
|
+
logoSize: logoDimensions(branding.logoWidth, branding.logoHeight),
|
|
1705
|
+
appName: escapeHtml(content.appName ?? "App"),
|
|
1706
|
+
welcome: "",
|
|
1707
|
+
heading: escapeHtml(content.heading),
|
|
1708
|
+
body: [...paragraphs, ...sections].join("\n "),
|
|
1709
|
+
button: content.button ? { label: escapeHtml(content.button.label), url: link(content.button.url) } : null,
|
|
1710
|
+
notice: content.notice ? text(content.notice) : "",
|
|
1711
|
+
footerNote: content.footerNote ? text(content.footerNote) : "",
|
|
1712
|
+
marketingUrl: marketingUrlFor(branding.marketingUrl),
|
|
1713
|
+
brand
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1783
1716
|
|
|
1784
1717
|
// src/core/passwordless.ts
|
|
1785
1718
|
var PASSWORDLESS_TTL_MS = 10 * 60 * 1e3;
|
|
@@ -6461,6 +6394,7 @@ function verifyWebhookSignature(payload, headers, secret) {
|
|
|
6461
6394
|
rateLimit,
|
|
6462
6395
|
recentPasswordlessFailures,
|
|
6463
6396
|
refreshSession,
|
|
6397
|
+
renderEmail,
|
|
6464
6398
|
requestEmailChange,
|
|
6465
6399
|
requireAuth,
|
|
6466
6400
|
requireVerifiedEmail,
|