@company-semantics/contracts 33.0.0 → 35.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 +4 -4
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +2 -2
- package/src/index.ts +17 -11
- package/src/notifications/README.md +140 -0
- package/src/notifications/__tests__/README.md +54 -0
- package/src/notifications/__tests__/__snapshots__/README.md +32 -0
- package/src/notifications/__tests__/content.test.ts +186 -0
- package/src/notifications/__tests__/context.test.ts +72 -0
- package/src/notifications/__tests__/definition.test.ts +222 -0
- package/src/notifications/__tests__/kinds.test.ts +81 -0
- package/src/notifications/__tests__/output-parity.golden.ts +363 -0
- package/src/notifications/__tests__/output-parity.test.ts +122 -0
- package/src/notifications/__tests__/registry.test.ts +184 -0
- package/src/{email/render → notifications}/__tests__/render-snapshot.test.ts +58 -29
- package/src/notifications/__tests__/renderer.test.ts +181 -0
- package/src/notifications/content.ts +249 -0
- package/src/notifications/context.ts +70 -0
- package/src/notifications/definition.ts +82 -0
- package/src/notifications/index.ts +104 -0
- package/src/notifications/kinds/README.md +57 -0
- package/src/notifications/kinds/auth-otp.ts +99 -0
- package/src/notifications/kinds/chat-shared.ts +56 -0
- package/src/notifications/kinds/company-md-access-approved.ts +57 -0
- package/src/notifications/kinds/company-md-access-denied.ts +61 -0
- package/src/notifications/kinds/company-md-access-requested.ts +65 -0
- package/src/notifications/kinds/index.ts +19 -0
- package/src/notifications/kinds/org-invite.ts +62 -0
- package/src/notifications/kinds/ownership-transfer-completed.ts +57 -0
- package/src/notifications/kinds/ownership-transfer.ts +68 -0
- package/src/notifications/kinds/security-alert.ts +78 -0
- package/src/notifications/kinds/share-granted.ts +74 -0
- package/src/notifications/kinds/unit-owner-granted.ts +95 -0
- package/src/notifications/kinds.ts +68 -0
- package/src/{email/types.ts → notifications/payloads.ts} +33 -68
- package/src/notifications/registry.ts +107 -0
- package/src/notifications/render.ts +106 -0
- package/src/notifications/renderer.ts +50 -0
- package/src/notifications/renderers/README.md +50 -0
- package/src/notifications/renderers/email/README.md +74 -0
- package/src/notifications/renderers/email/__tests__/README.md +29 -0
- package/src/notifications/renderers/email/__tests__/render.test.ts +180 -0
- package/src/{email/render → notifications/renderers/email}/chat.ts +53 -107
- package/src/notifications/renderers/email/constants.ts +47 -0
- package/src/notifications/renderers/email/cta.ts +63 -0
- package/src/{email/render → notifications/renderers/email}/escape-html.ts +5 -1
- package/src/notifications/renderers/email/index.ts +73 -0
- package/src/notifications/renderers/email/render.ts +225 -0
- package/src/notifications/renderers/email/shells.ts +60 -0
- package/src/notifications/renderers/slack/README.md +73 -0
- package/src/notifications/renderers/slack/__tests__/README.md +33 -0
- package/src/notifications/renderers/slack/__tests__/index.test.ts +201 -0
- package/src/notifications/renderers/slack/index.ts +261 -0
- package/src/notifications/renderers/sms/README.md +69 -0
- package/src/notifications/renderers/sms/__tests__/README.md +31 -0
- package/src/notifications/renderers/sms/__tests__/index.test.ts +162 -0
- package/src/notifications/renderers/sms/index.ts +131 -0
- package/src/notifications/text.ts +52 -0
- package/src/email/README.md +0 -51
- package/src/email/__tests__/registry.test.ts +0 -161
- package/src/email/index.ts +0 -36
- package/src/email/registry.ts +0 -155
- package/src/email/render/auth-otp.ts +0 -71
- package/src/email/render/blocks.ts +0 -281
- package/src/email/render/chat-shared.ts +0 -35
- package/src/email/render/company-md-access-approved.ts +0 -42
- package/src/email/render/company-md-access-denied.ts +0 -43
- package/src/email/render/company-md-access-requested.ts +0 -46
- package/src/email/render/constants.ts +0 -18
- package/src/email/render/index.ts +0 -58
- package/src/email/render/org-invite.ts +0 -40
- package/src/email/render/ownership-transfer-completed.ts +0 -41
- package/src/email/render/ownership-transfer.ts +0 -42
- package/src/email/render/render-email.ts +0 -194
- package/src/email/render/security-alert.ts +0 -61
- package/src/email/render/share-granted.ts +0 -52
- package/src/email/render/unit-owner-granted.ts +0 -60
- /package/src/{email/render → notifications}/__tests__/__snapshots__/render-snapshot.test.ts.snap +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-shared notification (someone shared a chat).
|
|
3
|
+
*
|
|
4
|
+
* The only kind that reproduces a real conversation: the chat's title is shown
|
|
5
|
+
* as the user's turn and the preview as the assistant's reply, elided into the
|
|
6
|
+
* call to action. `visibility` is carried by the payload but says nothing here —
|
|
7
|
+
* it is a fact about the share, not about what this notification tells you.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ChatUnitItem } from "../content";
|
|
11
|
+
import type { NotificationDefinition } from "../definition";
|
|
12
|
+
|
|
13
|
+
import { NOTICE } from "../text";
|
|
14
|
+
|
|
15
|
+
export const chatSharedDefinition: NotificationDefinition<"chat.shared"> = {
|
|
16
|
+
kind: "chat.shared",
|
|
17
|
+
compose: (payload, context) => {
|
|
18
|
+
const { chatTitle, shareUrl, previewText, sharedByName } = payload;
|
|
19
|
+
|
|
20
|
+
const items: ChatUnitItem[] = [
|
|
21
|
+
{ type: "message", role: "user", text: chatTitle, from: sharedByName },
|
|
22
|
+
...(previewText
|
|
23
|
+
? [
|
|
24
|
+
{
|
|
25
|
+
type: "message" as const,
|
|
26
|
+
role: "assistant" as const,
|
|
27
|
+
text: previewText,
|
|
28
|
+
},
|
|
29
|
+
]
|
|
30
|
+
: []),
|
|
31
|
+
{ type: "continuation" },
|
|
32
|
+
{ type: "callToAction", label: "SEE MORE", href: shareUrl },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
metadata: {
|
|
37
|
+
kind: "chat.shared",
|
|
38
|
+
title: "A chat has been shared with you",
|
|
39
|
+
},
|
|
40
|
+
sections: [
|
|
41
|
+
{
|
|
42
|
+
elements: [
|
|
43
|
+
{ type: "greeting" },
|
|
44
|
+
{ type: "body", text: "A chat was shared with you." },
|
|
45
|
+
{ type: "chatUnit", items },
|
|
46
|
+
{
|
|
47
|
+
type: "notice",
|
|
48
|
+
lines: [`This share was sent via ${context.brand.name}.`, NOTICE],
|
|
49
|
+
},
|
|
50
|
+
{ type: "signature" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company.md access-approved notification (sent to the requester).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NotificationDefinition } from "../definition";
|
|
6
|
+
|
|
7
|
+
import { ACCESS_PHRASE } from "../text";
|
|
8
|
+
|
|
9
|
+
export const accessApprovedDefinition: NotificationDefinition<"companyMd.access_request_approved"> =
|
|
10
|
+
{
|
|
11
|
+
kind: "companyMd.access_request_approved",
|
|
12
|
+
compose: (payload, context) => {
|
|
13
|
+
const { approverName, docTitle, accessLevel, docUrl } = payload;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
metadata: {
|
|
17
|
+
kind: "companyMd.access_request_approved",
|
|
18
|
+
title: "Your access request was approved",
|
|
19
|
+
},
|
|
20
|
+
sections: [
|
|
21
|
+
{
|
|
22
|
+
elements: [
|
|
23
|
+
{ type: "greeting" },
|
|
24
|
+
{ type: "body", text: "Your access request was approved." },
|
|
25
|
+
{
|
|
26
|
+
type: "chatUnit",
|
|
27
|
+
items: [
|
|
28
|
+
{
|
|
29
|
+
type: "message",
|
|
30
|
+
role: "assistant",
|
|
31
|
+
text: "Open to view.",
|
|
32
|
+
},
|
|
33
|
+
{ type: "callToAction", label: "OPEN", href: docUrl },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: "keyValueTable",
|
|
38
|
+
rows: [
|
|
39
|
+
{ label: "Document", value: `"${docTitle}"` },
|
|
40
|
+
{ label: "Access", value: ACCESS_PHRASE[accessLevel] },
|
|
41
|
+
{ label: "Approved by", value: approverName },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "notice",
|
|
46
|
+
lines: [
|
|
47
|
+
`This notification was sent via ${context.brand.name}.`,
|
|
48
|
+
"Access is now active.",
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{ type: "signature" },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company.md access-denied notification (sent to the requester).
|
|
3
|
+
*
|
|
4
|
+
* The only kind with no call to action, and the only one whose chat unit exists
|
|
5
|
+
* solely to carry someone's words: with no `reason` there is nothing to quote,
|
|
6
|
+
* so the whole unit drops rather than degrading into an empty bubble.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { NotificationElement } from "../content";
|
|
10
|
+
import type { NotificationDefinition } from "../definition";
|
|
11
|
+
|
|
12
|
+
export const accessDeniedDefinition: NotificationDefinition<"companyMd.access_request_denied"> =
|
|
13
|
+
{
|
|
14
|
+
kind: "companyMd.access_request_denied",
|
|
15
|
+
compose: (payload, context) => {
|
|
16
|
+
const { approverName, docTitle, reason } = payload;
|
|
17
|
+
|
|
18
|
+
const elements: NotificationElement[] = [
|
|
19
|
+
{ type: "greeting" },
|
|
20
|
+
{ type: "body", text: "Your access request was declined." },
|
|
21
|
+
...(reason
|
|
22
|
+
? [
|
|
23
|
+
{
|
|
24
|
+
type: "chatUnit" as const,
|
|
25
|
+
items: [
|
|
26
|
+
{
|
|
27
|
+
type: "message" as const,
|
|
28
|
+
role: "user" as const,
|
|
29
|
+
text: reason,
|
|
30
|
+
from: approverName,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
: []),
|
|
36
|
+
{
|
|
37
|
+
type: "keyValueTable",
|
|
38
|
+
rows: [
|
|
39
|
+
{ label: "Document", value: `"${docTitle}"` },
|
|
40
|
+
{ label: "Declined by", value: approverName },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: "notice",
|
|
45
|
+
lines: [
|
|
46
|
+
`This notification was sent via ${context.brand.name}.`,
|
|
47
|
+
"This request is now closed.",
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{ type: "signature" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
metadata: {
|
|
55
|
+
kind: "companyMd.access_request_denied",
|
|
56
|
+
title: "Your access request was reviewed",
|
|
57
|
+
},
|
|
58
|
+
sections: [{ elements }],
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company.md access-requested notification (sent to the doc owner).
|
|
3
|
+
*
|
|
4
|
+
* Closes by telling the recipient WHY they were asked ("You own this document.")
|
|
5
|
+
* rather than with the standard `NOTICE` — this notification is a request for a
|
|
6
|
+
* decision, so "no action is required" would be false.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { NotificationDefinition } from "../definition";
|
|
10
|
+
|
|
11
|
+
export const accessRequestedDefinition: NotificationDefinition<"companyMd.access_requested"> =
|
|
12
|
+
{
|
|
13
|
+
kind: "companyMd.access_requested",
|
|
14
|
+
compose: (payload, context) => {
|
|
15
|
+
const { requesterName, docTitle, message, reviewUrl } = payload;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
metadata: {
|
|
19
|
+
kind: "companyMd.access_requested",
|
|
20
|
+
title: "Someone requested access to a document",
|
|
21
|
+
},
|
|
22
|
+
sections: [
|
|
23
|
+
{
|
|
24
|
+
elements: [
|
|
25
|
+
{ type: "greeting" },
|
|
26
|
+
{ type: "body", text: "Someone requested document access." },
|
|
27
|
+
{
|
|
28
|
+
type: "chatUnit",
|
|
29
|
+
items: [
|
|
30
|
+
message
|
|
31
|
+
? {
|
|
32
|
+
type: "message",
|
|
33
|
+
role: "user",
|
|
34
|
+
text: message,
|
|
35
|
+
from: requesterName,
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
type: "message",
|
|
39
|
+
role: "assistant",
|
|
40
|
+
text: "Review the request.",
|
|
41
|
+
},
|
|
42
|
+
{ type: "callToAction", label: "REVIEW", href: reviewUrl },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "keyValueTable",
|
|
47
|
+
rows: [
|
|
48
|
+
{ label: "From", value: requesterName },
|
|
49
|
+
{ label: "Document", value: `"${docTitle}"` },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "notice",
|
|
54
|
+
lines: [
|
|
55
|
+
`This notification was sent via ${context.brand.name}.`,
|
|
56
|
+
"You own this document.",
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{ type: "signature" },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every notification definition, one module each (ADR-CONTRACTS-085).
|
|
3
|
+
*
|
|
4
|
+
* Import this barrel as `./kinds/index`, never `./kinds` — the sibling
|
|
5
|
+
* `../kinds.ts` holds the `NotificationKind` union and wins that specifier under
|
|
6
|
+
* `moduleResolution: "bundler"`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { accessApprovedDefinition } from "./company-md-access-approved";
|
|
10
|
+
export { accessDeniedDefinition } from "./company-md-access-denied";
|
|
11
|
+
export { accessRequestedDefinition } from "./company-md-access-requested";
|
|
12
|
+
export { authOtpDefinition } from "./auth-otp";
|
|
13
|
+
export { chatSharedDefinition } from "./chat-shared";
|
|
14
|
+
export { orgInviteDefinition } from "./org-invite";
|
|
15
|
+
export { ownershipTransferCompletedDefinition } from "./ownership-transfer-completed";
|
|
16
|
+
export { ownershipTransferDefinition } from "./ownership-transfer";
|
|
17
|
+
export { securityAlertDefinition } from "./security-alert";
|
|
18
|
+
export { shareGrantedDefinition } from "./share-granted";
|
|
19
|
+
export { unitOwnerGrantedDefinition } from "./unit-owner-granted";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org invite notification (join a workspace).
|
|
3
|
+
*
|
|
4
|
+
* The title interpolates `orgName` directly. `renderEmail` fills the registry's
|
|
5
|
+
* `{orgName}` placeholder against an untyped bag and leaves it verbatim when the
|
|
6
|
+
* field is missing; here the compiler guarantees the field, so the fallback has
|
|
7
|
+
* nothing left to catch. See ADR-CONTRACTS-085.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { NotificationDefinition } from "../definition";
|
|
11
|
+
|
|
12
|
+
import { formatExpiry, NOTICE, titleCase } from "../text";
|
|
13
|
+
|
|
14
|
+
export const orgInviteDefinition: NotificationDefinition<"org.invite"> = {
|
|
15
|
+
kind: "org.invite",
|
|
16
|
+
compose: (payload, context) => {
|
|
17
|
+
const { inviterName, orgName, role, acceptUrl, expiresAt } = payload;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
metadata: {
|
|
21
|
+
kind: "org.invite",
|
|
22
|
+
title: `You've been invited to join ${orgName} on Company Semantics`,
|
|
23
|
+
},
|
|
24
|
+
sections: [
|
|
25
|
+
{
|
|
26
|
+
elements: [
|
|
27
|
+
{ type: "greeting" },
|
|
28
|
+
{ type: "body", text: `${orgName} uses Company Semantics.` },
|
|
29
|
+
{
|
|
30
|
+
type: "chatUnit",
|
|
31
|
+
items: [
|
|
32
|
+
{
|
|
33
|
+
type: "message",
|
|
34
|
+
role: "assistant",
|
|
35
|
+
text: "Join to accept the invitation.",
|
|
36
|
+
},
|
|
37
|
+
{ type: "callToAction", label: "JOIN", href: acceptUrl },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "keyValueTable",
|
|
42
|
+
rows: [
|
|
43
|
+
{ label: "From", value: inviterName },
|
|
44
|
+
{ label: "Workspace", value: orgName },
|
|
45
|
+
{ label: "Role", value: titleCase(role) },
|
|
46
|
+
{ label: "Expires", value: formatExpiry(expiresAt) },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: "notice",
|
|
51
|
+
lines: [
|
|
52
|
+
`This invitation was sent via ${context.brand.name}.`,
|
|
53
|
+
NOTICE,
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{ type: "signature" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership-transfer-completed notice.
|
|
3
|
+
*
|
|
4
|
+
* Closes with a support instruction rather than the standard `NOTICE`: this one
|
|
5
|
+
* reports a change that already happened, so "no action is required" would be
|
|
6
|
+
* wrong if the recipient did not authorise it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { NotificationDefinition } from "../definition";
|
|
10
|
+
|
|
11
|
+
export const ownershipTransferCompletedDefinition: NotificationDefinition<"org.ownership_transfer_completed"> =
|
|
12
|
+
{
|
|
13
|
+
kind: "org.ownership_transfer_completed",
|
|
14
|
+
compose: (payload, context) => {
|
|
15
|
+
const { orgName, newOwnerEmail } = payload;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
metadata: {
|
|
19
|
+
kind: "org.ownership_transfer_completed",
|
|
20
|
+
title: "Workspace ownership has been transferred",
|
|
21
|
+
},
|
|
22
|
+
sections: [
|
|
23
|
+
{
|
|
24
|
+
elements: [
|
|
25
|
+
{ type: "greeting" },
|
|
26
|
+
{ type: "body", text: "Ownership was transferred." },
|
|
27
|
+
{
|
|
28
|
+
type: "chatUnit",
|
|
29
|
+
items: [
|
|
30
|
+
{
|
|
31
|
+
type: "message",
|
|
32
|
+
role: "assistant",
|
|
33
|
+
text: "You're no longer the owner, but you remain an admin.",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "keyValueTable",
|
|
39
|
+
rows: [
|
|
40
|
+
{ label: "Workspace", value: orgName },
|
|
41
|
+
{ label: "New owner", value: newOwnerEmail },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "notice",
|
|
46
|
+
lines: [
|
|
47
|
+
`This confirmation was sent via ${context.brand.name}.`,
|
|
48
|
+
"If you did not authorize this, contact support immediately.",
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{ type: "signature" },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership-transfer invitation notification.
|
|
3
|
+
*
|
|
4
|
+
* `fromName` is absent for an admin-initiated transfer — there is no workspace
|
|
5
|
+
* owner to attribute — so the note bubble becomes an unattributed prompt and the
|
|
6
|
+
* From row drops out entirely.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { KeyValueRow } from "../content";
|
|
10
|
+
import type { NotificationDefinition } from "../definition";
|
|
11
|
+
|
|
12
|
+
import { formatExpiry, NOTICE } from "../text";
|
|
13
|
+
|
|
14
|
+
export const ownershipTransferDefinition: NotificationDefinition<"org.ownership_transfer"> =
|
|
15
|
+
{
|
|
16
|
+
kind: "org.ownership_transfer",
|
|
17
|
+
compose: (payload, context) => {
|
|
18
|
+
const { orgName, acceptUrl, expiresAt, note, fromName } = payload;
|
|
19
|
+
|
|
20
|
+
const rows: KeyValueRow[] = [
|
|
21
|
+
...(fromName ? [{ label: "From", value: fromName }] : []),
|
|
22
|
+
{ label: "Workspace", value: orgName },
|
|
23
|
+
{ label: "Expires", value: formatExpiry(expiresAt) },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
metadata: {
|
|
28
|
+
kind: "org.ownership_transfer",
|
|
29
|
+
title: "You've been invited to become a workspace owner",
|
|
30
|
+
},
|
|
31
|
+
sections: [
|
|
32
|
+
{
|
|
33
|
+
elements: [
|
|
34
|
+
{ type: "greeting" },
|
|
35
|
+
{ type: "body", text: "You've been invited to take ownership." },
|
|
36
|
+
{
|
|
37
|
+
type: "chatUnit",
|
|
38
|
+
items: [
|
|
39
|
+
note
|
|
40
|
+
? {
|
|
41
|
+
type: "message",
|
|
42
|
+
role: "user",
|
|
43
|
+
text: note,
|
|
44
|
+
...(fromName ? { from: fromName } : {}),
|
|
45
|
+
}
|
|
46
|
+
: {
|
|
47
|
+
type: "message",
|
|
48
|
+
role: "assistant",
|
|
49
|
+
text: "Accept to take ownership.",
|
|
50
|
+
},
|
|
51
|
+
{ type: "callToAction", label: "ACCEPT", href: acceptUrl },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{ type: "keyValueTable", rows },
|
|
55
|
+
{
|
|
56
|
+
type: "notice",
|
|
57
|
+
lines: [
|
|
58
|
+
`This transfer was sent via ${context.brand.name}.`,
|
|
59
|
+
NOTICE,
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{ type: "signature" },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security alert notification.
|
|
3
|
+
*
|
|
4
|
+
* The odd one out, in three ways, all faithful to what this alert says today:
|
|
5
|
+
* - It opens on a `warning` and has no greeting. A security notice addresses the
|
|
6
|
+
* account, not the person.
|
|
7
|
+
* - It closes with no `notice`. "If you weren't expecting this, no action is
|
|
8
|
+
* required" is the opposite of this notification's point.
|
|
9
|
+
* - It emits TWO `keyValueTable`s rather than one. `Details` sits between the
|
|
10
|
+
* body lines and `Time` closes the alert, so grouping them would reorder what
|
|
11
|
+
* the recipient reads. Order is content; the tables stay split.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { NotificationElement } from "../content";
|
|
15
|
+
import type { NotificationDefinition } from "../definition";
|
|
16
|
+
import type { NotificationPayloads } from "../payloads";
|
|
17
|
+
|
|
18
|
+
type SecurityAlertType = NotificationPayloads["security.alert"]["alertType"];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The opening line and the "if you didn't…" line, per alert type. The three
|
|
22
|
+
* lines around them are identical for every alert, so only the difference is
|
|
23
|
+
* tabulated — the shape of the alert is stated once, in `compose`.
|
|
24
|
+
*/
|
|
25
|
+
const ALERT_COPY: Record<
|
|
26
|
+
SecurityAlertType,
|
|
27
|
+
{ opening: string; denial: string }
|
|
28
|
+
> = {
|
|
29
|
+
excessive_otp_requests: {
|
|
30
|
+
opening: "Unusual login-code activity detected.",
|
|
31
|
+
denial:
|
|
32
|
+
"If you didn't request these codes, someone may be trying to access your account.",
|
|
33
|
+
},
|
|
34
|
+
unusual_login_location: {
|
|
35
|
+
opening: "A login from an unusual location was detected.",
|
|
36
|
+
denial:
|
|
37
|
+
"If you didn't attempt to log in, someone may be trying to access your account.",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const securityAlertDefinition: NotificationDefinition<"security.alert"> =
|
|
42
|
+
{
|
|
43
|
+
kind: "security.alert",
|
|
44
|
+
compose: (payload) => {
|
|
45
|
+
const { alertType, details, timestamp } = payload;
|
|
46
|
+
const copy = ALERT_COPY[alertType];
|
|
47
|
+
|
|
48
|
+
const elements: NotificationElement[] = [
|
|
49
|
+
{ type: "warning" },
|
|
50
|
+
{ type: "body", text: copy.opening },
|
|
51
|
+
{ type: "keyValueTable", rows: [{ label: "Details", value: details }] },
|
|
52
|
+
// ONE paragraph, two lines: the reassurance and the denial are a couplet
|
|
53
|
+
// ("if it was you, nothing; if it wasn't, worry"), and the alert has read
|
|
54
|
+
// that way since it was written — the old `email/render/security-alert`
|
|
55
|
+
// spelled it `paragraph(..., "tight")` followed by a normal paragraph.
|
|
56
|
+
// Splitting them into two `body` elements would say they are two
|
|
57
|
+
// thoughts, and no renderer could recover that they are not.
|
|
58
|
+
{
|
|
59
|
+
type: "body",
|
|
60
|
+
text: `If this was you, no action is needed.\n${copy.denial}`,
|
|
61
|
+
},
|
|
62
|
+
{ type: "body", text: "We recommend reviewing your account security." },
|
|
63
|
+
{
|
|
64
|
+
type: "keyValueTable",
|
|
65
|
+
rows: [{ label: "Time", value: new Date(timestamp).toUTCString() }],
|
|
66
|
+
},
|
|
67
|
+
{ type: "signature" },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
metadata: {
|
|
72
|
+
kind: "security.alert",
|
|
73
|
+
title: "Security alert for your account",
|
|
74
|
+
},
|
|
75
|
+
sections: [{ elements }],
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share-granted notification (an entity was shared with the recipient).
|
|
3
|
+
*
|
|
4
|
+
* `entityTitle` is absent for entities that have no title (a meeting), so the
|
|
5
|
+
* Item row drops rather than rendering an empty quote.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { KeyValueRow } from "../content";
|
|
9
|
+
import type { NotificationDefinition } from "../definition";
|
|
10
|
+
|
|
11
|
+
import { ACCESS_PHRASE, NOTICE } from "../text";
|
|
12
|
+
|
|
13
|
+
export const shareGrantedDefinition: NotificationDefinition<"share.granted"> = {
|
|
14
|
+
kind: "share.granted",
|
|
15
|
+
compose: (payload, context) => {
|
|
16
|
+
const {
|
|
17
|
+
granterName,
|
|
18
|
+
recipientName,
|
|
19
|
+
entityLabel,
|
|
20
|
+
entityTitle,
|
|
21
|
+
accessLevel,
|
|
22
|
+
ctaUrl,
|
|
23
|
+
message,
|
|
24
|
+
} = payload;
|
|
25
|
+
|
|
26
|
+
const rows: KeyValueRow[] = [
|
|
27
|
+
{ label: "From", value: granterName },
|
|
28
|
+
...(entityTitle ? [{ label: "Item", value: `"${entityTitle}"` }] : []),
|
|
29
|
+
{ label: "Access", value: ACCESS_PHRASE[accessLevel] },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
metadata: {
|
|
34
|
+
kind: "share.granted",
|
|
35
|
+
title: "Something has been shared with you",
|
|
36
|
+
},
|
|
37
|
+
sections: [
|
|
38
|
+
{
|
|
39
|
+
elements: [
|
|
40
|
+
{ type: "greeting", ...(recipientName ? { recipientName } : {}) },
|
|
41
|
+
{ type: "body", text: `A ${entityLabel} was shared with you.` },
|
|
42
|
+
{
|
|
43
|
+
type: "chatUnit",
|
|
44
|
+
items: [
|
|
45
|
+
message
|
|
46
|
+
? {
|
|
47
|
+
type: "message",
|
|
48
|
+
role: "user",
|
|
49
|
+
text: message,
|
|
50
|
+
from: granterName,
|
|
51
|
+
}
|
|
52
|
+
: {
|
|
53
|
+
type: "message",
|
|
54
|
+
role: "assistant",
|
|
55
|
+
text: "Open to view.",
|
|
56
|
+
},
|
|
57
|
+
{ type: "callToAction", label: "OPEN", href: ctaUrl },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{ type: "keyValueTable", rows },
|
|
61
|
+
{
|
|
62
|
+
type: "notice",
|
|
63
|
+
lines: [
|
|
64
|
+
`This notification was sent via ${context.brand.name}.`,
|
|
65
|
+
NOTICE,
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{ type: "signature" },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|