@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.
Files changed (78) hide show
  1. package/package.json +4 -4
  2. package/src/api/generated-spec-hash.ts +2 -2
  3. package/src/api/generated.ts +2 -2
  4. package/src/index.ts +17 -11
  5. package/src/notifications/README.md +140 -0
  6. package/src/notifications/__tests__/README.md +54 -0
  7. package/src/notifications/__tests__/__snapshots__/README.md +32 -0
  8. package/src/notifications/__tests__/content.test.ts +186 -0
  9. package/src/notifications/__tests__/context.test.ts +72 -0
  10. package/src/notifications/__tests__/definition.test.ts +222 -0
  11. package/src/notifications/__tests__/kinds.test.ts +81 -0
  12. package/src/notifications/__tests__/output-parity.golden.ts +363 -0
  13. package/src/notifications/__tests__/output-parity.test.ts +122 -0
  14. package/src/notifications/__tests__/registry.test.ts +184 -0
  15. package/src/{email/render → notifications}/__tests__/render-snapshot.test.ts +58 -29
  16. package/src/notifications/__tests__/renderer.test.ts +181 -0
  17. package/src/notifications/content.ts +249 -0
  18. package/src/notifications/context.ts +70 -0
  19. package/src/notifications/definition.ts +82 -0
  20. package/src/notifications/index.ts +104 -0
  21. package/src/notifications/kinds/README.md +57 -0
  22. package/src/notifications/kinds/auth-otp.ts +99 -0
  23. package/src/notifications/kinds/chat-shared.ts +56 -0
  24. package/src/notifications/kinds/company-md-access-approved.ts +57 -0
  25. package/src/notifications/kinds/company-md-access-denied.ts +61 -0
  26. package/src/notifications/kinds/company-md-access-requested.ts +65 -0
  27. package/src/notifications/kinds/index.ts +19 -0
  28. package/src/notifications/kinds/org-invite.ts +62 -0
  29. package/src/notifications/kinds/ownership-transfer-completed.ts +57 -0
  30. package/src/notifications/kinds/ownership-transfer.ts +68 -0
  31. package/src/notifications/kinds/security-alert.ts +78 -0
  32. package/src/notifications/kinds/share-granted.ts +74 -0
  33. package/src/notifications/kinds/unit-owner-granted.ts +95 -0
  34. package/src/notifications/kinds.ts +68 -0
  35. package/src/{email/types.ts → notifications/payloads.ts} +33 -68
  36. package/src/notifications/registry.ts +107 -0
  37. package/src/notifications/render.ts +106 -0
  38. package/src/notifications/renderer.ts +50 -0
  39. package/src/notifications/renderers/README.md +50 -0
  40. package/src/notifications/renderers/email/README.md +74 -0
  41. package/src/notifications/renderers/email/__tests__/README.md +29 -0
  42. package/src/notifications/renderers/email/__tests__/render.test.ts +180 -0
  43. package/src/{email/render → notifications/renderers/email}/chat.ts +53 -107
  44. package/src/notifications/renderers/email/constants.ts +47 -0
  45. package/src/notifications/renderers/email/cta.ts +63 -0
  46. package/src/{email/render → notifications/renderers/email}/escape-html.ts +5 -1
  47. package/src/notifications/renderers/email/index.ts +73 -0
  48. package/src/notifications/renderers/email/render.ts +225 -0
  49. package/src/notifications/renderers/email/shells.ts +60 -0
  50. package/src/notifications/renderers/slack/README.md +73 -0
  51. package/src/notifications/renderers/slack/__tests__/README.md +33 -0
  52. package/src/notifications/renderers/slack/__tests__/index.test.ts +201 -0
  53. package/src/notifications/renderers/slack/index.ts +261 -0
  54. package/src/notifications/renderers/sms/README.md +69 -0
  55. package/src/notifications/renderers/sms/__tests__/README.md +31 -0
  56. package/src/notifications/renderers/sms/__tests__/index.test.ts +162 -0
  57. package/src/notifications/renderers/sms/index.ts +131 -0
  58. package/src/notifications/text.ts +52 -0
  59. package/src/email/README.md +0 -51
  60. package/src/email/__tests__/registry.test.ts +0 -161
  61. package/src/email/index.ts +0 -36
  62. package/src/email/registry.ts +0 -155
  63. package/src/email/render/auth-otp.ts +0 -71
  64. package/src/email/render/blocks.ts +0 -281
  65. package/src/email/render/chat-shared.ts +0 -35
  66. package/src/email/render/company-md-access-approved.ts +0 -42
  67. package/src/email/render/company-md-access-denied.ts +0 -43
  68. package/src/email/render/company-md-access-requested.ts +0 -46
  69. package/src/email/render/constants.ts +0 -18
  70. package/src/email/render/index.ts +0 -58
  71. package/src/email/render/org-invite.ts +0 -40
  72. package/src/email/render/ownership-transfer-completed.ts +0 -41
  73. package/src/email/render/ownership-transfer.ts +0 -42
  74. package/src/email/render/render-email.ts +0 -194
  75. package/src/email/render/security-alert.ts +0 -61
  76. package/src/email/render/share-granted.ts +0 -52
  77. package/src/email/render/unit-owner-granted.ts +0 -60
  78. /package/src/{email/render → notifications}/__tests__/__snapshots__/render-snapshot.test.ts.snap +0 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The stock wording and the value formats every kind's copy is built from
3
+ * (ADR-CONTRACTS-086).
4
+ *
5
+ * `compose` needs a few things that are neither the content MODEL nor any
6
+ * channel's markup: a reassurance line every kind ends on, the phrasing for an
7
+ * access level, and the two display formats the kinds agree on. They lived in
8
+ * the email layer's `blocks.ts` alongside the markup because that was the only
9
+ * layer there was. They are here because what a notification SAYS is the same
10
+ * on every channel — an SMS reassures in the same words email does.
11
+ *
12
+ * INVARIANTS:
13
+ * - Channel-agnostic. Nothing here may emit markup. The moment one of these
14
+ * returns a tag, the seam ADR-CONTRACTS-086 draws is back where it started.
15
+ * - Pure. `formatExpiry` builds a `Date` from its ARGUMENT, which is not a clock
16
+ * read; nothing here may add one.
17
+ * - These strings are output. The output-parity golden asserts them
18
+ * byte-for-byte, so a wording change here changes a real sent email.
19
+ */
20
+
21
+ /** The standard "no action required" reassurance line. */
22
+ export const NOTICE = "If you weren't expecting this, no action is required.";
23
+
24
+ /** Human phrasing per access level (share-granted, access-approved). */
25
+ export const ACCESS_PHRASE: Record<"editor" | "commenter" | "viewer", string> =
26
+ {
27
+ editor: "can edit",
28
+ commenter: "can comment",
29
+ viewer: "can view",
30
+ };
31
+
32
+ /** Title-case a display value — capitalize the first letter of each word, e.g. a
33
+ * role like "admin" → "Admin". Leaves already-capitalized letters untouched. */
34
+ export function titleCase(s: string): string {
35
+ return s.replace(/\b\w/g, (c) => c.toUpperCase());
36
+ }
37
+
38
+ /**
39
+ * Format an ISO timestamp as "Jun 13, 2026". Pinned to en-US + UTC so the
40
+ * output is locale-/timezone-independent and rendered bytes stay deterministic.
41
+ * Returns the raw input unchanged if it is not a parseable date.
42
+ */
43
+ export function formatExpiry(iso: string): string {
44
+ const d = new Date(iso);
45
+ if (Number.isNaN(d.getTime())) return iso;
46
+ return d.toLocaleDateString("en-US", {
47
+ month: "short",
48
+ day: "numeric",
49
+ year: "numeric",
50
+ timeZone: "UTC",
51
+ });
52
+ }
@@ -1,51 +0,0 @@
1
- # email/
2
-
3
- ## Purpose
4
-
5
- Transactional email vocabulary **and** render layer: kind types, the subject
6
- registry, and the shared component system (`render/`) that both the backend
7
- (real sends) and the app (Ladle preview) compose. See ADR-CONTRACTS-077.
8
-
9
- ## Invariants
10
-
11
- - Types + **pure** render functions only — no classes, no side effects, no
12
- external imports beyond `zod` (vocabulary-guard enforced).
13
- - `render/blocks.ts` owns all styled markup; templates supply only content text.
14
- Editing a block restyles every email — HTML and plain text — in both repos.
15
- - `renderEmail(kind, payload, options?)` is the single dispatcher; subject comes
16
- from `EMAIL_KINDS`, never hardcoded in a template.
17
- - `EmailKind` union MUST match keys in `EMAIL_KINDS` registry.
18
- - OTP values MUST NEVER appear as literals in contracts (security: logged if
19
- leaked) — they are runtime payload fields only.
20
- - Every user-controlled field in an HTML variant MUST pass through `escapeHtml`.
21
- - Rendered output is locked by `render/__tests__/render-snapshot.test.ts`
22
- (production-parity markup — a snapshot change means a real email changed).
23
- - Unknown email kinds MUST be rejected at API boundaries via `isValidEmailKind()`.
24
-
25
- <!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
26
-
27
- ## Public API
28
-
29
- - `EMAIL_KINDS` — EMAILKINDS is the authoritative registry.
30
- - `EmailKind` _(type)_ — EmailKind identifies the type of transactional email.
31
- - `EmailKindDefinition` _(type)_ — Complete definition for an email kind.
32
- - `EmailPayloads` _(type)_ — Type-safe payload mapping for each email kind.
33
- - `SendEmailInput` _(type)_ — Type-safe input for sending emails.
34
- - `getEmailKindDefinition` — Type-safe registry lookup.
35
- - `isValidEmailKind` — Check if a string is a valid EmailKind.
36
-
37
- <!-- END GENERATED: readme-public-api -->
38
-
39
- <!-- BEGIN GENERATED: readme-dependencies — derived from code by `pnpm readme-api`. Do not edit. -->
40
-
41
- ## Dependencies
42
-
43
- **Internal domains:**
44
-
45
- _None._
46
-
47
- **External packages:**
48
-
49
- _None._
50
-
51
- <!-- END GENERATED: readme-dependencies -->
@@ -1,161 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- EMAIL_KINDS,
4
- getEmailKindDefinition,
5
- isValidEmailKind,
6
- } from "../registry.js";
7
-
8
- describe("EMAIL_KINDS golden snapshot", () => {
9
- it("exact values are frozen", () => {
10
- expect(EMAIL_KINDS).toStrictEqual({
11
- "auth.otp": {
12
- kind: "auth.otp",
13
- subject: "Your login code for Company Semantics",
14
- plainTextRequired: true,
15
- htmlSupported: false,
16
- },
17
- "auth.magic_link": {
18
- kind: "auth.magic_link",
19
- subject: "Your login link",
20
- plainTextRequired: true,
21
- htmlSupported: false,
22
- },
23
- "org.invite": {
24
- kind: "org.invite",
25
- subject: "You've been invited to join {orgName} on Company Semantics",
26
- plainTextRequired: true,
27
- htmlSupported: true,
28
- },
29
- "org.unit_owner_granted": {
30
- kind: "org.unit_owner_granted",
31
- subject: "You've been added as {roleWord} to {unitName} in {orgName}",
32
- plainTextRequired: true,
33
- htmlSupported: true,
34
- },
35
- "org.ownership_transfer": {
36
- kind: "org.ownership_transfer",
37
- subject: "You've been invited to become a workspace owner",
38
- plainTextRequired: true,
39
- htmlSupported: false,
40
- },
41
- "org.ownership_transfer_completed": {
42
- kind: "org.ownership_transfer_completed",
43
- subject: "Workspace ownership has been transferred",
44
- plainTextRequired: true,
45
- htmlSupported: false,
46
- },
47
- "security.alert": {
48
- kind: "security.alert",
49
- subject: "Security alert for your account",
50
- plainTextRequired: true,
51
- htmlSupported: false,
52
- },
53
- "chat.shared": {
54
- kind: "chat.shared",
55
- subject: "A chat has been shared with you",
56
- plainTextRequired: true,
57
- htmlSupported: true,
58
- },
59
- "share.granted": {
60
- kind: "share.granted",
61
- subject: "Something has been shared with you",
62
- plainTextRequired: true,
63
- htmlSupported: true,
64
- },
65
- "companyMd.access_requested": {
66
- kind: "companyMd.access_requested",
67
- subject: "Someone requested access to a document",
68
- plainTextRequired: true,
69
- htmlSupported: true,
70
- },
71
- "companyMd.access_request_approved": {
72
- kind: "companyMd.access_request_approved",
73
- subject: "Your access request was approved",
74
- plainTextRequired: true,
75
- htmlSupported: true,
76
- },
77
- "companyMd.access_request_denied": {
78
- kind: "companyMd.access_request_denied",
79
- subject: "Your access request was reviewed",
80
- plainTextRequired: true,
81
- htmlSupported: true,
82
- },
83
- });
84
- });
85
- });
86
-
87
- describe("EMAIL_KINDS registry invariants", () => {
88
- it("every registry key matches its definition.kind field", () => {
89
- for (const [key, def] of Object.entries(EMAIL_KINDS)) {
90
- expect(def.kind).toBe(key);
91
- }
92
- });
93
-
94
- it("all subjects are non-empty strings", () => {
95
- for (const def of Object.values(EMAIL_KINDS)) {
96
- expect(typeof def.subject).toBe("string");
97
- expect(def.subject.length).toBeGreaterThan(0);
98
- }
99
- });
100
-
101
- it("all entries have required boolean fields plainTextRequired and htmlSupported", () => {
102
- for (const def of Object.values(EMAIL_KINDS)) {
103
- expect(typeof def.plainTextRequired).toBe("boolean");
104
- expect(typeof def.htmlSupported).toBe("boolean");
105
- }
106
- });
107
- });
108
-
109
- describe("getEmailKindDefinition", () => {
110
- it("returns correct definition for auth.otp", () => {
111
- const def = getEmailKindDefinition("auth.otp");
112
- expect(def).toStrictEqual(EMAIL_KINDS["auth.otp"]);
113
- });
114
-
115
- it("returns correct definition for auth.magic_link", () => {
116
- const def = getEmailKindDefinition("auth.magic_link");
117
- expect(def).toStrictEqual(EMAIL_KINDS["auth.magic_link"]);
118
- });
119
-
120
- it("returns correct definition for org.invite", () => {
121
- const def = getEmailKindDefinition("org.invite");
122
- expect(def).toStrictEqual(EMAIL_KINDS["org.invite"]);
123
- });
124
-
125
- it("returns correct definition for security.alert", () => {
126
- const def = getEmailKindDefinition("security.alert");
127
- expect(def).toStrictEqual(EMAIL_KINDS["security.alert"]);
128
- });
129
-
130
- it("returns correct definition for chat.shared", () => {
131
- const def = getEmailKindDefinition("chat.shared");
132
- expect(def).toStrictEqual(EMAIL_KINDS["chat.shared"]);
133
- });
134
-
135
- it("return value matches registry entry exactly", () => {
136
- for (const [key, expected] of Object.entries(EMAIL_KINDS)) {
137
- const def = getEmailKindDefinition(key as keyof typeof EMAIL_KINDS);
138
- expect(def).toBe(expected);
139
- }
140
- });
141
- });
142
-
143
- describe("isValidEmailKind", () => {
144
- it("returns true for all valid email kinds", () => {
145
- for (const kind of Object.keys(EMAIL_KINDS)) {
146
- expect(isValidEmailKind(kind)).toBe(true);
147
- }
148
- });
149
-
150
- it("returns false for unknown string", () => {
151
- expect(isValidEmailKind("unknown.kind")).toBe(false);
152
- });
153
-
154
- it("returns false for empty string", () => {
155
- expect(isValidEmailKind("")).toBe(false);
156
- });
157
-
158
- it("returns false for partial match", () => {
159
- expect(isValidEmailKind("auth")).toBe(false);
160
- });
161
- });
@@ -1,36 +0,0 @@
1
- /**
2
- * Email Domain Barrel
3
- *
4
- * Re-exports email kind vocabulary types and registry.
5
- * Import from '@company-semantics/contracts/email'.
6
- *
7
- * @see ADR-CONT-034 for design rationale
8
- */
9
-
10
- // =============================================================================
11
- // Kind Types
12
- // =============================================================================
13
-
14
- export type { EmailKind, EmailPayloads, SendEmailInput } from "./types";
15
-
16
- // =============================================================================
17
- // Definition Types
18
- // =============================================================================
19
-
20
- export type { EmailKindDefinition } from "./registry";
21
-
22
- // =============================================================================
23
- // Registry
24
- // =============================================================================
25
-
26
- export {
27
- EMAIL_KINDS,
28
- getEmailKindDefinition,
29
- isValidEmailKind,
30
- } from "./registry";
31
-
32
- // =============================================================================
33
- // Render Layer
34
- // =============================================================================
35
-
36
- export * from "./render";
@@ -1,155 +0,0 @@
1
- /**
2
- * Email Kind Registry
3
- *
4
- * Central registry of all email kinds and their definitions.
5
- * This is the single source of truth for email metadata.
6
- *
7
- * Invariants:
8
- * - Every EmailKind MUST have an entry in EMAIL_KINDS
9
- * - Registry keys MUST match definition.kind
10
- * - Registry is exhaustive (satisfies Record<EmailKind, ...>)
11
- * - Subjects are owned here, not duplicated in templates
12
- *
13
- * @see ADR-CONT-034 for design rationale
14
- */
15
-
16
- import type { EmailKind } from "./types";
17
-
18
- // =============================================================================
19
- // Email Kind Definition
20
- // =============================================================================
21
-
22
- /**
23
- * Complete definition for an email kind.
24
- *
25
- * This interface is the schema for entries in EMAIL_KINDS registry.
26
- * It captures subject, rendering requirements, and domain metadata.
27
- *
28
- * Invariants:
29
- * - kind field MUST match the registry key
30
- * - subject is the authoritative source (templates import from here)
31
- */
32
- export interface EmailKindDefinition {
33
- /** The email kind this definition describes */
34
- kind: EmailKind;
35
- /** Email subject line (single source of truth). May contain `{field}`
36
- * placeholders that `renderEmail` fills from the payload (e.g. `{orgName}`);
37
- * an unmatched placeholder is left verbatim. */
38
- subject: string;
39
- /** Whether plain text body is required */
40
- plainTextRequired: boolean;
41
- /** Whether HTML body is supported */
42
- htmlSupported: boolean;
43
- }
44
-
45
- // =============================================================================
46
- // Registry
47
- // =============================================================================
48
-
49
- /**
50
- * EMAIL_KINDS is the authoritative registry.
51
- *
52
- * All subjects, rendering rules, and domain metadata are derived from here.
53
- * Backend email templates MUST use this registry for subjects
54
- * rather than hardcoding values.
55
- *
56
- * To add a new kind:
57
- * 1. Add to EmailKind union in types.ts
58
- * 2. Add entry to this registry
59
- * 3. Add payload to EmailPayloads if needed
60
- * 4. Implement template in backend
61
- */
62
- export const EMAIL_KINDS = {
63
- "auth.otp": {
64
- kind: "auth.otp",
65
- subject: "Your login code for Company Semantics",
66
- plainTextRequired: true,
67
- htmlSupported: false,
68
- },
69
- "auth.magic_link": {
70
- kind: "auth.magic_link",
71
- subject: "Your login link",
72
- plainTextRequired: true,
73
- htmlSupported: false,
74
- },
75
- "org.invite": {
76
- kind: "org.invite",
77
- subject: "You've been invited to join {orgName} on Company Semantics",
78
- plainTextRequired: true,
79
- htmlSupported: true,
80
- },
81
- "org.unit_owner_granted": {
82
- kind: "org.unit_owner_granted",
83
- subject: "You've been added as {roleWord} to {unitName} in {orgName}",
84
- plainTextRequired: true,
85
- htmlSupported: true,
86
- },
87
- "org.ownership_transfer": {
88
- kind: "org.ownership_transfer",
89
- subject: "You've been invited to become a workspace owner",
90
- plainTextRequired: true,
91
- htmlSupported: false,
92
- },
93
- "org.ownership_transfer_completed": {
94
- kind: "org.ownership_transfer_completed",
95
- subject: "Workspace ownership has been transferred",
96
- plainTextRequired: true,
97
- htmlSupported: false,
98
- },
99
- "security.alert": {
100
- kind: "security.alert",
101
- subject: "Security alert for your account",
102
- plainTextRequired: true,
103
- htmlSupported: false,
104
- },
105
- "chat.shared": {
106
- kind: "chat.shared",
107
- subject: "A chat has been shared with you",
108
- plainTextRequired: true,
109
- htmlSupported: true,
110
- },
111
- "share.granted": {
112
- kind: "share.granted",
113
- subject: "Something has been shared with you",
114
- plainTextRequired: true,
115
- htmlSupported: true,
116
- },
117
- "companyMd.access_requested": {
118
- kind: "companyMd.access_requested",
119
- subject: "Someone requested access to a document",
120
- plainTextRequired: true,
121
- htmlSupported: true,
122
- },
123
- "companyMd.access_request_approved": {
124
- kind: "companyMd.access_request_approved",
125
- subject: "Your access request was approved",
126
- plainTextRequired: true,
127
- htmlSupported: true,
128
- },
129
- "companyMd.access_request_denied": {
130
- kind: "companyMd.access_request_denied",
131
- subject: "Your access request was reviewed",
132
- plainTextRequired: true,
133
- htmlSupported: true,
134
- },
135
- } as const satisfies Record<EmailKind, EmailKindDefinition>;
136
-
137
- // =============================================================================
138
- // Registry Helpers
139
- // =============================================================================
140
-
141
- /**
142
- * Type-safe registry lookup.
143
- * Returns the definition for a given email kind.
144
- */
145
- export function getEmailKindDefinition(kind: EmailKind): EmailKindDefinition {
146
- return EMAIL_KINDS[kind];
147
- }
148
-
149
- /**
150
- * Check if a string is a valid EmailKind.
151
- * Use at API boundaries to reject unknown kinds.
152
- */
153
- export function isValidEmailKind(kind: string): kind is EmailKind {
154
- return kind in EMAIL_KINDS;
155
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * Auth OTP email (login code).
3
- *
4
- * INVARIANT: the OTP value is a runtime payload field — never a literal in this
5
- * package. Request metadata (IP / user agent) is gated by the caller-supplied
6
- * `includeRequestMetadata` option (backend reads its env flag and passes it).
7
- */
8
-
9
- import type { EmailPayloads } from "../types";
10
-
11
- import {
12
- type Block,
13
- footer,
14
- greeting,
15
- keyValue,
16
- paragraph,
17
- signature,
18
- } from "./blocks";
19
- import { chatAssistant, chatCta, chatUnit } from "./chat";
20
- import { COMPANY_NAME } from "./constants";
21
-
22
- export type AuthOtpPayload = EmailPayloads["auth.otp"];
23
-
24
- export interface RenderOptions {
25
- /** Include request IP / device details (PII; opt-in). */
26
- includeRequestMetadata?: boolean;
27
- }
28
-
29
- export function renderAuthOtp(
30
- payload: AuthOtpPayload,
31
- options?: RenderOptions,
32
- ): Block[] {
33
- const { otp, expiresInMinutes, requestIp, userAgent } = payload;
34
-
35
- const blocks: Block[] = [
36
- greeting(),
37
- paragraph("A login code was requested."),
38
- chatUnit(
39
- chatAssistant("Copy + paste this code in the login form."),
40
- chatCta({ label: otp }),
41
- ),
42
- keyValue("Status", "Valid"),
43
- keyValue(
44
- "Expires in",
45
- `${expiresInMinutes} ${expiresInMinutes === 1 ? "minute" : "minutes"}`,
46
- "normal",
47
- ),
48
- ];
49
-
50
- // PRIVACY: IP address is PII under GDPR, so this is opt-in.
51
- if (options?.includeRequestMetadata && (requestIp || userAgent)) {
52
- blocks.push(paragraph("Request details:", "tight"));
53
- if (requestIp) blocks.push(keyValue("IP address", requestIp));
54
- if (userAgent) {
55
- const truncated =
56
- userAgent.length > 80 ? userAgent.slice(0, 77) + "..." : userAgent;
57
- blocks.push(keyValue("Device", truncated, "normal"));
58
- }
59
- }
60
-
61
- blocks.push(
62
- footer(
63
- `This login code was sent via ${COMPANY_NAME}.`,
64
- "If this wasn't you, no action is required.",
65
- "none",
66
- ),
67
- signature(),
68
- );
69
-
70
- return blocks;
71
- }