@assistant-ui/react-generative-ui 0.0.7 → 0.0.9

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 (181) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +0 -1
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.server.d.ts +0 -1
  4. package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
  5. package/dist/JSONGenerativeUI.shared.d.ts +0 -1
  6. package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
  7. package/dist/actionRegistry.d.ts +2 -11
  8. package/dist/actionRegistry.d.ts.map +1 -1
  9. package/dist/actionRegistry.js.map +1 -1
  10. package/dist/buildPresentParameters.d.ts +5 -5
  11. package/dist/buildPresentParameters.d.ts.map +1 -1
  12. package/dist/buildPresentParameters.js +10 -5
  13. package/dist/buildPresentParameters.js.map +1 -1
  14. package/dist/constants.d.ts.map +1 -1
  15. package/dist/defineGenerativeComponents.d.ts +0 -1
  16. package/dist/defineGenerativeComponents.d.ts.map +1 -1
  17. package/dist/generativeUIToJSX.d.ts +15 -10
  18. package/dist/generativeUIToJSX.d.ts.map +1 -1
  19. package/dist/generativeUIToJSX.js +47 -17
  20. package/dist/generativeUIToJSX.js.map +1 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +2 -2
  23. package/dist/index.server.d.ts +4 -3
  24. package/dist/index.server.js +2 -2
  25. package/dist/index.shared.d.ts +3 -3
  26. package/dist/index.shared.js +2 -2
  27. package/dist/ir.d.ts +6 -6
  28. package/dist/ir.d.ts.map +1 -1
  29. package/dist/ir.js +27 -1
  30. package/dist/ir.js.map +1 -1
  31. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
  32. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
  33. package/dist/renderGenerativeUI.d.ts +0 -1
  34. package/dist/renderGenerativeUI.d.ts.map +1 -1
  35. package/dist/slack/constants.d.ts +111 -0
  36. package/dist/slack/constants.d.ts.map +1 -0
  37. package/dist/slack/constants.js +162 -0
  38. package/dist/slack/constants.js.map +1 -0
  39. package/dist/slack/decodeBlockAction.d.ts +11 -0
  40. package/dist/slack/decodeBlockAction.d.ts.map +1 -0
  41. package/dist/slack/decodeBlockAction.js +40 -0
  42. package/dist/slack/decodeBlockAction.js.map +1 -0
  43. package/dist/slack/fromSlackBlocks.d.ts +30 -0
  44. package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
  45. package/dist/slack/fromSlackBlocks.js +447 -0
  46. package/dist/slack/fromSlackBlocks.js.map +1 -0
  47. package/dist/slack/toSlackBlocks.d.ts +7 -0
  48. package/dist/slack/toSlackBlocks.d.ts.map +1 -0
  49. package/dist/slack/toSlackBlocks.js +649 -0
  50. package/dist/slack/toSlackBlocks.js.map +1 -0
  51. package/dist/slack/types.d.ts +178 -0
  52. package/dist/slack/types.d.ts.map +1 -0
  53. package/dist/slack/types.js +0 -0
  54. package/dist/slack.d.ts +5 -0
  55. package/dist/slack.js +4 -0
  56. package/dist/teams/boundSpec.d.ts +20 -0
  57. package/dist/teams/boundSpec.d.ts.map +1 -0
  58. package/dist/teams/boundSpec.js +67 -0
  59. package/dist/teams/boundSpec.js.map +1 -0
  60. package/dist/teams/constants.d.ts +57 -0
  61. package/dist/teams/constants.d.ts.map +1 -0
  62. package/dist/teams/constants.js +92 -0
  63. package/dist/teams/constants.js.map +1 -0
  64. package/dist/teams/decodeSubmitData.d.ts +25 -0
  65. package/dist/teams/decodeSubmitData.d.ts.map +1 -0
  66. package/dist/teams/decodeSubmitData.js +44 -0
  67. package/dist/teams/decodeSubmitData.js.map +1 -0
  68. package/dist/teams/toAdaptiveCard.d.ts +39 -0
  69. package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
  70. package/dist/teams/toAdaptiveCard.js +496 -0
  71. package/dist/teams/toAdaptiveCard.js.map +1 -0
  72. package/dist/teams/toTeamsAttachments.d.ts +13 -0
  73. package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
  74. package/dist/teams/toTeamsAttachments.js +95 -0
  75. package/dist/teams/toTeamsAttachments.js.map +1 -0
  76. package/dist/teams/types.d.ts +196 -0
  77. package/dist/teams/types.d.ts.map +1 -0
  78. package/dist/teams/types.js +0 -0
  79. package/dist/teams.d.ts +5 -0
  80. package/dist/teams.js +4 -0
  81. package/dist/types.d.ts +7 -4
  82. package/dist/types.d.ts.map +1 -1
  83. package/dist/vocabulary/alert.d.ts +6 -12
  84. package/dist/vocabulary/alert.d.ts.map +1 -1
  85. package/dist/vocabulary/alert.js +13 -4
  86. package/dist/vocabulary/alert.js.map +1 -1
  87. package/dist/vocabulary/collectFormValues.d.ts +23 -0
  88. package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
  89. package/dist/vocabulary/collectFormValues.js +32 -0
  90. package/dist/vocabulary/collectFormValues.js.map +1 -0
  91. package/dist/vocabulary/data.d.ts +16 -17
  92. package/dist/vocabulary/data.d.ts.map +1 -1
  93. package/dist/vocabulary/data.js +244 -13
  94. package/dist/vocabulary/data.js.map +1 -1
  95. package/dist/vocabulary/dispatch.d.ts +11 -0
  96. package/dist/vocabulary/dispatch.d.ts.map +1 -0
  97. package/dist/vocabulary/dispatch.js +21 -0
  98. package/dist/vocabulary/dispatch.js.map +1 -0
  99. package/dist/vocabulary/fact.d.ts +1 -6
  100. package/dist/vocabulary/fact.d.ts.map +1 -1
  101. package/dist/vocabulary/fact.js.map +1 -1
  102. package/dist/vocabulary/form.d.ts +14 -0
  103. package/dist/vocabulary/form.d.ts.map +1 -0
  104. package/dist/vocabulary/form.js +23 -0
  105. package/dist/vocabulary/form.js.map +1 -0
  106. package/dist/vocabulary/icon.d.ts +44 -0
  107. package/dist/vocabulary/icon.d.ts.map +1 -0
  108. package/dist/vocabulary/icon.js +272 -0
  109. package/dist/vocabulary/icon.js.map +1 -0
  110. package/dist/vocabulary/index.d.ts +0 -1
  111. package/dist/vocabulary/index.d.ts.map +1 -1
  112. package/dist/vocabulary/index.js +7 -1
  113. package/dist/vocabulary/index.js.map +1 -1
  114. package/dist/vocabulary/interactive.d.ts +45 -33
  115. package/dist/vocabulary/interactive.d.ts.map +1 -1
  116. package/dist/vocabulary/interactive.js +78 -29
  117. package/dist/vocabulary/interactive.js.map +1 -1
  118. package/dist/vocabulary/layout.d.ts +27 -23
  119. package/dist/vocabulary/layout.d.ts.map +1 -1
  120. package/dist/vocabulary/layout.js +78 -14
  121. package/dist/vocabulary/layout.js.map +1 -1
  122. package/dist/vocabulary/list.d.ts +17 -0
  123. package/dist/vocabulary/list.d.ts.map +1 -0
  124. package/dist/vocabulary/list.js +48 -0
  125. package/dist/vocabulary/list.js.map +1 -0
  126. package/dist/vocabulary/media.d.ts +3 -9
  127. package/dist/vocabulary/media.d.ts.map +1 -1
  128. package/dist/vocabulary/media.js +17 -7
  129. package/dist/vocabulary/media.js.map +1 -1
  130. package/dist/vocabulary/text.d.ts +3 -17
  131. package/dist/vocabulary/text.d.ts.map +1 -1
  132. package/dist/vocabulary/text.js.map +1 -1
  133. package/package.json +15 -7
  134. package/src/actionRegistry.ts +2 -10
  135. package/src/buildPresentParameters.ts +15 -9
  136. package/src/generativeUIToJSX.test.ts +186 -0
  137. package/src/generativeUIToJSX.ts +112 -21
  138. package/src/index.shared.ts +3 -0
  139. package/src/ir.ts +31 -5
  140. package/src/renderGenerativeUI.test.tsx +16 -2
  141. package/src/slack/constants.ts +212 -0
  142. package/src/slack/decodeBlockAction.test.ts +135 -0
  143. package/src/slack/decodeBlockAction.ts +66 -0
  144. package/src/slack/fromSlackBlocks.test.ts +1145 -0
  145. package/src/slack/fromSlackBlocks.ts +709 -0
  146. package/src/slack/toSlackBlocks.test.ts +1936 -0
  147. package/src/slack/toSlackBlocks.ts +1319 -0
  148. package/src/slack/types.ts +230 -0
  149. package/src/slack.ts +37 -0
  150. package/src/teams/boundSpec.ts +102 -0
  151. package/src/teams/constants.ts +127 -0
  152. package/src/teams/decodeSubmitData.test.ts +148 -0
  153. package/src/teams/decodeSubmitData.ts +57 -0
  154. package/src/teams/toAdaptiveCard.test.ts +1410 -0
  155. package/src/teams/toAdaptiveCard.ts +762 -0
  156. package/src/teams/toTeamsAttachments.test.ts +29 -0
  157. package/src/teams/toTeamsAttachments.ts +137 -0
  158. package/src/teams/types.ts +237 -0
  159. package/src/teams.ts +34 -0
  160. package/src/vocabulary/alert.test.tsx +72 -2
  161. package/src/vocabulary/alert.tsx +24 -5
  162. package/src/vocabulary/collectFormValues.test.ts +127 -0
  163. package/src/vocabulary/collectFormValues.ts +58 -0
  164. package/src/vocabulary/data.test.tsx +503 -3
  165. package/src/vocabulary/data.tsx +426 -18
  166. package/src/vocabulary/dispatch.ts +22 -0
  167. package/src/vocabulary/form.test.tsx +112 -0
  168. package/src/vocabulary/form.tsx +34 -0
  169. package/src/vocabulary/icon.test.tsx +64 -0
  170. package/src/vocabulary/icon.tsx +174 -0
  171. package/src/vocabulary/index.test.ts +7 -0
  172. package/src/vocabulary/index.ts +6 -0
  173. package/src/vocabulary/interactive.dispatch.test.tsx +243 -1
  174. package/src/vocabulary/interactive.test.tsx +143 -3
  175. package/src/vocabulary/interactive.tsx +134 -30
  176. package/src/vocabulary/layout.test.tsx +231 -3
  177. package/src/vocabulary/layout.tsx +168 -15
  178. package/src/vocabulary/list.test.tsx +195 -0
  179. package/src/vocabulary/list.tsx +48 -0
  180. package/src/vocabulary/media.test.tsx +36 -4
  181. package/src/vocabulary/media.tsx +27 -8
