@assistant-ui/react-generative-ui 0.0.6 → 0.0.8

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 (201) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +4 -2
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +9 -2
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/JSONGenerativeUI.server.d.ts +0 -1
  6. package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
  7. package/dist/JSONGenerativeUI.shared.d.ts +8 -1
  8. package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
  9. package/dist/JSONGenerativeUI.shared.js.map +1 -1
  10. package/dist/actionRegistry.d.ts +32 -0
  11. package/dist/actionRegistry.d.ts.map +1 -0
  12. package/dist/actionRegistry.js +41 -0
  13. package/dist/actionRegistry.js.map +1 -0
  14. package/dist/buildPresentParameters.d.ts +5 -5
  15. package/dist/buildPresentParameters.d.ts.map +1 -1
  16. package/dist/buildPresentParameters.js +20 -6
  17. package/dist/buildPresentParameters.js.map +1 -1
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/defineGenerativeComponents.d.ts +0 -1
  20. package/dist/defineGenerativeComponents.d.ts.map +1 -1
  21. package/dist/generativeUIToJSX.d.ts +15 -10
  22. package/dist/generativeUIToJSX.d.ts.map +1 -1
  23. package/dist/generativeUIToJSX.js +47 -17
  24. package/dist/generativeUIToJSX.js.map +1 -1
  25. package/dist/index.d.ts +8 -4
  26. package/dist/index.js +5 -2
  27. package/dist/index.server.d.ts +13 -0
  28. package/dist/index.server.js +10 -0
  29. package/dist/index.shared.d.ts +11 -0
  30. package/dist/index.shared.js +9 -0
  31. package/dist/ir.d.ts +115 -0
  32. package/dist/ir.d.ts.map +1 -0
  33. package/dist/ir.js +175 -0
  34. package/dist/ir.js.map +1 -0
  35. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
  36. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
  37. package/dist/renderGenerativeUI.d.ts +5 -5
  38. package/dist/renderGenerativeUI.d.ts.map +1 -1
  39. package/dist/renderGenerativeUI.js +18 -45
  40. package/dist/renderGenerativeUI.js.map +1 -1
  41. package/dist/slack/constants.d.ts +111 -0
  42. package/dist/slack/constants.d.ts.map +1 -0
  43. package/dist/slack/constants.js +168 -0
  44. package/dist/slack/constants.js.map +1 -0
  45. package/dist/slack/decodeBlockAction.d.ts +11 -0
  46. package/dist/slack/decodeBlockAction.d.ts.map +1 -0
  47. package/dist/slack/decodeBlockAction.js +40 -0
  48. package/dist/slack/decodeBlockAction.js.map +1 -0
  49. package/dist/slack/fromSlackBlocks.d.ts +30 -0
  50. package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
  51. package/dist/slack/fromSlackBlocks.js +447 -0
  52. package/dist/slack/fromSlackBlocks.js.map +1 -0
  53. package/dist/slack/toSlackBlocks.d.ts +7 -0
  54. package/dist/slack/toSlackBlocks.d.ts.map +1 -0
  55. package/dist/slack/toSlackBlocks.js +649 -0
  56. package/dist/slack/toSlackBlocks.js.map +1 -0
  57. package/dist/slack/types.d.ts +178 -0
  58. package/dist/slack/types.d.ts.map +1 -0
  59. package/dist/slack/types.js +0 -0
  60. package/dist/slack.d.ts +5 -0
  61. package/dist/slack.js +4 -0
  62. package/dist/teams/boundSpec.d.ts +20 -0
  63. package/dist/teams/boundSpec.d.ts.map +1 -0
  64. package/dist/teams/boundSpec.js +67 -0
  65. package/dist/teams/boundSpec.js.map +1 -0
  66. package/dist/teams/constants.d.ts +57 -0
  67. package/dist/teams/constants.d.ts.map +1 -0
  68. package/dist/teams/constants.js +92 -0
  69. package/dist/teams/constants.js.map +1 -0
  70. package/dist/teams/decodeSubmitData.d.ts +25 -0
  71. package/dist/teams/decodeSubmitData.d.ts.map +1 -0
  72. package/dist/teams/decodeSubmitData.js +44 -0
  73. package/dist/teams/decodeSubmitData.js.map +1 -0
  74. package/dist/teams/toAdaptiveCard.d.ts +39 -0
  75. package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
  76. package/dist/teams/toAdaptiveCard.js +496 -0
  77. package/dist/teams/toAdaptiveCard.js.map +1 -0
  78. package/dist/teams/toTeamsAttachments.d.ts +13 -0
  79. package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
  80. package/dist/teams/toTeamsAttachments.js +95 -0
  81. package/dist/teams/toTeamsAttachments.js.map +1 -0
  82. package/dist/teams/types.d.ts +196 -0
  83. package/dist/teams/types.d.ts.map +1 -0
  84. package/dist/teams/types.js +0 -0
  85. package/dist/teams.d.ts +5 -0
  86. package/dist/teams.js +4 -0
  87. package/dist/types.d.ts +41 -34
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/vocabulary/alert.d.ts +28 -0
  90. package/dist/vocabulary/alert.d.ts.map +1 -0
  91. package/dist/vocabulary/alert.js +58 -0
  92. package/dist/vocabulary/alert.js.map +1 -0
  93. package/dist/vocabulary/collectFormValues.d.ts +23 -0
  94. package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
  95. package/dist/vocabulary/collectFormValues.js +32 -0
  96. package/dist/vocabulary/collectFormValues.js.map +1 -0
  97. package/dist/vocabulary/data.d.ts +52 -0
  98. package/dist/vocabulary/data.d.ts.map +1 -0
  99. package/dist/vocabulary/data.js +294 -0
  100. package/dist/vocabulary/data.js.map +1 -0
  101. package/dist/vocabulary/dispatch.d.ts +11 -0
  102. package/dist/vocabulary/dispatch.d.ts.map +1 -0
  103. package/dist/vocabulary/dispatch.js +21 -0
  104. package/dist/vocabulary/dispatch.js.map +1 -0
  105. package/dist/vocabulary/fact.d.ts +15 -0
  106. package/dist/vocabulary/fact.d.ts.map +1 -0
  107. package/dist/vocabulary/fact.js +24 -0
  108. package/dist/vocabulary/fact.js.map +1 -0
  109. package/dist/vocabulary/form.d.ts +14 -0
  110. package/dist/vocabulary/form.d.ts.map +1 -0
  111. package/dist/vocabulary/form.js +23 -0
  112. package/dist/vocabulary/form.js.map +1 -0
  113. package/dist/vocabulary/icon.d.ts +44 -0
  114. package/dist/vocabulary/icon.d.ts.map +1 -0
  115. package/dist/vocabulary/icon.js +272 -0
  116. package/dist/vocabulary/icon.js.map +1 -0
  117. package/dist/vocabulary/index.d.ts +15 -0
  118. package/dist/vocabulary/index.d.ts.map +1 -0
  119. package/dist/vocabulary/index.js +36 -0
  120. package/dist/vocabulary/index.js.map +1 -0
  121. package/dist/vocabulary/interactive.d.ts +93 -0
  122. package/dist/vocabulary/interactive.d.ts.map +1 -0
  123. package/dist/vocabulary/interactive.js +172 -0
  124. package/dist/vocabulary/interactive.js.map +1 -0
  125. package/dist/vocabulary/layout.d.ts +82 -0
  126. package/dist/vocabulary/layout.d.ts.map +1 -0
  127. package/dist/vocabulary/layout.js +136 -0
  128. package/dist/vocabulary/layout.js.map +1 -0
  129. package/dist/vocabulary/list.d.ts +17 -0
  130. package/dist/vocabulary/list.d.ts.map +1 -0
  131. package/dist/vocabulary/list.js +48 -0
  132. package/dist/vocabulary/list.js.map +1 -0
  133. package/dist/vocabulary/media.d.ts +28 -0
  134. package/dist/vocabulary/media.d.ts.map +1 -0
  135. package/dist/vocabulary/media.js +42 -0
  136. package/dist/vocabulary/media.js.map +1 -0
  137. package/dist/vocabulary/text.d.ts +60 -0
  138. package/dist/vocabulary/text.d.ts.map +1 -0
  139. package/dist/vocabulary/text.js +48 -0
  140. package/dist/vocabulary/text.js.map +1 -0
  141. package/package.json +21 -13
  142. package/src/JSONGenerativeUI.client.tsx +10 -2
  143. package/src/JSONGenerativeUI.shared.ts +8 -0
  144. package/src/actionRegistry.test.ts +90 -0
  145. package/src/actionRegistry.ts +88 -0
  146. package/src/buildPresentParameters.ts +38 -16
  147. package/src/generativeUIToJSX.test.ts +186 -0
  148. package/src/generativeUIToJSX.ts +112 -21
  149. package/src/index.server.ts +2 -0
  150. package/src/index.shared.ts +61 -0
  151. package/src/index.ts +2 -26
  152. package/src/ir.test.ts +203 -0
  153. package/src/ir.ts +279 -0
  154. package/src/renderGenerativeUI.test.tsx +69 -2
  155. package/src/renderGenerativeUI.tsx +49 -82
  156. package/src/slack/constants.ts +217 -0
  157. package/src/slack/decodeBlockAction.test.ts +135 -0
  158. package/src/slack/decodeBlockAction.ts +66 -0
  159. package/src/slack/fromSlackBlocks.test.ts +1145 -0
  160. package/src/slack/fromSlackBlocks.ts +709 -0
  161. package/src/slack/toSlackBlocks.test.ts +1936 -0
  162. package/src/slack/toSlackBlocks.ts +1319 -0
  163. package/src/slack/types.ts +230 -0
  164. package/src/slack.ts +37 -0
  165. package/src/teams/boundSpec.ts +102 -0
  166. package/src/teams/constants.ts +127 -0
  167. package/src/teams/decodeSubmitData.test.ts +148 -0
  168. package/src/teams/decodeSubmitData.ts +57 -0
  169. package/src/teams/toAdaptiveCard.test.ts +1410 -0
  170. package/src/teams/toAdaptiveCard.ts +762 -0
  171. package/src/teams/toTeamsAttachments.test.ts +29 -0
  172. package/src/teams/toTeamsAttachments.ts +137 -0
  173. package/src/teams/types.ts +237 -0
  174. package/src/teams.ts +34 -0
  175. package/src/types.ts +57 -42
  176. package/src/vocabulary/alert.test.tsx +136 -0
  177. package/src/vocabulary/alert.tsx +66 -0
  178. package/src/vocabulary/collectFormValues.test.ts +127 -0
  179. package/src/vocabulary/collectFormValues.ts +58 -0
  180. package/src/vocabulary/data.test.tsx +567 -0
  181. package/src/vocabulary/data.tsx +494 -0
  182. package/src/vocabulary/dispatch.ts +22 -0
  183. package/src/vocabulary/fact.test.tsx +15 -0
  184. package/src/vocabulary/fact.tsx +22 -0
  185. package/src/vocabulary/form.test.tsx +112 -0
  186. package/src/vocabulary/form.tsx +34 -0
  187. package/src/vocabulary/icon.test.tsx +64 -0
  188. package/src/vocabulary/icon.tsx +174 -0
  189. package/src/vocabulary/index.test.ts +63 -0
  190. package/src/vocabulary/index.ts +33 -0
  191. package/src/vocabulary/interactive.dispatch.test.tsx +352 -0
  192. package/src/vocabulary/interactive.test.tsx +248 -0
  193. package/src/vocabulary/interactive.tsx +269 -0
  194. package/src/vocabulary/layout.test.tsx +289 -0
  195. package/src/vocabulary/layout.tsx +229 -0
  196. package/src/vocabulary/list.test.tsx +195 -0
  197. package/src/vocabulary/list.tsx +48 -0
  198. package/src/vocabulary/media.test.tsx +85 -0
  199. package/src/vocabulary/media.tsx +56 -0
  200. package/src/vocabulary/text.test.tsx +73 -0
  201. package/src/vocabulary/text.tsx +59 -0
