@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.
- package/dist/JSONGenerativeUI.client.d.ts +0 -1
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.server.d.ts +0 -1
- package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.shared.d.ts +0 -1
- package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
- package/dist/actionRegistry.d.ts +2 -11
- package/dist/actionRegistry.d.ts.map +1 -1
- package/dist/actionRegistry.js.map +1 -1
- package/dist/buildPresentParameters.d.ts +5 -5
- package/dist/buildPresentParameters.d.ts.map +1 -1
- package/dist/buildPresentParameters.js +10 -5
- package/dist/buildPresentParameters.js.map +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/defineGenerativeComponents.d.ts +0 -1
- package/dist/defineGenerativeComponents.d.ts.map +1 -1
- package/dist/generativeUIToJSX.d.ts +15 -10
- package/dist/generativeUIToJSX.d.ts.map +1 -1
- package/dist/generativeUIToJSX.js +47 -17
- package/dist/generativeUIToJSX.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/index.server.d.ts +4 -3
- package/dist/index.server.js +2 -2
- package/dist/index.shared.d.ts +3 -3
- package/dist/index.shared.js +2 -2
- package/dist/ir.d.ts +6 -6
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js +27 -1
- package/dist/ir.js.map +1 -1
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
- package/dist/renderGenerativeUI.d.ts +0 -1
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/slack/constants.d.ts +111 -0
- package/dist/slack/constants.d.ts.map +1 -0
- package/dist/slack/constants.js +162 -0
- package/dist/slack/constants.js.map +1 -0
- package/dist/slack/decodeBlockAction.d.ts +11 -0
- package/dist/slack/decodeBlockAction.d.ts.map +1 -0
- package/dist/slack/decodeBlockAction.js +40 -0
- package/dist/slack/decodeBlockAction.js.map +1 -0
- package/dist/slack/fromSlackBlocks.d.ts +30 -0
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
- package/dist/slack/fromSlackBlocks.js +447 -0
- package/dist/slack/fromSlackBlocks.js.map +1 -0
- package/dist/slack/toSlackBlocks.d.ts +7 -0
- package/dist/slack/toSlackBlocks.d.ts.map +1 -0
- package/dist/slack/toSlackBlocks.js +649 -0
- package/dist/slack/toSlackBlocks.js.map +1 -0
- package/dist/slack/types.d.ts +178 -0
- package/dist/slack/types.d.ts.map +1 -0
- package/dist/slack/types.js +0 -0
- package/dist/slack.d.ts +5 -0
- package/dist/slack.js +4 -0
- package/dist/teams/boundSpec.d.ts +20 -0
- package/dist/teams/boundSpec.d.ts.map +1 -0
- package/dist/teams/boundSpec.js +67 -0
- package/dist/teams/boundSpec.js.map +1 -0
- package/dist/teams/constants.d.ts +57 -0
- package/dist/teams/constants.d.ts.map +1 -0
- package/dist/teams/constants.js +92 -0
- package/dist/teams/constants.js.map +1 -0
- package/dist/teams/decodeSubmitData.d.ts +25 -0
- package/dist/teams/decodeSubmitData.d.ts.map +1 -0
- package/dist/teams/decodeSubmitData.js +44 -0
- package/dist/teams/decodeSubmitData.js.map +1 -0
- package/dist/teams/toAdaptiveCard.d.ts +39 -0
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
- package/dist/teams/toAdaptiveCard.js +496 -0
- package/dist/teams/toAdaptiveCard.js.map +1 -0
- package/dist/teams/toTeamsAttachments.d.ts +13 -0
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
- package/dist/teams/toTeamsAttachments.js +95 -0
- package/dist/teams/toTeamsAttachments.js.map +1 -0
- package/dist/teams/types.d.ts +196 -0
- package/dist/teams/types.d.ts.map +1 -0
- package/dist/teams/types.js +0 -0
- package/dist/teams.d.ts +5 -0
- package/dist/teams.js +4 -0
- package/dist/types.d.ts +7 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/vocabulary/alert.d.ts +6 -12
- package/dist/vocabulary/alert.d.ts.map +1 -1
- package/dist/vocabulary/alert.js +13 -4
- package/dist/vocabulary/alert.js.map +1 -1
- package/dist/vocabulary/collectFormValues.d.ts +23 -0
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
- package/dist/vocabulary/collectFormValues.js +32 -0
- package/dist/vocabulary/collectFormValues.js.map +1 -0
- package/dist/vocabulary/data.d.ts +16 -17
- package/dist/vocabulary/data.d.ts.map +1 -1
- package/dist/vocabulary/data.js +244 -13
- package/dist/vocabulary/data.js.map +1 -1
- package/dist/vocabulary/dispatch.d.ts +11 -0
- package/dist/vocabulary/dispatch.d.ts.map +1 -0
- package/dist/vocabulary/dispatch.js +21 -0
- package/dist/vocabulary/dispatch.js.map +1 -0
- package/dist/vocabulary/fact.d.ts +1 -6
- package/dist/vocabulary/fact.d.ts.map +1 -1
- package/dist/vocabulary/fact.js.map +1 -1
- package/dist/vocabulary/form.d.ts +14 -0
- package/dist/vocabulary/form.d.ts.map +1 -0
- package/dist/vocabulary/form.js +23 -0
- package/dist/vocabulary/form.js.map +1 -0
- package/dist/vocabulary/icon.d.ts +44 -0
- package/dist/vocabulary/icon.d.ts.map +1 -0
- package/dist/vocabulary/icon.js +272 -0
- package/dist/vocabulary/icon.js.map +1 -0
- package/dist/vocabulary/index.d.ts +0 -1
- package/dist/vocabulary/index.d.ts.map +1 -1
- package/dist/vocabulary/index.js +7 -1
- package/dist/vocabulary/index.js.map +1 -1
- package/dist/vocabulary/interactive.d.ts +45 -33
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +78 -29
- package/dist/vocabulary/interactive.js.map +1 -1
- package/dist/vocabulary/layout.d.ts +27 -23
- package/dist/vocabulary/layout.d.ts.map +1 -1
- package/dist/vocabulary/layout.js +78 -14
- package/dist/vocabulary/layout.js.map +1 -1
- package/dist/vocabulary/list.d.ts +17 -0
- package/dist/vocabulary/list.d.ts.map +1 -0
- package/dist/vocabulary/list.js +48 -0
- package/dist/vocabulary/list.js.map +1 -0
- package/dist/vocabulary/media.d.ts +3 -9
- package/dist/vocabulary/media.d.ts.map +1 -1
- package/dist/vocabulary/media.js +17 -7
- package/dist/vocabulary/media.js.map +1 -1
- package/dist/vocabulary/text.d.ts +3 -17
- package/dist/vocabulary/text.d.ts.map +1 -1
- package/dist/vocabulary/text.js.map +1 -1
- package/package.json +15 -7
- package/src/actionRegistry.ts +2 -10
- package/src/buildPresentParameters.ts +15 -9
- package/src/generativeUIToJSX.test.ts +186 -0
- package/src/generativeUIToJSX.ts +112 -21
- package/src/index.shared.ts +3 -0
- package/src/ir.ts +31 -5
- package/src/renderGenerativeUI.test.tsx +16 -2
- package/src/slack/constants.ts +212 -0
- package/src/slack/decodeBlockAction.test.ts +135 -0
- package/src/slack/decodeBlockAction.ts +66 -0
- package/src/slack/fromSlackBlocks.test.ts +1145 -0
- package/src/slack/fromSlackBlocks.ts +709 -0
- package/src/slack/toSlackBlocks.test.ts +1936 -0
- package/src/slack/toSlackBlocks.ts +1319 -0
- package/src/slack/types.ts +230 -0
- package/src/slack.ts +37 -0
- package/src/teams/boundSpec.ts +102 -0
- package/src/teams/constants.ts +127 -0
- package/src/teams/decodeSubmitData.test.ts +148 -0
- package/src/teams/decodeSubmitData.ts +57 -0
- package/src/teams/toAdaptiveCard.test.ts +1410 -0
- package/src/teams/toAdaptiveCard.ts +762 -0
- package/src/teams/toTeamsAttachments.test.ts +29 -0
- package/src/teams/toTeamsAttachments.ts +137 -0
- package/src/teams/types.ts +237 -0
- package/src/teams.ts +34 -0
- package/src/vocabulary/alert.test.tsx +72 -2
- package/src/vocabulary/alert.tsx +24 -5
- package/src/vocabulary/collectFormValues.test.ts +127 -0
- package/src/vocabulary/collectFormValues.ts +58 -0
- package/src/vocabulary/data.test.tsx +503 -3
- package/src/vocabulary/data.tsx +426 -18
- package/src/vocabulary/dispatch.ts +22 -0
- package/src/vocabulary/form.test.tsx +112 -0
- package/src/vocabulary/form.tsx +34 -0
- package/src/vocabulary/icon.test.tsx +64 -0
- package/src/vocabulary/icon.tsx +174 -0
- package/src/vocabulary/index.test.ts +7 -0
- package/src/vocabulary/index.ts +6 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +243 -1
- package/src/vocabulary/interactive.test.tsx +143 -3
- package/src/vocabulary/interactive.tsx +134 -30
- package/src/vocabulary/layout.test.tsx +231 -3
- package/src/vocabulary/layout.tsx +168 -15
- package/src/vocabulary/list.test.tsx +195 -0
- package/src/vocabulary/list.tsx +48 -0
- package/src/vocabulary/media.test.tsx +36 -4
- package/src/vocabulary/media.tsx +27 -8
|
@@ -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
|
+
}
|