@@ -0,0 +1,212 @@
1
+ import type {
2
+ SlackAlertBlock,
3
+ SlackButtonElement,
4
+ SlackCardBlock,
5
+ SlackCarouselBlock,
6
+ SlackContextBlock,
7
+ SlackDataTableBlock,
8
+ SlackDataTableCell,
9
+ SlackImageBlock,
10
+ SlackSectionBlock,
11
+ SlackTextObject,
12
+ } from "./types";
13
+
14
+ /** The maximum normalized traversal depth. */
15
+ export const MAX_TRAVERSAL_DEPTH = 64;
16
+
17
+ /**
18
+ * The maximum number of entries kept at any single level (root, or a
19
+ * `children` array at any depth) of the raw spec, before it ever reaches
20
+ * `normalizeSpec`. Bounds a hostile array's reported `length` so the pre-pass
21
+ * can never be made to walk further than this regardless of what the array
22
+ * claims about itself.
23
+ */
24
+ export const CHILDREN_CAP = 200;
25
+
26
+ /**
27
+ * The total number of nodes the pre-normalization bounding walk will visit
28
+ * across one call, regardless of how many times a shared reference recurs.
29
+ * Bounds the combinatorial work a DAG of shared or self-referential nodes
30
+ * would otherwise force even though each individual array stays within
31
+ * {@link CHILDREN_CAP}.
32
+ */
33
+ export const NODE_BUDGET = 5000;
34
+
35
+ /** The block limit for a Slack message. */
36
+ export const MESSAGE_BLOCK_CAP = 50;
37
+
38
+ /** The block limit for a Slack modal. */
39
+ export const MODAL_BLOCK_CAP = 100;
40
+
41
+ /**
42
+ * The maximum number of top-level blocks accepted when parsing Slack Block
43
+ * Kit JSON back into generative UI, guarding against a hostile or malformed
44
+ * payload.
45
+ */
46
+ export const INBOUND_BLOCK_CAP = 200;
47
+
48
+ /** The character limit for header text. */
49
+ export const HEADER_TEXT_CAP = 150;
50
+
51
+ /** The character limit for section text. */
52
+ export const SECTION_TEXT_CAP = 3000;
53
+
54
+ /** The maximum number of fields in one section. */
55
+ export const FACT_FIELD_CAP = 10;
56
+
57
+ /** The character limit for one section field. */
58
+ export const FACT_FIELD_TEXT_CAP = 2000;
59
+
60
+ /** The character limit for button labels and option labels. */
61
+ export const INTERACTIVE_TEXT_CAP = 75;
62
+
63
+ /** The character limit for an action_id. */
64
+ export const ACTION_ID_CAP = 255;
65
+
66
+ /** The character limit for a button's serialized value payload. */
67
+ export const BUTTON_VALUE_CAP = 2000;
68
+
69
+ /** The maximum number of elements in one actions block. */
70
+ export const ACTIONS_ELEMENT_CAP = 25;
71
+
72
+ /** The maximum number of options in a static select. */
73
+ export const SELECT_OPTION_CAP = 100;
74
+
75
+ /** The maximum number of options in a radio-button group. */
76
+ export const RADIO_OPTION_CAP = 10;
77
+
78
+ /** The character limit for a select or input placeholder. */
79
+ export const PLACEHOLDER_TEXT_CAP = 150;
80
+
81
+ /** The character limit for an input's label. */
82
+ export const INPUT_LABEL_CAP = 2000;
83
+
84
+ /** The maximum number of elements in one context block. */
85
+ export const CONTEXT_ELEMENT_CAP = 10;
86
+
87
+ /** The character limit for one context element's text. */
88
+ export const CONTEXT_TEXT_CAP = 3000;
89
+
90
+ /** The character limit for card titles and subtitles. */
91
+ export const CARD_TITLE_CAP = 150;
92
+
93
+ /** The character limit for a card's body text. */
94
+ export const CARD_BODY_CAP = 200;
95
+
96
+ /** The character limit for a card's subtext. */
97
+ export const CARD_SUBTEXT_CAP = 200;
98
+
99
+ /** The maximum number of buttons in a card's actions. */
100
+ export const CARD_ACTIONS_CAP = 3;
101
+
102
+ /** The minimum number of elements in a renderable carousel. */
103
+ export const CAROUSEL_CARD_MIN = 1;
104
+
105
+ /** The maximum number of elements in a carousel. */
106
+ export const CAROUSEL_CARD_CAP = 10;
107
+
108
+ /** The character limit for an alert's text. */
109
+ export const ALERT_TEXT_CAP = 200;
110
+
111
+ /**
112
+ * The maximum number of data rows in a data-table block; the emitted `rows`
113
+ * array additionally carries one header row.
114
+ */
115
+ export const DATA_TABLE_ROW_CAP = 100;
116
+
117
+ /** The maximum number of columns in a data-table block. */
118
+ export const DATA_TABLE_COLUMN_CAP = 20;
119
+
120
+ /**
121
+ * The cumulative character limit (summed across every cell's text) shared by
122
+ * every data-table block in one payload.
123
+ */
124
+ export const DATA_TABLE_CHAR_BUDGET = 10000;
125
+
126
+ /** The caption emitted on every native data-table block. */
127
+ export const TABLE_CAPTION = "Table";
128
+
129
+ /** The cumulative character limit for native markdown blocks. */
130
+ export const MARKDOWN_TEXT_BUDGET = 12000;
131
+
132
+ /** Builds the message-surface fallback for an alert. */
133
+ export function buildAlertFallback(
134
+ title: string,
135
+ description: string,
136
+ tone: "info" | "success" | "warning" | "danger",
137
+ ): readonly [SlackContextBlock, SlackSectionBlock] {
138
+ const emoji = {
139
+ info: "ℹ️",
140
+ success: "✅",
141
+ warning: "⚠️",
142
+ danger: "🛑",
143
+ }[tone];
144
+ return [
145
+ {
146
+ type: "context",
147
+ elements: [
148
+ { type: "mrkdwn", text: title ? `${emoji} *${title}*` : emoji },
149
+ ],
150
+ },
151
+ { type: "section", text: { type: "mrkdwn", text: description } },
152
+ ];
153
+ }
154
+
155
+ /** Builds a 2026 Slack modal-only alert block, mapping `tone` to its `level`. */
156
+ export function buildAlertBlock(
157
+ text: string,
158
+ tone: "info" | "success" | "warning" | "danger",
159
+ ): SlackAlertBlock {
160
+ const level = tone === "danger" ? "error" : tone;
161
+ return { type: "alert", text: { type: "mrkdwn", text }, level };
162
+ }
163
+
164
+ type CardBlockFields = {
165
+ readonly heroImage?: SlackImageBlock;
166
+ readonly title?: SlackTextObject;
167
+ readonly subtitle?: SlackTextObject;
168
+ readonly body?: SlackTextObject;
169
+ readonly subtext?: SlackTextObject;
170
+ readonly actions?: readonly SlackButtonElement[];
171
+ };
172
+
173
+ /** Builds a 2026 Slack card block from its already-clamped fields. */
174
+ export function buildCardBlock(fields: CardBlockFields): SlackCardBlock {
175
+ const { heroImage, title, subtitle, body, subtext, actions } = fields;
176
+ return {
177
+ type: "card",
178
+ ...(heroImage !== undefined ? { hero_image: heroImage } : {}),
179
+ ...(title !== undefined ? { title } : {}),
180
+ ...(subtitle !== undefined ? { subtitle } : {}),
181
+ ...(body !== undefined ? { body } : {}),
182
+ ...(subtext !== undefined ? { subtext } : {}),
183
+ ...(actions !== undefined ? { actions } : {}),
184
+ };
185
+ }
186
+
187
+ /** Builds a 2026 Slack carousel block. */
188
+ export function buildCarouselBlock(
189
+ elements: readonly SlackCardBlock[],
190
+ ): SlackCarouselBlock {
191
+ return { type: "carousel", elements };
192
+ }
193
+
194
+ /** Builds a 2026 Slack data-table block; `rows[0]` must be the header row. */
195
+ export function buildDataTableBlock(
196
+ rows: readonly (readonly SlackDataTableCell[])[],
197
+ ): SlackDataTableBlock {
198
+ return { type: "data_table", caption: TABLE_CAPTION, rows };
199
+ }
200
+
201
+ /** Builds the final note used when a surface block budget is exceeded. */
202
+ export function buildBlockBudgetNote(omitted: number): SlackContextBlock {
203
+ return {
204
+ type: "context",
205
+ elements: [
206
+ {
207
+ type: "mrkdwn",
208
+ text: `${omitted} ${omitted === 1 ? "block was" : "blocks were"} omitted.`,
209
+ },
210
+ ],
211
+ };
212
+ }
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { decodeBlockAction } from "./decodeBlockAction";
3
+
4
+ describe("decodeBlockAction", () => {
5
+ it("decodes a button action, spreading its JSON value into the payload", () => {
6
+ const action = {
7
+ action_id: "buy_item",
8
+ value: JSON.stringify({ sku: "abc123", qty: 2 }),
9
+ };
10
+ expect(decodeBlockAction(action)).toEqual({
11
+ type: "buy_item",
12
+ sku: "abc123",
13
+ qty: 2,
14
+ });
15
+ });
16
+
17
+ it("decodes a static_select action, reading $input from selected_option.value", () => {
18
+ const action = {
19
+ action_id: "choose_color",
20
+ selected_option: {
21
+ value: "red",
22
+ text: { type: "plain_text", text: "Red" },
23
+ },
24
+ };
25
+ expect(decodeBlockAction(action)).toEqual({
26
+ type: "choose_color",
27
+ $input: "red",
28
+ });
29
+ });
30
+
31
+ it("decodes a datepicker action, reading $input from selected_date", () => {
32
+ const action = { action_id: "pick_date", selected_date: "2026-07-15" };
33
+ expect(decodeBlockAction(action)).toEqual({
34
+ type: "pick_date",
35
+ $input: "2026-07-15",
36
+ });
37
+ });
38
+
39
+ it("decodes a checkboxes action, reading $input from selected_options[].value", () => {
40
+ const action = {
41
+ action_id: "toggle_opts",
42
+ selected_options: [{ value: "a" }, { value: "b" }],
43
+ };
44
+ expect(decodeBlockAction(action)).toEqual({
45
+ type: "toggle_opts",
46
+ $input: ["a", "b"],
47
+ });
48
+ });
49
+
50
+ it("decodes a radio_buttons action, reading $input from selected_option.value", () => {
51
+ const action = {
52
+ action_id: "select_plan",
53
+ selected_option: { value: "pro" },
54
+ };
55
+ expect(decodeBlockAction(action)).toEqual({
56
+ type: "select_plan",
57
+ $input: "pro",
58
+ });
59
+ });
60
+
61
+ it("treats a value that parses to a non-object JSON literal as the raw-string $input instead of swallowing it", () => {
62
+ for (const value of ["42", "true", "null", "[1]"]) {
63
+ expect(decodeBlockAction({ action_id: "note", value })).toEqual({
64
+ type: "note",
65
+ $input: value,
66
+ });
67
+ }
68
+ });
69
+
70
+ it("uses the raw value as $input when it is not JSON", () => {
71
+ const action = { action_id: "leave_note", value: "hello world" };
72
+ expect(decodeBlockAction(action)).toEqual({
73
+ type: "leave_note",
74
+ $input: "hello world",
75
+ });
76
+ });
77
+
78
+ it("spreads a JSON value into the payload and separately captures the runtime selection as $input", () => {
79
+ const action = {
80
+ action_id: "combo_case",
81
+ value: JSON.stringify({ context: "c1" }),
82
+ selected_option: { value: "x" },
83
+ };
84
+ expect(decodeBlockAction(action)).toEqual({
85
+ type: "combo_case",
86
+ context: "c1",
87
+ $input: "x",
88
+ });
89
+ });
90
+
91
+ it("drops a $input key inside the JSON value payload when nothing was selected", () => {
92
+ const action = {
93
+ action_id: "spoof_case",
94
+ value: JSON.stringify({ context: "c1", $input: "spoofed" }),
95
+ };
96
+ const decoded = decodeBlockAction(action);
97
+ expect(decoded).toEqual({ type: "spoof_case", context: "c1" });
98
+ expect(decoded !== undefined && "$input" in decoded).toBe(false);
99
+ });
100
+
101
+ it("drops a payload $input in favor of the runtime selection", () => {
102
+ const action = {
103
+ action_id: "spoof_case",
104
+ value: JSON.stringify({ $input: "spoofed" }),
105
+ selected_option: { value: "x" },
106
+ };
107
+ expect(decodeBlockAction(action)).toEqual({
108
+ type: "spoof_case",
109
+ $input: "x",
110
+ });
111
+ });
112
+
113
+ it("ignores a non-string value without throwing", () => {
114
+ expect(decodeBlockAction({ action_id: "weird", value: 42 })).toEqual({
115
+ type: "weird",
116
+ });
117
+ });
118
+
119
+ it("returns undefined for malformed or non-actionable input", () => {
120
+ const malformed: unknown[] = [
121
+ null,
122
+ undefined,
123
+ 42,
124
+ "just a string",
125
+ true,
126
+ [],
127
+ {},
128
+ { action_id: 123 },
129
+ { action_id: "" },
130
+ ];
131
+ for (const input of malformed) {
132
+ expect(decodeBlockAction(input)).toBeUndefined();
133
+ }
134
+ });
135
+ });
@@ -0,0 +1,66 @@
1
+ import type { Action } from "../ir";
2
+
3
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
4
+ typeof value === "object" && value !== null && !Array.isArray(value);
5
+
6
+ const optionValue = (value: unknown): string | undefined =>
7
+ isRecord(value) && typeof value["value"] === "string"
8
+ ? value["value"]
9
+ : undefined;
10
+
11
+ /**
12
+ * Decodes one structural entry from a Slack `block_actions` payload.
13
+ * `$input` is reserved for the runtime selection: a `$input` key inside the
14
+ * button's JSON `value` payload is dropped rather than spread into the result.
15
+ */
16
+ export function decodeBlockAction(action: unknown): Action | undefined {
17
+ try {
18
+ if (!isRecord(action) || typeof action["action_id"] !== "string") {
19
+ return undefined;
20
+ }
21
+
22
+ const actionId = action["action_id"];
23
+ if (!actionId) return undefined;
24
+
25
+ const rawValue = action["value"];
26
+ let payload: Record<string, unknown> = {};
27
+ let plainValue: string | undefined;
28
+ if (typeof rawValue === "string") {
29
+ try {
30
+ const parsed: unknown = JSON.parse(rawValue);
31
+ if (isRecord(parsed)) {
32
+ payload = parsed;
33
+ } else {
34
+ plainValue = rawValue;
35
+ }
36
+ } catch {
37
+ plainValue = rawValue;
38
+ }
39
+ }
40
+
41
+ const selectedOption = optionValue(action["selected_option"]);
42
+ const selectedDate =
43
+ typeof action["selected_date"] === "string"
44
+ ? action["selected_date"]
45
+ : undefined;
46
+ const selectedOptions = Array.isArray(action["selected_options"])
47
+ ? action["selected_options"]
48
+ .map(optionValue)
49
+ .filter((value): value is string => value !== undefined)
50
+ : undefined;
51
+ const input =
52
+ selectedOption ??
53
+ selectedDate ??
54
+ (selectedOptions !== undefined ? selectedOptions : plainValue);
55
+
56
+ return {
57
+ ...Object.fromEntries(
58
+ Object.entries(payload).filter(([key]) => key !== "$input"),
59
+ ),
60
+ type: actionId,
61
+ ...(input !== undefined ? { $input: input } : {}),
62
+ };
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ }