@company-semantics/contracts 27.14.0 → 28.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/package.json +1 -1
- package/src/email/render/__tests__/__snapshots__/render-snapshot.test.ts.snap +209 -83
- package/src/email/render/auth-otp.ts +25 -43
- package/src/email/render/blocks.ts +183 -150
- package/src/email/render/chat-shared.ts +15 -42
- package/src/email/render/company-md-access-approved.ts +16 -40
- package/src/email/render/company-md-access-denied.ts +23 -39
- package/src/email/render/company-md-access-requested.ts +21 -41
- package/src/email/render/index.ts +12 -47
- package/src/email/render/org-invite.ts +9 -32
- package/src/email/render/ownership-transfer-completed.ts +19 -17
- package/src/email/render/ownership-transfer.ts +36 -21
- package/src/email/render/render-email.ts +85 -103
- package/src/email/render/security-alert.ts +27 -27
- package/src/email/render/share-granted.ts +25 -51
- package/src/email/render/unit-owner-granted.ts +24 -43
- package/src/permissions/access-source.ts +6 -0
- package/src/email/render/base-text.ts +0 -26
|
@@ -1,39 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Central email dispatcher. Given a kind + payload it returns
|
|
3
|
-
* `{ subject, text, html
|
|
4
|
-
* source of truth)
|
|
5
|
-
*
|
|
3
|
+
* `{ subject, text, html }`, pulling the subject from the registry (single
|
|
4
|
+
* source of truth) and deriving both surfaces from ONE composed block list
|
|
5
|
+
* (`htmlShell` + `textShell`). This is the one entry point both the backend
|
|
6
|
+
* (real sends) and the app (Ladle preview) call.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { EMAIL_KINDS } from "../registry";
|
|
9
10
|
import type { EmailKind, EmailPayloads } from "../types";
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "./company-md-access-
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "./
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from "./company-md-access-requested";
|
|
24
|
-
import { renderChatSharedBody, renderChatSharedHtml } from "./chat-shared";
|
|
25
|
-
import { renderOrgInviteBody, renderOrgInviteHtml } from "./org-invite";
|
|
26
|
-
import { renderOwnershipTransferBody } from "./ownership-transfer";
|
|
27
|
-
import { renderOwnershipTransferCompletedBody } from "./ownership-transfer-completed";
|
|
28
|
-
import { renderSecurityAlertBody } from "./security-alert";
|
|
29
|
-
import {
|
|
30
|
-
renderShareGrantedBody,
|
|
31
|
-
renderShareGrantedHtml,
|
|
32
|
-
} from "./share-granted";
|
|
33
|
-
import {
|
|
34
|
-
renderUnitOwnerGrantedBody,
|
|
35
|
-
renderUnitOwnerGrantedHtml,
|
|
36
|
-
} from "./unit-owner-granted";
|
|
12
|
+
import { renderAuthOtp } from "./auth-otp";
|
|
13
|
+
import { type Block, htmlShell, textShell } from "./blocks";
|
|
14
|
+
import { renderAccessApproved } from "./company-md-access-approved";
|
|
15
|
+
import { renderAccessDenied } from "./company-md-access-denied";
|
|
16
|
+
import { renderAccessRequested } from "./company-md-access-requested";
|
|
17
|
+
import { renderChatShared } from "./chat-shared";
|
|
18
|
+
import { renderOrgInvite } from "./org-invite";
|
|
19
|
+
import { renderOwnershipTransfer } from "./ownership-transfer";
|
|
20
|
+
import { renderOwnershipTransferCompleted } from "./ownership-transfer-completed";
|
|
21
|
+
import { renderSecurityAlert } from "./security-alert";
|
|
22
|
+
import { renderShareGranted } from "./share-granted";
|
|
23
|
+
import { renderUnitOwnerGranted } from "./unit-owner-granted";
|
|
37
24
|
|
|
38
25
|
/** Rendered email output. */
|
|
39
26
|
export interface RenderedEmail {
|
|
@@ -41,8 +28,8 @@ export interface RenderedEmail {
|
|
|
41
28
|
subject: string;
|
|
42
29
|
/** Plain-text body. */
|
|
43
30
|
text: string;
|
|
44
|
-
/** HTML body (
|
|
45
|
-
html
|
|
31
|
+
/** HTML body (every kind is dual-output). */
|
|
32
|
+
html: string;
|
|
46
33
|
}
|
|
47
34
|
|
|
48
35
|
/** Caller-supplied render options. */
|
|
@@ -73,11 +60,15 @@ type PayloadFor<K extends EmailKind> = K extends keyof EmailPayloads
|
|
|
73
60
|
? EmailPayloads[K]
|
|
74
61
|
: never;
|
|
75
62
|
|
|
63
|
+
function toEmail(subject: string, blocks: Block[]): RenderedEmail {
|
|
64
|
+
return { subject, text: textShell(blocks), html: htmlShell(blocks) };
|
|
65
|
+
}
|
|
66
|
+
|
|
76
67
|
/**
|
|
77
|
-
* Render an email by kind. Subject comes from `EMAIL_KINDS`;
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
68
|
+
* Render an email by kind. Subject comes from `EMAIL_KINDS`; both surfaces
|
|
69
|
+
* derive from one composed block list. Accepts the full `EmailKind` union so
|
|
70
|
+
* callers with a generic kind (e.g. the backend `EmailService`) type cleanly;
|
|
71
|
+
* unimplemented kinds throw at runtime.
|
|
81
72
|
*
|
|
82
73
|
* @throws if the kind has no implementation.
|
|
83
74
|
*/
|
|
@@ -89,82 +80,73 @@ export function renderEmail<K extends EmailKind>(
|
|
|
89
80
|
const subject = EMAIL_KINDS[kind].subject;
|
|
90
81
|
|
|
91
82
|
switch (kind) {
|
|
92
|
-
case "auth.otp":
|
|
93
|
-
|
|
94
|
-
return {
|
|
83
|
+
case "auth.otp":
|
|
84
|
+
return toEmail(
|
|
95
85
|
subject,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
case "org.invite": {
|
|
101
|
-
const p = payload as EmailPayloads["org.invite"];
|
|
102
|
-
return {
|
|
86
|
+
renderAuthOtp(payload as EmailPayloads["auth.otp"], options),
|
|
87
|
+
);
|
|
88
|
+
case "org.invite":
|
|
89
|
+
return toEmail(
|
|
103
90
|
subject,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
case "org.unit_owner_granted": {
|
|
109
|
-
const p = payload as EmailPayloads["org.unit_owner_granted"];
|
|
110
|
-
return {
|
|
91
|
+
renderOrgInvite(payload as EmailPayloads["org.invite"]),
|
|
92
|
+
);
|
|
93
|
+
case "org.unit_owner_granted":
|
|
94
|
+
return toEmail(
|
|
111
95
|
subject,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
case "org.ownership_transfer":
|
|
117
|
-
|
|
118
|
-
return { subject, text: renderOwnershipTransferBody(p) };
|
|
119
|
-
}
|
|
120
|
-
case "org.ownership_transfer_completed": {
|
|
121
|
-
const p = payload as EmailPayloads["org.ownership_transfer_completed"];
|
|
122
|
-
return { subject, text: renderOwnershipTransferCompletedBody(p) };
|
|
123
|
-
}
|
|
124
|
-
case "security.alert": {
|
|
125
|
-
const p = payload as EmailPayloads["security.alert"];
|
|
126
|
-
return { subject, text: renderSecurityAlertBody(p) };
|
|
127
|
-
}
|
|
128
|
-
case "chat.shared": {
|
|
129
|
-
const p = payload as EmailPayloads["chat.shared"];
|
|
130
|
-
return {
|
|
96
|
+
renderUnitOwnerGranted(
|
|
97
|
+
payload as EmailPayloads["org.unit_owner_granted"],
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
case "org.ownership_transfer":
|
|
101
|
+
return toEmail(
|
|
131
102
|
subject,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
case "
|
|
137
|
-
|
|
138
|
-
return {
|
|
103
|
+
renderOwnershipTransfer(
|
|
104
|
+
payload as EmailPayloads["org.ownership_transfer"],
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
case "org.ownership_transfer_completed":
|
|
108
|
+
return toEmail(
|
|
139
109
|
subject,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
case "
|
|
145
|
-
|
|
146
|
-
return {
|
|
110
|
+
renderOwnershipTransferCompleted(
|
|
111
|
+
payload as EmailPayloads["org.ownership_transfer_completed"],
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
case "security.alert":
|
|
115
|
+
return toEmail(
|
|
147
116
|
subject,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
case "companyMd.access_request_approved": {
|
|
153
|
-
const p = payload as EmailPayloads["companyMd.access_request_approved"];
|
|
154
|
-
return {
|
|
117
|
+
renderSecurityAlert(payload as EmailPayloads["security.alert"]),
|
|
118
|
+
);
|
|
119
|
+
case "chat.shared":
|
|
120
|
+
return toEmail(
|
|
155
121
|
subject,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
case "companyMd.access_request_denied": {
|
|
161
|
-
const p = payload as EmailPayloads["companyMd.access_request_denied"];
|
|
162
|
-
return {
|
|
122
|
+
renderChatShared(payload as EmailPayloads["chat.shared"]),
|
|
123
|
+
);
|
|
124
|
+
case "share.granted":
|
|
125
|
+
return toEmail(
|
|
163
126
|
subject,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
127
|
+
renderShareGranted(payload as EmailPayloads["share.granted"]),
|
|
128
|
+
);
|
|
129
|
+
case "companyMd.access_requested":
|
|
130
|
+
return toEmail(
|
|
131
|
+
subject,
|
|
132
|
+
renderAccessRequested(
|
|
133
|
+
payload as EmailPayloads["companyMd.access_requested"],
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
case "companyMd.access_request_approved":
|
|
137
|
+
return toEmail(
|
|
138
|
+
subject,
|
|
139
|
+
renderAccessApproved(
|
|
140
|
+
payload as EmailPayloads["companyMd.access_request_approved"],
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
case "companyMd.access_request_denied":
|
|
144
|
+
return toEmail(
|
|
145
|
+
subject,
|
|
146
|
+
renderAccessDenied(
|
|
147
|
+
payload as EmailPayloads["companyMd.access_request_denied"],
|
|
148
|
+
),
|
|
149
|
+
);
|
|
168
150
|
default: {
|
|
169
151
|
// Reachable for registered-but-unimplemented kinds (e.g. auth.magic_link).
|
|
170
152
|
const unimplemented: string = kind;
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Security alert email (plain text only).
|
|
3
|
-
*
|
|
4
|
-
* Uses a single shared footer via `baseTextLayout` with a "Security" signer —
|
|
5
|
-
* the previous copy hand-rolled a second footer, printing two.
|
|
2
|
+
* Security alert email (plain text only). Signs off with a "Security" signer.
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
5
|
import type { EmailPayloads } from "../types";
|
|
9
6
|
|
|
10
|
-
import {
|
|
7
|
+
import { type Block, keyValue, paragraph, signature } from "./blocks";
|
|
11
8
|
import { COMPANY_NAME } from "./constants";
|
|
12
9
|
|
|
13
10
|
export type SecurityAlertPayload = EmailPayloads["security.alert"];
|
|
@@ -18,34 +15,35 @@ export const SECURITY_ALERT_TYPES = [
|
|
|
18
15
|
"unusual_login_location",
|
|
19
16
|
] as const;
|
|
20
17
|
|
|
21
|
-
export function
|
|
18
|
+
export function renderSecurityAlert(payload: SecurityAlertPayload): Block[] {
|
|
22
19
|
const { alertType, details, timestamp } = payload;
|
|
23
20
|
|
|
24
|
-
const
|
|
21
|
+
const blocks: Block[] = [];
|
|
25
22
|
|
|
26
23
|
switch (alertType) {
|
|
27
24
|
case "excessive_otp_requests":
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
blocks.push(
|
|
26
|
+
paragraph(
|
|
27
|
+
"We detected an unusual number of login code requests for your account.",
|
|
28
|
+
),
|
|
29
|
+
keyValue("Details", details, "normal"),
|
|
30
|
+
paragraph("If this was you, no action is needed.", "tight"),
|
|
31
|
+
paragraph(
|
|
32
|
+
"If you didn't request these codes, someone may be trying to access your account.",
|
|
33
|
+
"tight",
|
|
34
|
+
),
|
|
35
|
+
paragraph("We recommend reviewing your account security."),
|
|
30
36
|
);
|
|
31
|
-
sections.push("");
|
|
32
|
-
sections.push(`Details: ${details}`);
|
|
33
|
-
sections.push("");
|
|
34
|
-
sections.push("If this was you, no action is needed.");
|
|
35
|
-
sections.push(
|
|
36
|
-
"If you didn't request these codes, someone may be trying to access your account.",
|
|
37
|
-
);
|
|
38
|
-
sections.push("We recommend reviewing your account security.");
|
|
39
37
|
break;
|
|
40
38
|
|
|
41
39
|
case "unusual_login_location":
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
blocks.push(
|
|
41
|
+
paragraph("We detected a login attempt from an unusual location."),
|
|
42
|
+
keyValue("Details", details, "normal"),
|
|
43
|
+
paragraph("If this was you, no action is needed.", "tight"),
|
|
44
|
+
paragraph(
|
|
45
|
+
"If you didn't attempt to log in, please secure your account immediately.",
|
|
46
|
+
),
|
|
49
47
|
);
|
|
50
48
|
break;
|
|
51
49
|
|
|
@@ -55,8 +53,10 @@ export function renderSecurityAlertBody(payload: SecurityAlertPayload): string {
|
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
blocks.push(
|
|
57
|
+
keyValue("Time", new Date(timestamp).toUTCString(), "normal"),
|
|
58
|
+
signature(`${COMPANY_NAME} Security`),
|
|
59
|
+
);
|
|
60
60
|
|
|
61
|
-
return
|
|
61
|
+
return blocks;
|
|
62
62
|
}
|
|
@@ -1,62 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Share-granted email (an entity was shared with the recipient).
|
|
2
|
+
* Share-granted email (an entity was shared with the recipient).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { EmailPayloads } from "../types";
|
|
6
6
|
|
|
7
|
-
import { baseTextLayout } from "./base-text";
|
|
8
7
|
import {
|
|
9
8
|
ACCESS_PHRASE,
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
type Block,
|
|
10
|
+
bold,
|
|
11
|
+
chatUser,
|
|
12
12
|
ctaBox,
|
|
13
13
|
footer,
|
|
14
14
|
greeting,
|
|
15
|
-
|
|
15
|
+
type Inline,
|
|
16
16
|
NOTICE,
|
|
17
|
-
|
|
18
|
-
chatUserBoxText,
|
|
17
|
+
paragraph,
|
|
19
18
|
signature,
|
|
20
19
|
} from "./blocks";
|
|
21
20
|
import { COMPANY_NAME } from "./constants";
|
|
22
|
-
import { escapeHtml } from "./escape-html";
|
|
23
21
|
|
|
24
22
|
export type ShareGrantedPayload = EmailPayloads["share.granted"];
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
function describeEntity(payload: ShareGrantedPayload): string {
|
|
28
|
-
return payload.entityTitle
|
|
29
|
-
? `the ${payload.entityLabel} "${payload.entityTitle}"`
|
|
30
|
-
: `a ${payload.entityLabel}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function renderShareGrantedBody(payload: ShareGrantedPayload): string {
|
|
34
|
-
const { granterName, recipientName, accessLevel, ctaUrl, message } = payload;
|
|
35
|
-
|
|
36
|
-
const sections: string[] = [];
|
|
37
|
-
sections.push(recipientName ? `Hi ${recipientName},` : "Hi,");
|
|
38
|
-
sections.push("");
|
|
39
|
-
sections.push(
|
|
40
|
-
`${granterName} shared ${describeEntity(payload)} with you — you ${ACCESS_PHRASE[accessLevel]}.`,
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
if (message) {
|
|
44
|
-
sections.push("");
|
|
45
|
-
sections.push(chatUserBoxText(message, `Message from ${granterName}`));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
sections.push("");
|
|
49
|
-
sections.push(asciiCtaBox("OPEN"));
|
|
50
|
-
sections.push("");
|
|
51
|
-
sections.push(ctaUrl);
|
|
52
|
-
sections.push("");
|
|
53
|
-
sections.push(`This notification was sent via ${COMPANY_NAME}.`);
|
|
54
|
-
sections.push(NOTICE);
|
|
55
|
-
|
|
56
|
-
return baseTextLayout(sections.join("\n"));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function renderShareGrantedHtml(payload: ShareGrantedPayload): string {
|
|
24
|
+
export function renderShareGranted(payload: ShareGrantedPayload): Block[] {
|
|
60
25
|
const {
|
|
61
26
|
granterName,
|
|
62
27
|
recipientName,
|
|
@@ -67,16 +32,23 @@ export function renderShareGrantedHtml(payload: ShareGrantedPayload): string {
|
|
|
67
32
|
message,
|
|
68
33
|
} = payload;
|
|
69
34
|
|
|
70
|
-
const
|
|
71
|
-
? `the ${
|
|
72
|
-
: `a ${
|
|
35
|
+
const entity: Array<string | Inline> = entityTitle
|
|
36
|
+
? [`the ${entityLabel} `, bold(`"${entityTitle}"`)]
|
|
37
|
+
: [`a ${entityLabel}`];
|
|
73
38
|
|
|
74
|
-
|
|
39
|
+
const blocks: Block[] = [
|
|
75
40
|
greeting(recipientName),
|
|
76
|
-
paragraph(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
41
|
+
paragraph([
|
|
42
|
+
bold(granterName),
|
|
43
|
+
" shared ",
|
|
44
|
+
...entity,
|
|
45
|
+
` with you — you ${ACCESS_PHRASE[accessLevel]}.`,
|
|
46
|
+
]),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
if (message) blocks.push(chatUser(message, `Message from ${granterName}`));
|
|
50
|
+
|
|
51
|
+
blocks.push(
|
|
80
52
|
ctaBox({
|
|
81
53
|
label: "OPEN",
|
|
82
54
|
href: ctaUrl,
|
|
@@ -86,5 +58,7 @@ export function renderShareGrantedHtml(payload: ShareGrantedPayload): string {
|
|
|
86
58
|
}),
|
|
87
59
|
footer(`This notification was sent via ${COMPANY_NAME}.`, NOTICE),
|
|
88
60
|
signature(),
|
|
89
|
-
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return blocks;
|
|
90
64
|
}
|
|
@@ -1,66 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit-owner-granted email (added to a team's unit ownership).
|
|
2
|
+
* Unit-owner-granted email (added to a team's unit ownership).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { EmailPayloads } from "../types";
|
|
6
6
|
|
|
7
|
-
import { baseTextLayout } from "./base-text";
|
|
8
7
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
type Block,
|
|
9
|
+
bold,
|
|
10
|
+
chatUser,
|
|
11
11
|
ctaBox,
|
|
12
12
|
footer,
|
|
13
13
|
greeting,
|
|
14
|
-
htmlShell,
|
|
15
14
|
NOTICE,
|
|
16
|
-
|
|
17
|
-
chatUserBoxText,
|
|
15
|
+
paragraph,
|
|
18
16
|
signature,
|
|
19
17
|
} from "./blocks";
|
|
20
18
|
import { COMPANY_NAME } from "./constants";
|
|
21
|
-
import { escapeHtml } from "./escape-html";
|
|
22
19
|
|
|
23
20
|
export type UnitOwnerGrantedPayload = EmailPayloads["org.unit_owner_granted"];
|
|
24
21
|
|
|
25
|
-
export function
|
|
26
|
-
payload: UnitOwnerGrantedPayload,
|
|
27
|
-
): string {
|
|
28
|
-
const { granterName, recipientName, unitName, roleLabel, ctaUrl, message } =
|
|
29
|
-
payload;
|
|
30
|
-
|
|
31
|
-
const sections: string[] = [];
|
|
32
|
-
sections.push(recipientName ? `Hi ${recipientName},` : "Hi,");
|
|
33
|
-
sections.push("");
|
|
34
|
-
sections.push(`${granterName} added you as a ${roleLabel} of "${unitName}".`);
|
|
35
|
-
|
|
36
|
-
if (message) {
|
|
37
|
-
sections.push("");
|
|
38
|
-
sections.push(chatUserBoxText(message, `Message from ${granterName}`));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
sections.push("");
|
|
42
|
-
sections.push(asciiCtaBox("VIEW TEAM"));
|
|
43
|
-
sections.push("");
|
|
44
|
-
sections.push(ctaUrl);
|
|
45
|
-
sections.push("");
|
|
46
|
-
sections.push(`This notification was sent via ${COMPANY_NAME}.`);
|
|
47
|
-
sections.push(NOTICE);
|
|
48
|
-
|
|
49
|
-
return baseTextLayout(sections.join("\n"));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function renderUnitOwnerGrantedHtml(
|
|
22
|
+
export function renderUnitOwnerGranted(
|
|
53
23
|
payload: UnitOwnerGrantedPayload,
|
|
54
|
-
):
|
|
24
|
+
): Block[] {
|
|
55
25
|
const { granterName, recipientName, unitName, roleLabel, ctaUrl, message } =
|
|
56
26
|
payload;
|
|
57
27
|
|
|
58
|
-
|
|
28
|
+
const blocks: Block[] = [
|
|
59
29
|
greeting(recipientName),
|
|
60
|
-
paragraph(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
30
|
+
paragraph([
|
|
31
|
+
bold(granterName),
|
|
32
|
+
" added you as a ",
|
|
33
|
+
bold(roleLabel),
|
|
34
|
+
" of ",
|
|
35
|
+
bold(unitName),
|
|
36
|
+
".",
|
|
37
|
+
]),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
if (message) blocks.push(chatUser(message, `Message from ${granterName}`));
|
|
41
|
+
|
|
42
|
+
blocks.push(
|
|
64
43
|
ctaBox({
|
|
65
44
|
label: "VIEW TEAM",
|
|
66
45
|
href: ctaUrl,
|
|
@@ -70,5 +49,7 @@ export function renderUnitOwnerGrantedHtml(
|
|
|
70
49
|
}),
|
|
71
50
|
footer(`This notification was sent via ${COMPANY_NAME}.`, NOTICE),
|
|
72
51
|
signature(),
|
|
73
|
-
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return blocks;
|
|
74
55
|
}
|
|
@@ -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];
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plain-text email layout.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { COMPANY_NAME, SUPPORT_EMAIL } from "./constants";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Wrap plain-text email content in the shared footer
|
|
9
|
-
* (`--- / <signer> / Questions? Contact <support>`).
|
|
10
|
-
*
|
|
11
|
-
* @param content - Body content (without footer); trimmed.
|
|
12
|
-
* @param signer - Footer signer name; defaults to the company name.
|
|
13
|
-
* (`security.alert` passes "Company Semantics Security".)
|
|
14
|
-
*/
|
|
15
|
-
export function baseTextLayout(
|
|
16
|
-
content: string,
|
|
17
|
-
signer: string = COMPANY_NAME,
|
|
18
|
-
): string {
|
|
19
|
-
const footer = `
|
|
20
|
-
---
|
|
21
|
-
${signer}
|
|
22
|
-
Questions? Contact ${SUPPORT_EMAIL}
|
|
23
|
-
`;
|
|
24
|
-
|
|
25
|
-
return content.trim() + "\n" + footer.trim() + "\n";
|
|
26
|
-
}
|