@company-semantics/contracts 28.0.0 → 29.0.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/package.json +1 -1
- package/src/email/render/__tests__/__snapshots__/render-snapshot.test.ts.snap +92 -17
- package/src/email/render/auth-otp.ts +1 -7
- package/src/email/render/blocks.ts +28 -16
- package/src/email/render/chat-shared.ts +1 -7
- package/src/email/render/company-md-access-approved.ts +1 -7
- package/src/email/render/company-md-access-requested.ts +1 -7
- package/src/email/render/org-invite.ts +1 -6
- package/src/email/render/ownership-transfer.ts +1 -7
- package/src/email/render/render-email.ts +5 -33
- package/src/email/render/share-granted.ts +1 -7
- package/src/email/render/unit-owner-granted.ts +1 -7
- package/src/permissions/access-source.ts +6 -0
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ exports[`email render > auth.otp · Default 1`] = `
|
|
|
8
8
|
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
9
9
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Copy/paste code in login form:</p>
|
|
10
10
|
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; border-radius: 0; margin: 0 0 20px 0;">
|
|
11
|
-
<tr><td style="padding: 20px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size:
|
|
11
|
+
<tr><td style="padding: 20px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px; letter-spacing: 4px;">
|
|
12
12
|
<b>>> 123456 <<</b>
|
|
13
13
|
</td></tr>
|
|
14
14
|
</table>
|
|
@@ -49,7 +49,7 @@ exports[`email render > auth.otp · Short expiry 1`] = `
|
|
|
49
49
|
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
50
50
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Copy/paste code in login form:</p>
|
|
51
51
|
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; border-radius: 0; margin: 0 0 20px 0;">
|
|
52
|
-
<tr><td style="padding: 20px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size:
|
|
52
|
+
<tr><td style="padding: 20px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px; letter-spacing: 4px;">
|
|
53
53
|
<b>>> 902413 <<</b>
|
|
54
54
|
</td></tr>
|
|
55
55
|
</table>
|
|
@@ -90,7 +90,7 @@ exports[`email render > auth.otp · With request metadata 1`] = `
|
|
|
90
90
|
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
91
91
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Copy/paste code in login form:</p>
|
|
92
92
|
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; border-radius: 0; margin: 0 0 20px 0;">
|
|
93
|
-
<tr><td style="padding: 20px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size:
|
|
93
|
+
<tr><td style="padding: 20px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px; letter-spacing: 4px;">
|
|
94
94
|
<b>>> 246810 <<</b>
|
|
95
95
|
</td></tr>
|
|
96
96
|
</table>
|
|
@@ -150,8 +150,8 @@ exports[`email render > chat.shared · Default 1`] = `
|
|
|
150
150
|
</tr>
|
|
151
151
|
</table>
|
|
152
152
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; margin: 0 0 16px 0; max-width: 320px; text-align: center;">⋮</p>
|
|
153
|
-
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width:
|
|
154
|
-
<tr><td style="padding:
|
|
153
|
+
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width: 220px;">
|
|
154
|
+
<tr><td style="padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px;">
|
|
155
155
|
<a href="https://app.companysemantics.ai/share/abc123" style="color: #0047FF; text-decoration: underline;"><b>>> SEE MORE <<</b></a>
|
|
156
156
|
</td></tr>
|
|
157
157
|
</table>
|
|
@@ -211,8 +211,8 @@ exports[`email render > chat.shared · Long preview 1`] = `
|
|
|
211
211
|
</tr>
|
|
212
212
|
</table>
|
|
213
213
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; margin: 0 0 16px 0; max-width: 320px; text-align: center;">⋮</p>
|
|
214
|
-
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width:
|
|
215
|
-
<tr><td style="padding:
|
|
214
|
+
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width: 220px;">
|
|
215
|
+
<tr><td style="padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px;">
|
|
216
216
|
<a href="https://app.companysemantics.ai/share/preview789" style="color: #0047FF; text-decoration: underline;"><b>>> SEE MORE <<</b></a>
|
|
217
217
|
</td></tr>
|
|
218
218
|
</table>
|
|
@@ -272,8 +272,8 @@ exports[`email render > chat.shared · Long title 1`] = `
|
|
|
272
272
|
</tr>
|
|
273
273
|
</table>
|
|
274
274
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; margin: 0 0 16px 0; max-width: 320px; text-align: center;">⋮</p>
|
|
275
|
-
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width:
|
|
276
|
-
<tr><td style="padding:
|
|
275
|
+
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width: 220px;">
|
|
276
|
+
<tr><td style="padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px;">
|
|
277
277
|
<a href="https://app.companysemantics.ai/share/long456" style="color: #0047FF; text-decoration: underline;"><b>>> SEE MORE <<</b></a>
|
|
278
278
|
</td></tr>
|
|
279
279
|
</table>
|
|
@@ -327,8 +327,8 @@ exports[`email render > chat.shared · No preview 1`] = `
|
|
|
327
327
|
</tr>
|
|
328
328
|
</table>
|
|
329
329
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; margin: 0 0 16px 0; max-width: 320px; text-align: center;">⋮</p>
|
|
330
|
-
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width:
|
|
331
|
-
<tr><td style="padding:
|
|
330
|
+
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width: 220px;">
|
|
331
|
+
<tr><td style="padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px;">
|
|
332
332
|
<a href="https://app.companysemantics.ai/share/xyz789" style="color: #0047FF; text-decoration: underline;"><b>>> SEE MORE <<</b></a>
|
|
333
333
|
</td></tr>
|
|
334
334
|
</table>
|
|
@@ -382,8 +382,8 @@ exports[`email render > chat.shared · Short 1`] = `
|
|
|
382
382
|
</tr>
|
|
383
383
|
</table>
|
|
384
384
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 20px; margin: 0 0 16px 0; max-width: 320px; text-align: center;">⋮</p>
|
|
385
|
-
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width:
|
|
386
|
-
<tr><td style="padding:
|
|
385
|
+
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width: 220px;">
|
|
386
|
+
<tr><td style="padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px;">
|
|
387
387
|
<a href="https://app.companysemantics.ai/share/short123" style="color: #0047FF; text-decoration: underline;"><b>>> SEE MORE <<</b></a>
|
|
388
388
|
</td></tr>
|
|
389
389
|
</table>
|
|
@@ -662,8 +662,8 @@ exports[`email render > org.invite · Admin 1`] = `
|
|
|
662
662
|
<head><meta charset="UTF-8"></head>
|
|
663
663
|
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
664
664
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Workspace invitation.</p>
|
|
665
|
-
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0;">
|
|
666
|
-
<tr><td style="padding:
|
|
665
|
+
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width: 220px;">
|
|
666
|
+
<tr><td style="padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px;">
|
|
667
667
|
<a href="https://app.companysemantics.ai/invite/abc123" style="color: #0047FF; text-decoration: underline;"><b>>> JOIN <<</b></a>
|
|
668
668
|
</td></tr>
|
|
669
669
|
</table>
|
|
@@ -706,8 +706,8 @@ exports[`email render > org.invite · Member 1`] = `
|
|
|
706
706
|
<head><meta charset="UTF-8"></head>
|
|
707
707
|
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
708
708
|
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Workspace invitation.</p>
|
|
709
|
-
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0;">
|
|
710
|
-
<tr><td style="padding:
|
|
709
|
+
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width: 220px;">
|
|
710
|
+
<tr><td style="padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px;">
|
|
711
711
|
<a href="https://app.companysemantics.ai/invite/def456" style="color: #0047FF; text-decoration: underline;"><b>>> JOIN <<</b></a>
|
|
712
712
|
</td></tr>
|
|
713
713
|
</table>
|
|
@@ -745,6 +745,21 @@ Questions? Contact support@companysemantics.ai
|
|
|
745
745
|
|
|
746
746
|
exports[`email render > org.ownership_transfer · No note 1`] = `
|
|
747
747
|
{
|
|
748
|
+
"html": "<!DOCTYPE html>
|
|
749
|
+
<html lang="en">
|
|
750
|
+
<head><meta charset="UTF-8"></head>
|
|
751
|
+
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
752
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">You have been invited to become the owner of <b>"Acme Corp"</b>.</p>
|
|
753
|
+
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width: 220px;">
|
|
754
|
+
<tr><td style="padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px;">
|
|
755
|
+
<a href="https://app.companysemantics.ai/owner-transfer/accept/tok456" style="color: #0047FF; text-decoration: underline;"><b>>> ACCEPT <<</b></a>
|
|
756
|
+
</td></tr>
|
|
757
|
+
</table>
|
|
758
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Expires in: <b>14 days</b></p>
|
|
759
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">If you did not expect this invitation, you can safely ignore this email.</p>
|
|
760
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0;">Company Semantics<br><span style="color: #888;">Questions? Contact <a href="mailto:support@companysemantics.ai" style="color: #888;">support@companysemantics.ai</a></span></p>
|
|
761
|
+
</body>
|
|
762
|
+
</html>",
|
|
748
763
|
"subject": "You've been invited to become a workspace owner",
|
|
749
764
|
"text": "You have been invited to become the owner of "Acme Corp".
|
|
750
765
|
|
|
@@ -767,6 +782,31 @@ Questions? Contact support@companysemantics.ai
|
|
|
767
782
|
|
|
768
783
|
exports[`email render > org.ownership_transfer · With note 1`] = `
|
|
769
784
|
{
|
|
785
|
+
"html": "<!DOCTYPE html>
|
|
786
|
+
<html lang="en">
|
|
787
|
+
<head><meta charset="UTF-8"></head>
|
|
788
|
+
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
789
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">You have been invited to become the owner of <b>"Acme Corp"</b>.</p>
|
|
790
|
+
<table cellpadding="0" cellspacing="0" border="0" style="margin: 0 0 16px 0;">
|
|
791
|
+
<tr>
|
|
792
|
+
<td style="border: 1px solid #1a1a1a; border-radius: 12px 12px 0 12px; padding: 10px 14px; min-width: 280px; text-align: right; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; background: #f5f5f5;">Handing this over as I move to an advisory role.</td>
|
|
793
|
+
<td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; padding-left: 8px; vertical-align: bottom;">(•̀_ರ╮)</td>
|
|
794
|
+
</tr>
|
|
795
|
+
<tr>
|
|
796
|
+
<td style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 11px; color: #888; text-align: right; padding-top: 4px;">Message from the current owner</td>
|
|
797
|
+
<td></td>
|
|
798
|
+
</tr>
|
|
799
|
+
</table>
|
|
800
|
+
<table cellpadding="0" cellspacing="0" border="0" style="border: 2px solid #1a1a1a; margin: 0 0 20px 0; max-width: 220px;">
|
|
801
|
+
<tr><td style="padding: 16px 24px; text-align: center; font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 16px;">
|
|
802
|
+
<a href="https://app.companysemantics.ai/owner-transfer/accept/tok123" style="color: #0047FF; text-decoration: underline;"><b>>> ACCEPT <<</b></a>
|
|
803
|
+
</td></tr>
|
|
804
|
+
</table>
|
|
805
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Expires in: <b>14 days</b></p>
|
|
806
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">If you did not expect this invitation, you can safely ignore this email.</p>
|
|
807
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0;">Company Semantics<br><span style="color: #888;">Questions? Contact <a href="mailto:support@companysemantics.ai" style="color: #888;">support@companysemantics.ai</a></span></p>
|
|
808
|
+
</body>
|
|
809
|
+
</html>",
|
|
770
810
|
"subject": "You've been invited to become a workspace owner",
|
|
771
811
|
"text": "You have been invited to become the owner of "Acme Corp".
|
|
772
812
|
|
|
@@ -795,6 +835,16 @@ Questions? Contact support@companysemantics.ai
|
|
|
795
835
|
|
|
796
836
|
exports[`email render > org.ownership_transfer_completed · Default 1`] = `
|
|
797
837
|
{
|
|
838
|
+
"html": "<!DOCTYPE html>
|
|
839
|
+
<html lang="en">
|
|
840
|
+
<head><meta charset="UTF-8"></head>
|
|
841
|
+
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
842
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">The ownership of <b>"Acme Corp"</b> has been transferred to <b>new.owner@acme.com</b>.</p>
|
|
843
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">You are no longer the owner of this workspace, but you remain an admin.</p>
|
|
844
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">If you did not authorize this transfer, please contact support immediately.</p>
|
|
845
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0;">Company Semantics<br><span style="color: #888;">Questions? Contact <a href="mailto:support@companysemantics.ai" style="color: #888;">support@companysemantics.ai</a></span></p>
|
|
846
|
+
</body>
|
|
847
|
+
</html>",
|
|
798
848
|
"subject": "Workspace ownership has been transferred",
|
|
799
849
|
"text": "The ownership of "Acme Corp" has been transferred to new.owner@acme.com.
|
|
800
850
|
|
|
@@ -903,6 +953,19 @@ Questions? Contact support@companysemantics.ai
|
|
|
903
953
|
|
|
904
954
|
exports[`email render > security.alert · Excessive OTP requests 1`] = `
|
|
905
955
|
{
|
|
956
|
+
"html": "<!DOCTYPE html>
|
|
957
|
+
<html lang="en">
|
|
958
|
+
<head><meta charset="UTF-8"></head>
|
|
959
|
+
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
960
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">We detected an unusual number of login code requests for your account.</p>
|
|
961
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Details: <b>12 login codes requested in 5 minutes</b></p>
|
|
962
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 4px 0;">If this was you, no action is needed.</p>
|
|
963
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 4px 0;">If you didn't request these codes, someone may be trying to access your account.</p>
|
|
964
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">We recommend reviewing your account security.</p>
|
|
965
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Time: <b>Sun, 12 Jul 2026 14:30:00 GMT</b></p>
|
|
966
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0;">Company Semantics Security<br><span style="color: #888;">Questions? Contact <a href="mailto:support@companysemantics.ai" style="color: #888;">support@companysemantics.ai</a></span></p>
|
|
967
|
+
</body>
|
|
968
|
+
</html>",
|
|
906
969
|
"subject": "Security alert for your account",
|
|
907
970
|
"text": "We detected an unusual number of login code requests for your account.
|
|
908
971
|
|
|
@@ -923,6 +986,18 @@ Questions? Contact support@companysemantics.ai
|
|
|
923
986
|
|
|
924
987
|
exports[`email render > security.alert · Unusual login location 1`] = `
|
|
925
988
|
{
|
|
989
|
+
"html": "<!DOCTYPE html>
|
|
990
|
+
<html lang="en">
|
|
991
|
+
<head><meta charset="UTF-8"></head>
|
|
992
|
+
<body style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; color: #1a1a1a; margin: 0; padding: 0;">
|
|
993
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">We detected a login attempt from an unusual location.</p>
|
|
994
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Details: <b>Login from Berlin, DE</b></p>
|
|
995
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 4px 0;">If this was you, no action is needed.</p>
|
|
996
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">If you didn't attempt to log in, please secure your account immediately.</p>
|
|
997
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0 0 20px 0;">Time: <b>Sun, 12 Jul 2026 14:30:00 GMT</b></p>
|
|
998
|
+
<p style="font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; margin: 0;">Company Semantics Security<br><span style="color: #888;">Questions? Contact <a href="mailto:support@companysemantics.ai" style="color: #888;">support@companysemantics.ai</a></span></p>
|
|
999
|
+
</body>
|
|
1000
|
+
</html>",
|
|
926
1001
|
"subject": "Security alert for your account",
|
|
927
1002
|
"text": "We detected a login attempt from an unusual location.
|
|
928
1003
|
|
|
@@ -33,13 +33,7 @@ export function renderAuthOtp(
|
|
|
33
33
|
|
|
34
34
|
const blocks: Block[] = [
|
|
35
35
|
paragraph("Copy/paste code in login form:"),
|
|
36
|
-
ctaBox({
|
|
37
|
-
label: otp,
|
|
38
|
-
padding: "20px 24px",
|
|
39
|
-
fontSize: "20px",
|
|
40
|
-
borderRadiusZero: true,
|
|
41
|
-
letterSpacing: "4px",
|
|
42
|
-
}),
|
|
36
|
+
ctaBox({ label: otp, variant: "code" }),
|
|
43
37
|
keyValue("Status", "VALID"),
|
|
44
38
|
keyValue(
|
|
45
39
|
"Expires in",
|
|
@@ -190,17 +190,35 @@ export interface CtaBoxOptions {
|
|
|
190
190
|
/** When present, the label links to this URL (and the URL rides under the
|
|
191
191
|
* plain-text box). */
|
|
192
192
|
href?: string;
|
|
193
|
-
/**
|
|
193
|
+
/** Visual kind. `"action"` = link CTA (default); `"code"` = copy/paste code
|
|
194
|
+
* box (square corners, letter-spaced). */
|
|
195
|
+
variant?: "action" | "code";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Component-owned styling per CTA variant. Templates supply only `label`/`href`/
|
|
199
|
+
* `variant` — the only allowable UI is the component, so all CSS lives here,
|
|
200
|
+
* never at the call site. */
|
|
201
|
+
interface CtaVariantStyle {
|
|
194
202
|
padding: string;
|
|
195
|
-
/** Label font-size, e.g. `"16px"`. */
|
|
196
203
|
fontSize: string;
|
|
197
|
-
/** Optional table `max-width`, e.g. `"220px"`. */
|
|
198
204
|
maxWidth?: string;
|
|
199
|
-
|
|
200
|
-
borderRadiusZero?: boolean;
|
|
201
|
-
/** Optional `letter-spacing`, e.g. `"4px"` (OTP code box). */
|
|
205
|
+
square: boolean;
|
|
202
206
|
letterSpacing?: string;
|
|
203
207
|
}
|
|
208
|
+
const CTA_VARIANTS: Record<"action" | "code", CtaVariantStyle> = {
|
|
209
|
+
action: {
|
|
210
|
+
padding: "16px 24px",
|
|
211
|
+
fontSize: "16px",
|
|
212
|
+
maxWidth: "220px",
|
|
213
|
+
square: false,
|
|
214
|
+
},
|
|
215
|
+
code: {
|
|
216
|
+
padding: "20px 24px",
|
|
217
|
+
fontSize: "16px",
|
|
218
|
+
square: true,
|
|
219
|
+
letterSpacing: "4px",
|
|
220
|
+
},
|
|
221
|
+
};
|
|
204
222
|
|
|
205
223
|
/** Padding columns on each side of the `>> LABEL <<` line in the ASCII box. */
|
|
206
224
|
const CTA_BOX_PAD = 3;
|
|
@@ -216,19 +234,13 @@ function asciiCtaBox(label: string): string {
|
|
|
216
234
|
/** The bordered `>> LABEL <<` CTA box (OTP / JOIN / OPEN / VIEW TEAM / …). When
|
|
217
235
|
* `href` is given, HTML links the label and plain text prints the URL below. */
|
|
218
236
|
export function ctaBox(opts: CtaBoxOptions): Block {
|
|
219
|
-
const {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
padding,
|
|
223
|
-
fontSize,
|
|
224
|
-
maxWidth,
|
|
225
|
-
borderRadiusZero,
|
|
226
|
-
letterSpacing,
|
|
227
|
-
} = opts;
|
|
237
|
+
const { label, href, variant = "action" } = opts;
|
|
238
|
+
const { padding, fontSize, maxWidth, square, letterSpacing } =
|
|
239
|
+
CTA_VARIANTS[variant];
|
|
228
240
|
|
|
229
241
|
const tableStyle =
|
|
230
242
|
`border: 2px solid #1a1a1a;` +
|
|
231
|
-
(
|
|
243
|
+
(square ? ` border-radius: 0;` : ``) +
|
|
232
244
|
` margin: 0 0 20px 0;` +
|
|
233
245
|
(maxWidth ? ` max-width: ${maxWidth};` : ``);
|
|
234
246
|
|
|
@@ -31,13 +31,7 @@ export function renderChatShared(payload: ChatSharedPayload): Block[] {
|
|
|
31
31
|
|
|
32
32
|
blocks.push(
|
|
33
33
|
chatDots(),
|
|
34
|
-
ctaBox({
|
|
35
|
-
label: "SEE MORE",
|
|
36
|
-
href: shareUrl,
|
|
37
|
-
padding: "12px 20px",
|
|
38
|
-
fontSize: "14px",
|
|
39
|
-
maxWidth: "200px",
|
|
40
|
-
}),
|
|
34
|
+
ctaBox({ label: "SEE MORE", href: shareUrl }),
|
|
41
35
|
footer(`This share was sent via ${COMPANY_NAME}.`),
|
|
42
36
|
paragraph(NOTICE, "tight"),
|
|
43
37
|
signature(),
|
|
@@ -30,13 +30,7 @@ export function renderAccessApproved(payload: AccessApprovedPayload): Block[] {
|
|
|
30
30
|
bold(`"${docTitle}"`),
|
|
31
31
|
` — you ${ACCESS_PHRASE[accessLevel]}.`,
|
|
32
32
|
]),
|
|
33
|
-
ctaBox({
|
|
34
|
-
label: "OPEN",
|
|
35
|
-
href: docUrl,
|
|
36
|
-
padding: "16px 24px",
|
|
37
|
-
fontSize: "16px",
|
|
38
|
-
maxWidth: "220px",
|
|
39
|
-
}),
|
|
33
|
+
ctaBox({ label: "OPEN", href: docUrl }),
|
|
40
34
|
footer(`This notification was sent via ${COMPANY_NAME}.`),
|
|
41
35
|
signature(),
|
|
42
36
|
];
|
|
@@ -39,13 +39,7 @@ export function renderAccessRequested(
|
|
|
39
39
|
if (message) blocks.push(chatUser(message, `Message from ${requesterName}`));
|
|
40
40
|
|
|
41
41
|
blocks.push(
|
|
42
|
-
ctaBox({
|
|
43
|
-
label: "REVIEW",
|
|
44
|
-
href: reviewUrl,
|
|
45
|
-
padding: "16px 24px",
|
|
46
|
-
fontSize: "16px",
|
|
47
|
-
maxWidth: "220px",
|
|
48
|
-
}),
|
|
42
|
+
ctaBox({ label: "REVIEW", href: reviewUrl }),
|
|
49
43
|
footer(`This notification was sent via ${COMPANY_NAME}.`, OWNER_NOTE),
|
|
50
44
|
signature(),
|
|
51
45
|
);
|
|
@@ -22,12 +22,7 @@ export function renderOrgInvite(payload: OrgInvitePayload): Block[] {
|
|
|
22
22
|
|
|
23
23
|
return [
|
|
24
24
|
paragraph("Workspace invitation."),
|
|
25
|
-
ctaBox({
|
|
26
|
-
label: "JOIN",
|
|
27
|
-
href: acceptUrl,
|
|
28
|
-
padding: "20px 24px",
|
|
29
|
-
fontSize: "16px",
|
|
30
|
-
}),
|
|
25
|
+
ctaBox({ label: "JOIN", href: acceptUrl }),
|
|
31
26
|
keyValue("From", inviterName),
|
|
32
27
|
keyValue("Workspace", orgName),
|
|
33
28
|
keyValue("Role", role),
|
|
@@ -33,13 +33,7 @@ export function renderOwnershipTransfer(
|
|
|
33
33
|
if (note) blocks.push(chatUser(note, "Message from the current owner"));
|
|
34
34
|
|
|
35
35
|
blocks.push(
|
|
36
|
-
ctaBox({
|
|
37
|
-
label: "ACCEPT",
|
|
38
|
-
href: acceptUrl,
|
|
39
|
-
padding: "16px 24px",
|
|
40
|
-
fontSize: "16px",
|
|
41
|
-
maxWidth: "220px",
|
|
42
|
-
}),
|
|
36
|
+
ctaBox({ label: "ACCEPT", href: acceptUrl }),
|
|
43
37
|
keyValue("Expires in", `${expiresInDays} days`, "normal"),
|
|
44
38
|
footer(
|
|
45
39
|
"If you did not expect this invitation, you can safely ignore this email.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Central email dispatcher. Given a kind + payload it returns
|
|
3
|
-
* `{ subject, text, html
|
|
3
|
+
* `{ subject, text, html }`, pulling the subject from the registry (single
|
|
4
4
|
* source of truth) and deriving both surfaces from ONE composed block list
|
|
5
5
|
* (`htmlShell` + `textShell`). This is the one entry point both the backend
|
|
6
6
|
* (real sends) and the app (Ladle preview) call.
|
|
@@ -28,8 +28,8 @@ export interface RenderedEmail {
|
|
|
28
28
|
subject: string;
|
|
29
29
|
/** Plain-text body. */
|
|
30
30
|
text: string;
|
|
31
|
-
/** HTML body (
|
|
32
|
-
html
|
|
31
|
+
/** HTML body (every kind is dual-output). */
|
|
32
|
+
html: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Caller-supplied render options. */
|
|
@@ -55,30 +55,13 @@ export const IMPLEMENTED_EMAIL_KINDS = [
|
|
|
55
55
|
|
|
56
56
|
export type ImplementedEmailKind = (typeof IMPLEMENTED_EMAIL_KINDS)[number];
|
|
57
57
|
|
|
58
|
-
/**
|
|
59
|
-
* Kinds delivered as plain text only. The unified block model renders HTML for
|
|
60
|
-
* every template, but these keep their historical text-only surface — the
|
|
61
|
-
* dispatcher omits `html` for them. (Enabling HTML later is deleting the entry.)
|
|
62
|
-
*/
|
|
63
|
-
const TEXT_ONLY_KINDS = new Set<EmailKind>([
|
|
64
|
-
"security.alert",
|
|
65
|
-
"org.ownership_transfer",
|
|
66
|
-
"org.ownership_transfer_completed",
|
|
67
|
-
]);
|
|
68
|
-
|
|
69
58
|
/** Payload type for a kind (or `never` for kinds without a payload). */
|
|
70
59
|
type PayloadFor<K extends EmailKind> = K extends keyof EmailPayloads
|
|
71
60
|
? EmailPayloads[K]
|
|
72
61
|
: never;
|
|
73
62
|
|
|
74
|
-
function toEmail(
|
|
75
|
-
subject:
|
|
76
|
-
kind: EmailKind,
|
|
77
|
-
blocks: Block[],
|
|
78
|
-
): RenderedEmail {
|
|
79
|
-
const email: RenderedEmail = { subject, text: textShell(blocks) };
|
|
80
|
-
if (!TEXT_ONLY_KINDS.has(kind)) email.html = htmlShell(blocks);
|
|
81
|
-
return email;
|
|
63
|
+
function toEmail(subject: string, blocks: Block[]): RenderedEmail {
|
|
64
|
+
return { subject, text: textShell(blocks), html: htmlShell(blocks) };
|
|
82
65
|
}
|
|
83
66
|
|
|
84
67
|
/**
|
|
@@ -100,19 +83,16 @@ export function renderEmail<K extends EmailKind>(
|
|
|
100
83
|
case "auth.otp":
|
|
101
84
|
return toEmail(
|
|
102
85
|
subject,
|
|
103
|
-
kind,
|
|
104
86
|
renderAuthOtp(payload as EmailPayloads["auth.otp"], options),
|
|
105
87
|
);
|
|
106
88
|
case "org.invite":
|
|
107
89
|
return toEmail(
|
|
108
90
|
subject,
|
|
109
|
-
kind,
|
|
110
91
|
renderOrgInvite(payload as EmailPayloads["org.invite"]),
|
|
111
92
|
);
|
|
112
93
|
case "org.unit_owner_granted":
|
|
113
94
|
return toEmail(
|
|
114
95
|
subject,
|
|
115
|
-
kind,
|
|
116
96
|
renderUnitOwnerGranted(
|
|
117
97
|
payload as EmailPayloads["org.unit_owner_granted"],
|
|
118
98
|
),
|
|
@@ -120,7 +100,6 @@ export function renderEmail<K extends EmailKind>(
|
|
|
120
100
|
case "org.ownership_transfer":
|
|
121
101
|
return toEmail(
|
|
122
102
|
subject,
|
|
123
|
-
kind,
|
|
124
103
|
renderOwnershipTransfer(
|
|
125
104
|
payload as EmailPayloads["org.ownership_transfer"],
|
|
126
105
|
),
|
|
@@ -128,7 +107,6 @@ export function renderEmail<K extends EmailKind>(
|
|
|
128
107
|
case "org.ownership_transfer_completed":
|
|
129
108
|
return toEmail(
|
|
130
109
|
subject,
|
|
131
|
-
kind,
|
|
132
110
|
renderOwnershipTransferCompleted(
|
|
133
111
|
payload as EmailPayloads["org.ownership_transfer_completed"],
|
|
134
112
|
),
|
|
@@ -136,25 +114,21 @@ export function renderEmail<K extends EmailKind>(
|
|
|
136
114
|
case "security.alert":
|
|
137
115
|
return toEmail(
|
|
138
116
|
subject,
|
|
139
|
-
kind,
|
|
140
117
|
renderSecurityAlert(payload as EmailPayloads["security.alert"]),
|
|
141
118
|
);
|
|
142
119
|
case "chat.shared":
|
|
143
120
|
return toEmail(
|
|
144
121
|
subject,
|
|
145
|
-
kind,
|
|
146
122
|
renderChatShared(payload as EmailPayloads["chat.shared"]),
|
|
147
123
|
);
|
|
148
124
|
case "share.granted":
|
|
149
125
|
return toEmail(
|
|
150
126
|
subject,
|
|
151
|
-
kind,
|
|
152
127
|
renderShareGranted(payload as EmailPayloads["share.granted"]),
|
|
153
128
|
);
|
|
154
129
|
case "companyMd.access_requested":
|
|
155
130
|
return toEmail(
|
|
156
131
|
subject,
|
|
157
|
-
kind,
|
|
158
132
|
renderAccessRequested(
|
|
159
133
|
payload as EmailPayloads["companyMd.access_requested"],
|
|
160
134
|
),
|
|
@@ -162,7 +136,6 @@ export function renderEmail<K extends EmailKind>(
|
|
|
162
136
|
case "companyMd.access_request_approved":
|
|
163
137
|
return toEmail(
|
|
164
138
|
subject,
|
|
165
|
-
kind,
|
|
166
139
|
renderAccessApproved(
|
|
167
140
|
payload as EmailPayloads["companyMd.access_request_approved"],
|
|
168
141
|
),
|
|
@@ -170,7 +143,6 @@ export function renderEmail<K extends EmailKind>(
|
|
|
170
143
|
case "companyMd.access_request_denied":
|
|
171
144
|
return toEmail(
|
|
172
145
|
subject,
|
|
173
|
-
kind,
|
|
174
146
|
renderAccessDenied(
|
|
175
147
|
payload as EmailPayloads["companyMd.access_request_denied"],
|
|
176
148
|
),
|
|
@@ -49,13 +49,7 @@ export function renderShareGranted(payload: ShareGrantedPayload): Block[] {
|
|
|
49
49
|
if (message) blocks.push(chatUser(message, `Message from ${granterName}`));
|
|
50
50
|
|
|
51
51
|
blocks.push(
|
|
52
|
-
ctaBox({
|
|
53
|
-
label: "OPEN",
|
|
54
|
-
href: ctaUrl,
|
|
55
|
-
padding: "16px 24px",
|
|
56
|
-
fontSize: "16px",
|
|
57
|
-
maxWidth: "220px",
|
|
58
|
-
}),
|
|
52
|
+
ctaBox({ label: "OPEN", href: ctaUrl }),
|
|
59
53
|
footer(`This notification was sent via ${COMPANY_NAME}.`, NOTICE),
|
|
60
54
|
signature(),
|
|
61
55
|
);
|
|
@@ -40,13 +40,7 @@ export function renderUnitOwnerGranted(
|
|
|
40
40
|
if (message) blocks.push(chatUser(message, `Message from ${granterName}`));
|
|
41
41
|
|
|
42
42
|
blocks.push(
|
|
43
|
-
ctaBox({
|
|
44
|
-
label: "VIEW TEAM",
|
|
45
|
-
href: ctaUrl,
|
|
46
|
-
padding: "16px 24px",
|
|
47
|
-
fontSize: "16px",
|
|
48
|
-
maxWidth: "220px",
|
|
49
|
-
}),
|
|
43
|
+
ctaBox({ label: "VIEW TEAM", href: ctaUrl }),
|
|
50
44
|
footer(`This notification was sent via ${COMPANY_NAME}.`, NOTICE),
|
|
51
45
|
signature(),
|
|
52
46
|
);
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* - `ownership` — entity.owner_user_id column projection
|
|
13
13
|
* - `visibility` — entity.visibility tier compiled to viewer rows
|
|
14
14
|
* - `inheritance` — org-chart-membership-based unit walk
|
|
15
|
+
* - `authority` — org-admin / account-owner / unit-editor / sharing-policy
|
|
16
|
+
* bands as synthetic live-resolved principals (ADR-BE-185)
|
|
17
|
+
* - `context_association` — a company.md context-bank doc inherits viewer access
|
|
18
|
+
* from every associated parent (the governed union, ADR-BE-392)
|
|
15
19
|
* - `migration` — one-shot backfill from legacy systems (AUTH-004)
|
|
16
20
|
*/
|
|
17
21
|
import { z } from "zod";
|
|
@@ -21,6 +25,8 @@ export const ACCESS_SOURCES = [
|
|
|
21
25
|
"ownership",
|
|
22
26
|
"visibility",
|
|
23
27
|
"inheritance",
|
|
28
|
+
"authority",
|
|
29
|
+
"context_association",
|
|
24
30
|
"migration",
|
|
25
31
|
] as const;
|
|
26
32
|
export type AccessSource = (typeof ACCESS_SOURCES)[number];
|