@@ -0,0 +1,230 @@
1
+ import type { UIElement } from "../ir";
2
+
3
+ /** A Slack plain-text composition object. */
4
+ export interface SlackPlainText {
5
+ readonly type: "plain_text";
6
+ readonly text: string;
7
+ }
8
+
9
+ /** A Slack mrkdwn composition object. */
10
+ export interface SlackMrkdwnText {
11
+ readonly type: "mrkdwn";
12
+ readonly text: string;
13
+ }
14
+
15
+ /** A text composition object accepted by card and alert text fields. */
16
+ export type SlackTextObject = SlackPlainText | SlackMrkdwnText;
17
+
18
+ /** A selectable Slack option. */
19
+ export interface SlackOption {
20
+ readonly text: SlackPlainText;
21
+ readonly value: string;
22
+ }
23
+
24
+ /** A Slack button element. */
25
+ export interface SlackButtonElement {
26
+ readonly type: "button";
27
+ readonly text: SlackPlainText;
28
+ readonly action_id: string;
29
+ readonly value?: string;
30
+ readonly style?: "primary" | "danger";
31
+ }
32
+
33
+ /** A Slack static-select element. */
34
+ export interface SlackStaticSelectElement {
35
+ readonly type: "static_select";
36
+ readonly action_id: string;
37
+ readonly options: readonly SlackOption[];
38
+ readonly placeholder?: SlackPlainText;
39
+ }
40
+
41
+ /** A Slack date-picker element. */
42
+ export interface SlackDatePickerElement {
43
+ readonly type: "datepicker";
44
+ readonly action_id: string;
45
+ readonly initial_date?: string;
46
+ }
47
+
48
+ /** A Slack checkbox-group element. */
49
+ export interface SlackCheckboxesElement {
50
+ readonly type: "checkboxes";
51
+ readonly action_id: string;
52
+ readonly options: readonly SlackOption[];
53
+ readonly initial_options?: readonly SlackOption[];
54
+ }
55
+
56
+ /** A Slack radio-button element. */
57
+ export interface SlackRadioButtonsElement {
58
+ readonly type: "radio_buttons";
59
+ readonly action_id: string;
60
+ readonly options: readonly SlackOption[];
61
+ readonly initial_option?: SlackOption;
62
+ }
63
+
64
+ /** A Slack plain-text input element. */
65
+ export interface SlackPlainTextInputElement {
66
+ readonly type: "plain_text_input";
67
+ readonly action_id: string;
68
+ readonly multiline?: boolean;
69
+ readonly placeholder?: SlackPlainText;
70
+ }
71
+
72
+ /** An element accepted by a Slack actions block. */
73
+ export type SlackActionElement =
74
+ | SlackButtonElement
75
+ | SlackStaticSelectElement
76
+ | SlackDatePickerElement
77
+ | SlackCheckboxesElement
78
+ | SlackRadioButtonsElement;
79
+
80
+ /** A Slack header block. */
81
+ export interface SlackHeaderBlock {
82
+ readonly type: "header";
83
+ readonly text: SlackPlainText;
84
+ }
85
+
86
+ /** A Slack section block. */
87
+ export interface SlackSectionBlock {
88
+ readonly type: "section";
89
+ readonly text?: SlackMrkdwnText;
90
+ readonly fields?: readonly SlackMrkdwnText[];
91
+ readonly accessory?: SlackButtonElement;
92
+ }
93
+
94
+ /** A Slack context block. */
95
+ export interface SlackContextBlock {
96
+ readonly type: "context";
97
+ readonly elements: readonly SlackMrkdwnText[];
98
+ }
99
+
100
+ /** A Slack image block. */
101
+ export interface SlackImageBlock {
102
+ readonly type: "image";
103
+ readonly image_url: string;
104
+ readonly alt_text: string;
105
+ }
106
+
107
+ /** A Slack divider block. */
108
+ export interface SlackDividerBlock {
109
+ readonly type: "divider";
110
+ }
111
+
112
+ /** A Slack actions block. */
113
+ export interface SlackActionsBlock {
114
+ readonly type: "actions";
115
+ readonly elements: readonly SlackActionElement[];
116
+ }
117
+
118
+ /** A Slack input block. */
119
+ export interface SlackInputBlock {
120
+ readonly type: "input";
121
+ readonly label: SlackPlainText;
122
+ readonly element: SlackPlainTextInputElement;
123
+ }
124
+
125
+ /** A text cell in a Slack data-table block. */
126
+ export interface SlackDataTableRawTextCell {
127
+ readonly type: "raw_text";
128
+ readonly text: string;
129
+ }
130
+
131
+ /** A numeric cell in a Slack data-table block. */
132
+ export interface SlackDataTableRawNumberCell {
133
+ readonly type: "raw_number";
134
+ readonly text: string;
135
+ }
136
+
137
+ /** A cell value in a Slack data-table block. */
138
+ export type SlackDataTableCell =
139
+ | SlackDataTableRawTextCell
140
+ | SlackDataTableRawNumberCell;
141
+
142
+ /**
143
+ * A Slack card block. A card does not nest arbitrary blocks: content is
144
+ * limited to a hero image, title, subtitle, body, subtext, and up to three
145
+ * action buttons. At least one of `hero_image`, `title`, `actions`, or `body`
146
+ * is required.
147
+ */
148
+ export interface SlackCardBlock {
149
+ readonly type: "card";
150
+ readonly hero_image?: SlackImageBlock;
151
+ readonly title?: SlackTextObject;
152
+ readonly subtitle?: SlackTextObject;
153
+ readonly body?: SlackTextObject;
154
+ readonly subtext?: SlackTextObject;
155
+ readonly actions?: readonly SlackButtonElement[];
156
+ }
157
+
158
+ /** A Slack carousel block: a horizontally scrollable group of card blocks. */
159
+ export interface SlackCarouselBlock {
160
+ readonly type: "carousel";
161
+ readonly elements: readonly SlackCardBlock[];
162
+ }
163
+
164
+ /** A Slack data-table block. The first entry in `rows` is the header row. */
165
+ export interface SlackDataTableBlock {
166
+ readonly type: "data_table";
167
+ readonly caption: string;
168
+ readonly rows: readonly (readonly SlackDataTableCell[])[];
169
+ readonly page_size?: number;
170
+ }
171
+
172
+ /** A Slack markdown block. */
173
+ export interface SlackMarkdownBlock {
174
+ readonly type: "markdown";
175
+ readonly text: string;
176
+ }
177
+
178
+ /** The severity level accepted by a Slack alert block. */
179
+ export type SlackAlertLevel =
180
+ | "default"
181
+ | "info"
182
+ | "warning"
183
+ | "error"
184
+ | "success";
185
+
186
+ /** A Slack alert block. Alert blocks are only supported in modals. */
187
+ export interface SlackAlertBlock {
188
+ readonly type: "alert";
189
+ readonly text: SlackTextObject;
190
+ readonly level?: SlackAlertLevel;
191
+ }
192
+
193
+ /** A block emitted by the Slack converter. */
194
+ export type SlackBlock =
195
+ | SlackHeaderBlock
196
+ | SlackSectionBlock
197
+ | SlackContextBlock
198
+ | SlackImageBlock
199
+ | SlackDividerBlock
200
+ | SlackActionsBlock
201
+ | SlackInputBlock
202
+ | SlackCardBlock
203
+ | SlackCarouselBlock
204
+ | SlackDataTableBlock
205
+ | SlackMarkdownBlock
206
+ | SlackAlertBlock;
207
+
208
+ /** Options that select the target Slack surface. */
209
+ export interface ToSlackBlocksOptions {
210
+ readonly surface?: "message" | "modal";
211
+ }
212
+
213
+ /** A non-fatal downgrade reported during Slack conversion. */
214
+ export interface SlackConversionWarning {
215
+ readonly code: "clamped" | "dropped" | "fallback";
216
+ readonly component: string;
217
+ readonly detail: string;
218
+ }
219
+
220
+ /** The blocks and non-fatal warnings produced by Slack conversion. */
221
+ export interface SlackBlocksResult {
222
+ readonly blocks: SlackBlock[];
223
+ readonly warnings: SlackConversionWarning[];
224
+ }
225
+
226
+ /** The IR nodes and non-fatal warnings produced by converting Slack Block Kit JSON back into generative UI. */
227
+ export interface FromSlackBlocksResult {
228
+ readonly nodes: UIElement[];
229
+ readonly warnings: SlackConversionWarning[];
230
+ }
package/src/slack.ts ADDED
@@ -0,0 +1,37 @@
1
+ export { decodeBlockAction } from "./slack/decodeBlockAction";
2
+ export { fromSlackBlocks } from "./slack/fromSlackBlocks";
3
+ export { toSlackBlocks } from "./slack/toSlackBlocks";
4
+ export type {
5
+ FromSlackBlocksResult,
6
+ SlackActionElement,
7
+ SlackActionsBlock,
8
+ SlackAlertBlock,
9
+ SlackAlertLevel,
10
+ SlackBlock,
11
+ SlackBlocksResult,
12
+ SlackButtonElement,
13
+ SlackCardBlock,
14
+ SlackCarouselBlock,
15
+ SlackCheckboxesElement,
16
+ SlackContextBlock,
17
+ SlackConversionWarning,
18
+ SlackDataTableBlock,
19
+ SlackDataTableCell,
20
+ SlackDataTableRawNumberCell,
21
+ SlackDataTableRawTextCell,
22
+ SlackDatePickerElement,
23
+ SlackDividerBlock,
24
+ SlackHeaderBlock,
25
+ SlackImageBlock,
26
+ SlackInputBlock,
27
+ SlackMarkdownBlock,
28
+ SlackMrkdwnText,
29
+ SlackOption,
30
+ SlackPlainText,
31
+ SlackPlainTextInputElement,
32
+ SlackRadioButtonsElement,
33
+ SlackSectionBlock,
34
+ SlackStaticSelectElement,
35
+ SlackTextObject,
36
+ ToSlackBlocksOptions,
37
+ } from "./slack/types";
@@ -0,0 +1,102 @@
1
+ import {
2
+ CHILDREN_CAP,
3
+ MAX_TRAVERSAL_DEPTH,
4
+ NODE_BUDGET,
5
+ type ClampReason,
6
+ } from "./constants";
7
+
8
+ interface BoundState {
9
+ remaining: number;
10
+ exhausted: boolean;
11
+ }
12
+
13
+ function boundNode(
14
+ value: unknown,
15
+ depth: number,
16
+ onClamp: (reason: ClampReason) => void,
17
+ state: BoundState,
18
+ ancestors: WeakSet<object>,
19
+ ): unknown {
20
+ if (state.remaining <= 0) {
21
+ if (!state.exhausted) {
22
+ state.exhausted = true;
23
+ onClamp("budget");
24
+ }
25
+ return null;
26
+ }
27
+ state.remaining -= 1;
28
+ if (depth > MAX_TRAVERSAL_DEPTH) {
29
+ onClamp("depth");
30
+ return null;
31
+ }
32
+ if (Array.isArray(value)) {
33
+ if (ancestors.has(value)) {
34
+ onClamp("cycle");
35
+ return null;
36
+ }
37
+ ancestors.add(value);
38
+ const bounded = Array.prototype.slice.call(
39
+ value,
40
+ 0,
41
+ CHILDREN_CAP,
42
+ ) as unknown[];
43
+ if (value.length > CHILDREN_CAP) onClamp("children");
44
+ const result = bounded.map((item) =>
45
+ boundNode(item, depth + 1, onClamp, state, ancestors),
46
+ );
47
+ ancestors.delete(value);
48
+ return result;
49
+ }
50
+ if (
51
+ value !== null &&
52
+ typeof value === "object" &&
53
+ "children" in (value as Record<string, unknown>)
54
+ ) {
55
+ if (ancestors.has(value)) {
56
+ onClamp("cycle");
57
+ return null;
58
+ }
59
+ ancestors.add(value);
60
+ const record = value as Record<string, unknown>;
61
+ const result = {
62
+ ...record,
63
+ children: boundNode(
64
+ record["children"],
65
+ depth + 1,
66
+ onClamp,
67
+ state,
68
+ ancestors,
69
+ ),
70
+ };
71
+ ancestors.delete(value);
72
+ return result;
73
+ }
74
+ return value;
75
+ }
76
+
77
+ /**
78
+ * Produces a bounded plain copy of a raw generative-ui spec before it
79
+ * reaches `normalizeSpec`, whose own traversal of the root array or any
80
+ * `children` array walks the full reported length of a hostile proxied
81
+ * array before any per-field cap downstream ever applies. Every array
82
+ * (root, or `children` at any depth) is capped to {@link CHILDREN_CAP}
83
+ * entries via `Array.prototype.slice`, which bounds even a proxy with a
84
+ * fabricated `length`; recursion itself is capped at
85
+ * {@link MAX_TRAVERSAL_DEPTH}. `onClamp` fires once per level that was
86
+ * truncated, receiving the reason for that truncation: `"children"`,
87
+ * `"depth"`, `"budget"`, or `"cycle"`. The walk also spends a total budget of
88
+ * {@link NODE_BUDGET} nodes, so shared references cannot multiply work
89
+ * exponentially, and a node that is its own ancestor is cut to `null`.
90
+ */
91
+ export function boundSpec(
92
+ spec: unknown,
93
+ onClamp: (reason: ClampReason) => void,
94
+ ): unknown {
95
+ return boundNode(
96
+ spec,
97
+ 0,
98
+ onClamp,
99
+ { remaining: NODE_BUDGET, exhausted: false },
100
+ new WeakSet(),
101
+ );
102
+ }
@@ -0,0 +1,127 @@
1
+ import type {
2
+ TeamsAdaptiveCard,
3
+ TeamsCardAttachment,
4
+ TeamsCardElement,
5
+ TeamsSubmitAction,
6
+ TeamsSubmitData,
7
+ } from "./types";
8
+
9
+ /** The Adaptive Card schema version emitted by {@link toAdaptiveCard}: the Teams desktop/web cap (mobile clients cap at 1.2). */
10
+ export const AC_VERSION = "1.5";
11
+
12
+ /** The `$schema` URL stamped on every emitted card. */
13
+ export const ADAPTIVE_CARD_SCHEMA =
14
+ "http://adaptivecards.io/schemas/adaptive-card.json";
15
+
16
+ /** The `contentType` stamped on every emitted attachment. */
17
+ export const ATTACHMENT_CONTENT_TYPE =
18
+ "application/vnd.microsoft.card.adaptive";
19
+
20
+ /** The maximum normalized traversal depth. */
21
+ export const MAX_TRAVERSAL_DEPTH = 64;
22
+
23
+ /**
24
+ * The maximum number of entries kept at any single level (root, or a
25
+ * `children` array at any depth) of the raw spec, before it ever reaches
26
+ * `normalizeSpec`. Bounds a hostile array's reported `length` so the pre-pass
27
+ * can never be made to walk further than this regardless of what the array
28
+ * claims about itself.
29
+ */
30
+ export const CHILDREN_CAP = 200;
31
+
32
+ /**
33
+ * The total number of nodes {@link boundSpec} will visit across one walk,
34
+ * regardless of how many times a shared reference recurs. Bounds the
35
+ * combinatorial work a DAG of shared or self-referential nodes would
36
+ * otherwise force even though each individual array stays within
37
+ * {@link CHILDREN_CAP}.
38
+ */
39
+ export const NODE_BUDGET = 5000;
40
+
41
+ export type ClampReason = "children" | "budget" | "cycle" | "depth";
42
+
43
+ export function clampReasonDetail(reason: ClampReason): string {
44
+ if (reason === "budget") {
45
+ return `the tree was truncated after ${NODE_BUDGET} nodes.`;
46
+ }
47
+ if (reason === "cycle") return "a self-referencing node was dropped.";
48
+ if (reason === "depth") {
49
+ return `nodes deeper than ${MAX_TRAVERSAL_DEPTH} levels were dropped.`;
50
+ }
51
+ return `children were clamped to ${CHILDREN_CAP} entries.`;
52
+ }
53
+
54
+ /** The maximum number of attachments produced from a root-level Carousel. */
55
+ export const CAROUSEL_ATTACHMENT_CAP = 10;
56
+
57
+ /** The maximum number of primary-mode actions in one ActionSet; extras are set to secondary mode instead of being dropped. */
58
+ export const PRIMARY_ACTION_CAP = 6;
59
+
60
+ /** The maximum number of data rows in a Table. */
61
+ export const TABLE_ROW_CAP = 100;
62
+
63
+ /** The maximum number of columns in a Table. */
64
+ export const TABLE_COLUMN_CAP = 20;
65
+
66
+ /** The maximum number of choices in a Select or RadioGroup. */
67
+ export const CHOICE_OPTION_CAP = 100;
68
+
69
+ /** The soft byte-count budget for a serialized card, past which a warning notes Teams' 100 KB bot message limit (the card is never truncated to fit it). */
70
+ export const PAYLOAD_SOFT_CAP = 80000;
71
+
72
+ const textEncoder = new TextEncoder();
73
+
74
+ /** Measures the wire size of `value` in UTF-8 bytes rather than UTF-16 code units. */
75
+ export function utf8ByteLength(value: string): number {
76
+ return textEncoder.encode(value).byteLength;
77
+ }
78
+
79
+ /** Builds the fixed-shape Adaptive Card envelope around a converted body. */
80
+ export function buildCard(body: TeamsCardElement[]): TeamsAdaptiveCard {
81
+ return {
82
+ $schema: ADAPTIVE_CARD_SCHEMA,
83
+ type: "AdaptiveCard",
84
+ version: AC_VERSION,
85
+ body,
86
+ actions: [],
87
+ };
88
+ }
89
+
90
+ /** Wraps a card as a bot-framework message attachment. */
91
+ export function buildAttachment(card: TeamsAdaptiveCard): TeamsCardAttachment {
92
+ return { contentType: ATTACHMENT_CONTENT_TYPE, content: card };
93
+ }
94
+
95
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
96
+ typeof value === "object" && value !== null && !Array.isArray(value);
97
+
98
+ /**
99
+ * Nests an IR action under an `aui` key so `decodeSubmitData` can invert it
100
+ * back out of the merged `activity.value` object a Teams bot receives,
101
+ * alongside whatever other named inputs shared the card.
102
+ */
103
+ export function buildSubmitData(action: unknown): TeamsSubmitData {
104
+ if (!isRecord(action)) return { aui: { type: "action" } };
105
+ const { type: rawType, ...payload } = action;
106
+ const type = typeof rawType === "string" ? rawType : "action";
107
+ return {
108
+ aui: {
109
+ type,
110
+ ...(Object.keys(payload).length > 0 ? { payload } : {}),
111
+ },
112
+ };
113
+ }
114
+
115
+ /** Builds an Action.Submit carrying `action` in the `aui` envelope. */
116
+ export function buildSubmitAction(
117
+ title: string,
118
+ action: unknown,
119
+ mode?: "secondary",
120
+ ): TeamsSubmitAction {
121
+ return {
122
+ type: "Action.Submit",
123
+ title,
124
+ data: buildSubmitData(action),
125
+ ...(mode !== undefined ? { mode } : {}),
126
+ };
127
+ }
@@ -0,0 +1,148 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { decodeSubmitData } from "./decodeSubmitData";
3
+
4
+ describe("decodeSubmitData", () => {
5
+ it("decodes the aui envelope, spreading its payload into the result", () => {
6
+ const value = { aui: { type: "approve", payload: { requestId: "r1" } } };
7
+ expect(decodeSubmitData(value)).toEqual({
8
+ type: "approve",
9
+ requestId: "r1",
10
+ });
11
+ });
12
+
13
+ it("collects every other top-level key into $input", () => {
14
+ const value = {
15
+ aui: { type: "approve", payload: { requestId: "r1" } },
16
+ email: "a@b.com",
17
+ subscribe: "true",
18
+ };
19
+ expect(decodeSubmitData(value)).toEqual({
20
+ type: "approve",
21
+ requestId: "r1",
22
+ $input: { email: "a@b.com", subscribe: "true" },
23
+ });
24
+ });
25
+
26
+ it("omits $input when there are no other top-level keys", () => {
27
+ expect(decodeSubmitData({ aui: { type: "approve" } })).toEqual({
28
+ type: "approve",
29
+ });
30
+ });
31
+
32
+ it("omits payload when the envelope carries none", () => {
33
+ const value = { aui: { type: "approve" }, note: "hi" };
34
+ expect(decodeSubmitData(value)).toEqual({
35
+ type: "approve",
36
+ $input: { note: "hi" },
37
+ });
38
+ });
39
+
40
+ it("drops a $input key inside the envelope payload, keeping the slot for collected inputs", () => {
41
+ const value = {
42
+ aui: { type: "approve", payload: { requestId: "r1", $input: "spoofed" } },
43
+ email: "a@b.com",
44
+ };
45
+ expect(decodeSubmitData(value)).toEqual({
46
+ type: "approve",
47
+ requestId: "r1",
48
+ $input: { email: "a@b.com" },
49
+ });
50
+ });
51
+
52
+ it("drops a payload $input even when no input values were collected", () => {
53
+ const value = {
54
+ aui: { type: "approve", payload: { $input: "spoofed" } },
55
+ };
56
+ const decoded = decodeSubmitData(value);
57
+ expect(decoded).toEqual({ type: "approve" });
58
+ expect(decoded !== undefined && "$input" in decoded).toBe(false);
59
+ });
60
+
61
+ it("keeps the envelope type over a payload key of the same name", () => {
62
+ const value = {
63
+ aui: { type: "approve", payload: { type: "spoofed" } },
64
+ };
65
+ expect(decodeSubmitData(value)).toEqual({ type: "approve" });
66
+ });
67
+
68
+ it("returns undefined when aui is missing", () => {
69
+ expect(decodeSubmitData({ email: "a@b.com" })).toBeUndefined();
70
+ });
71
+
72
+ it("returns undefined for malformed or null input, never throwing", () => {
73
+ const malformed: unknown[] = [
74
+ null,
75
+ undefined,
76
+ 42,
77
+ "just a string",
78
+ true,
79
+ [],
80
+ {},
81
+ { aui: "nope" },
82
+ { aui: {} },
83
+ { aui: { type: 123 } },
84
+ ];
85
+ for (const input of malformed) {
86
+ expect(() => decodeSubmitData(input)).not.toThrow();
87
+ expect(decodeSubmitData(input)).toBeUndefined();
88
+ }
89
+ });
90
+
91
+ it("never throws even when reading the input itself throws", () => {
92
+ const hostile = new Proxy(
93
+ {},
94
+ {
95
+ get() {
96
+ throw new Error("boom");
97
+ },
98
+ },
99
+ );
100
+ expect(() => decodeSubmitData(hostile)).not.toThrow();
101
+ expect(decodeSubmitData(hostile)).toBeUndefined();
102
+ });
103
+
104
+ it("returns undefined for a non-empty array activity.value", () => {
105
+ expect(decodeSubmitData([1, 2, 3])).toBeUndefined();
106
+ });
107
+
108
+ it("returns undefined when aui is inherited rather than owned", () => {
109
+ const value = Object.create({ aui: { type: "x" } });
110
+ expect(decodeSubmitData(value)).toBeUndefined();
111
+ });
112
+
113
+ it("returns undefined when the envelope's type is inherited rather than owned", () => {
114
+ const value = { aui: Object.create({ type: "x" }) };
115
+ expect(decodeSubmitData(value)).toBeUndefined();
116
+ });
117
+
118
+ it("keeps __proto__ and constructor keys from the payload as own data properties without polluting Object.prototype", () => {
119
+ const payload = JSON.parse(
120
+ '{"__proto__": {"evil": true}, "constructor": {"evil": true}}',
121
+ );
122
+ const value = { aui: { type: "approve", payload } };
123
+ const result = decodeSubmitData(value) as Record<string, unknown>;
124
+ expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(
125
+ true,
126
+ );
127
+ expect(result["__proto__"]).toEqual({ evil: true });
128
+ expect(result["constructor"]).toEqual({ evil: true });
129
+ expect(({} as Record<string, unknown>)["evil"]).toBeUndefined();
130
+ expect(Object.getPrototypeOf({})).toBe(Object.prototype);
131
+ });
132
+
133
+ it("keeps __proto__ and constructor keys from $input as own data properties without polluting Object.prototype", () => {
134
+ const value = JSON.parse(
135
+ '{"aui": {"type": "approve"}, "__proto__": "hostile", "constructor": "hostile"}',
136
+ );
137
+ const result = decodeSubmitData(value);
138
+ const input = result?.["$input"] as Record<string, unknown>;
139
+ expect(Object.prototype.hasOwnProperty.call(input, "__proto__")).toBe(true);
140
+ expect(Object.prototype.hasOwnProperty.call(input, "constructor")).toBe(
141
+ true,
142
+ );
143
+ expect(input["__proto__"]).toBe("hostile");
144
+ expect(input["constructor"]).toBe("hostile");
145
+ expect(typeof ({} as object).constructor).toBe("function");
146
+ expect(Object.getPrototypeOf({})).toBe(Object.prototype);
147
+ });
148
+ });
@@ -0,0 +1,57 @@
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
+ /**
7
+ * Decodes the merged `activity.value` object a Teams bot receives on
8
+ * Action.Submit, inverting the `aui` envelope {@link buildSubmitAction} nests
9
+ * the resume action under. Adaptive Cards fold every other same-card input's
10
+ * value into the same submit object keyed by its `id`, so every top-level key
11
+ * besides `aui` is collected into `$input` (omitted when there are none).
12
+ * `aui` is reserved for the envelope: `toAdaptiveCard` renames any input
13
+ * whose id would collide to an unused id derived from it before encoding, so
14
+ * a same-card input value can never land on this key. `$input` and `type`
15
+ * are reserved on the way out: a `$input` key inside the envelope payload is
16
+ * dropped so the slot only ever reflects same-card input values, and the
17
+ * envelope's own `type` wins over a payload key of the same name. Every
18
+ * collected key (in `payload` or `$input`) is kept as an own data property of
19
+ * the returned object, even a `__proto__` or `constructor` key, since the
20
+ * object is always built by spreading and `Object.fromEntries` rather than by
21
+ * keyed assignment. Envelope keys are read as own properties only, so
22
+ * prototype-inherited `aui`, `type`, or `payload` values never dispatch.
23
+ * Returns `undefined` for a missing or malformed `aui` envelope; never throws.
24
+ */
25
+ export function decodeSubmitData(value: unknown): Action | undefined {
26
+ try {
27
+ if (!isRecord(value) || !Object.hasOwn(value, "aui")) return undefined;
28
+ const aui = value["aui"];
29
+ if (
30
+ !isRecord(aui) ||
31
+ !Object.hasOwn(aui, "type") ||
32
+ typeof aui["type"] !== "string"
33
+ ) {
34
+ return undefined;
35
+ }
36
+ const type = aui["type"];
37
+
38
+ const payload =
39
+ Object.hasOwn(aui, "payload") && isRecord(aui["payload"])
40
+ ? aui["payload"]
41
+ : {};
42
+ const payloadEntries = Object.entries(payload).filter(
43
+ ([key]) => key !== "$input",
44
+ );
45
+ const inputEntries = Object.entries(value).filter(([key]) => key !== "aui");
46
+ const input =
47
+ inputEntries.length > 0 ? Object.fromEntries(inputEntries) : undefined;
48
+
49
+ return {
50
+ ...Object.fromEntries(payloadEntries),
51
+ type,
52
+ ...(input !== undefined ? { $input: input } : {}),
53
+ };
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ }