@tightknitai/slack-block-kit-validator 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/helpers/check-card-actions-max.d.ts +16 -0
- package/dist/helpers/check-card-actions-max.d.ts.map +1 -0
- package/dist/helpers/check-card-actions-max.js +25 -0
- package/dist/helpers/check-card-actions-max.js.map +1 -0
- package/dist/helpers/check-cumulative-markdown-length.d.ts +12 -0
- package/dist/helpers/check-cumulative-markdown-length.d.ts.map +1 -0
- package/dist/helpers/check-cumulative-markdown-length.js +23 -0
- package/dist/helpers/check-cumulative-markdown-length.js.map +1 -0
- package/dist/helpers/check-focus-on-load-uniqueness.d.ts +12 -0
- package/dist/helpers/check-focus-on-load-uniqueness.d.ts.map +1 -0
- package/dist/helpers/check-focus-on-load-uniqueness.js +52 -0
- package/dist/helpers/check-focus-on-load-uniqueness.js.map +1 -0
- package/dist/helpers/check-number-input-bounds.d.ts +12 -0
- package/dist/helpers/check-number-input-bounds.d.ts.map +1 -0
- package/dist/helpers/check-number-input-bounds.js +96 -0
- package/dist/helpers/check-number-input-bounds.js.map +1 -0
- package/dist/helpers/check-response-url-enabled-context.d.ts +25 -0
- package/dist/helpers/check-response-url-enabled-context.d.ts.map +1 -0
- package/dist/helpers/check-response-url-enabled-context.js +42 -0
- package/dist/helpers/check-response-url-enabled-context.js.map +1 -0
- package/dist/helpers/check-single-table-block.d.ts +10 -0
- package/dist/helpers/check-single-table-block.d.ts.map +1 -0
- package/dist/helpers/check-single-table-block.js +19 -0
- package/dist/helpers/check-single-table-block.js.map +1 -0
- package/dist/helpers/check-surface-compatibility.d.ts +22 -0
- package/dist/helpers/check-surface-compatibility.d.ts.map +1 -0
- package/dist/helpers/check-surface-compatibility.js +81 -0
- package/dist/helpers/check-surface-compatibility.js.map +1 -0
- package/dist/helpers/find-duplicate-block-ids.d.ts +17 -0
- package/dist/helpers/find-duplicate-block-ids.d.ts.map +1 -0
- package/dist/helpers/find-duplicate-block-ids.js +31 -0
- package/dist/helpers/find-duplicate-block-ids.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/slack-block-kit.schema.json +1605 -0
- package/dist/validate-block-kit.d.ts +33 -0
- package/dist/validate-block-kit.d.ts.map +1 -0
- package/dist/validate-block-kit.js +135 -0
- package/dist/validate-block-kit.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tightknit and slack-block-kit-validator contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# slack-block-kit-validator
|
|
2
|
+
|
|
3
|
+
[](https://github.com/TightknitAI/slack-block-kit-validator/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@tightknitai/slack-block-kit-validator)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
JSON Schema (draft 2020-12) and validation helpers for Slack Block Kit JSON. Catches invalid block payloads before Slack silently swallows them.
|
|
8
|
+
|
|
9
|
+
## Why this exists
|
|
10
|
+
|
|
11
|
+
Slack's API returns `200 OK` when you send malformed Block Kit JSON — the metadata is dropped and the message renders as plain text (or a modal opens blank). The only way to find out is to eyeball a real Slack channel. Slack hasn't open-sourced their validator.
|
|
12
|
+
|
|
13
|
+
This package compiles every rule in <https://docs.slack.dev/reference/block-kit> into a single JSON Schema, plus a handful of helpers for the cross-payload rules JSON Schema can't express (duplicate `block_id`, cumulative markdown length, one-table-per-message, `focus_on_load` uniqueness, surface compatibility).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pnpm add @tightknitai/slack-block-kit-validator
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Node 20+. No runtime peer dependencies.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { validateBlockKit } from "@tightknitai/slack-block-kit-validator";
|
|
27
|
+
|
|
28
|
+
const blocks = [
|
|
29
|
+
{ type: "section", text: { type: "mrkdwn", text: "Hello *world*" } },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const { valid, errors } = validateBlockKit(blocks);
|
|
33
|
+
if (!valid) {
|
|
34
|
+
console.error(errors);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Validating views
|
|
39
|
+
|
|
40
|
+
`validateBlockKit` takes an optional `target` for modal / home view envelopes:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
validateBlockKit(modalView, { target: "modal" });
|
|
44
|
+
validateBlockKit(homeView, { target: "home" });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
When `target` is `modal` or `home`, the surface-compatibility check is enforced automatically. For bare blocks arrays, pass `surface` explicitly:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
validateBlockKit(blocks, { surface: "message" });
|
|
51
|
+
// rejects input blocks, table on non-message surfaces, etc.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Example output
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
validateBlockKit([
|
|
58
|
+
{ type: "section" },
|
|
59
|
+
{ type: "divider", block_id: "x" },
|
|
60
|
+
{ type: "divider", block_id: "x" },
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
// {
|
|
64
|
+
// valid: false,
|
|
65
|
+
// errors: [
|
|
66
|
+
// "/0 must match a schema in anyOf",
|
|
67
|
+
// "blocks[2].block_id must be unique — 'x' appears at index 1 and 2"
|
|
68
|
+
// ]
|
|
69
|
+
// }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Using the helpers à la carte
|
|
73
|
+
|
|
74
|
+
The helpers are pure (no deps, no Ajv) and can be stacked on top of any validator — Zod, TypeBox, or a hand-rolled check:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import {
|
|
78
|
+
findDuplicateBlockIds,
|
|
79
|
+
checkCumulativeMarkdownLength,
|
|
80
|
+
checkSingleTableBlock,
|
|
81
|
+
checkFocusOnLoadUniqueness,
|
|
82
|
+
checkSurfaceCompatibility,
|
|
83
|
+
} from "@tightknitai/slack-block-kit-validator";
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Using the raw JSON Schema
|
|
87
|
+
|
|
88
|
+
Consuming from another validator, another language, or an OpenAPI spec:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { slackBlockKitSchema } from "@tightknitai/slack-block-kit-validator";
|
|
92
|
+
|
|
93
|
+
// With Ajv in a custom config
|
|
94
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
95
|
+
const validate = ajv.compile(slackBlockKitSchema);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The schema uses `$defs` for every block, element, composition object, rich-text leaf, and view envelope, so non-JS consumers can also import the JSON (`@tightknitai/slack-block-kit-validator/schema.json`) and pick the subset they need.
|
|
99
|
+
|
|
100
|
+
## API reference
|
|
101
|
+
|
|
102
|
+
### Wrapper
|
|
103
|
+
|
|
104
|
+
| Export | Signature | Description |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `validateBlockKit` | `(input, opts?) => { valid, errors[] }` | Runs schema + all caveat helpers. Defaults to validating a bare blocks array. |
|
|
107
|
+
| `ValidationResult` | `{ valid: boolean; errors: string[] }` | Return type. |
|
|
108
|
+
| `ValidationTarget` | `'blocks' \| 'modal' \| 'home'` | What shape `input` should match. |
|
|
109
|
+
| `ValidateBlockKitOptions` | `{ target?, surface? }` | Options bag. |
|
|
110
|
+
| `Surface` | `'message' \| 'modal' \| 'home'` | Surface compatibility target. |
|
|
111
|
+
|
|
112
|
+
### Helpers
|
|
113
|
+
|
|
114
|
+
| Helper | Signature | What it checks |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `findDuplicateBlockIds` | `(blocks) => string[]` | Duplicate `block_id` values in a blocks array. |
|
|
117
|
+
| `checkCumulativeMarkdownLength` | `(blocks) => string[]` | Sum of all `markdown` block text > 12,000 chars. |
|
|
118
|
+
| `checkSingleTableBlock` | `(blocks) => string[]` | More than one `table` block per payload. |
|
|
119
|
+
| `checkFocusOnLoadUniqueness` | `(blocks) => string[]` | More than one element with `focus_on_load: true` in a view (walks nested elements + accessories). |
|
|
120
|
+
| `checkSurfaceCompatibility` | `(blocks, surface) => string[]` | Blocks not allowed on the target surface (e.g. `input` on message, `video` on modal, `file_input` outside modals). |
|
|
121
|
+
| `checkCardActionsMax` | `(blocks) => string[]` | More than `CARD_ACTIONS_MAX` action buttons on a card block. |
|
|
122
|
+
| `checkNumberInputBounds` | `(blocks) => string[]` | `number_input` element with `min_value > max_value`. |
|
|
123
|
+
| `checkResponseUrlEnabledContext` | `(blocks, surface?) => string[]` | `response_url_enabled` set in contexts that don't support it. |
|
|
124
|
+
|
|
125
|
+
Each returns an array of human-readable error strings — empty when valid.
|
|
126
|
+
|
|
127
|
+
### Schema
|
|
128
|
+
|
|
129
|
+
| Export | Description |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `slackBlockKitSchema` | The full JSON Schema as a parsed object. `$id` is `https://tightknit.com/schemas/slack-block-kit.schema.json`. |
|
|
132
|
+
|
|
133
|
+
## Coverage
|
|
134
|
+
|
|
135
|
+
- **18 blocks**: actions, alert, card, carousel, context, context_actions, divider, file, header, image, input, markdown, plan, rich_text, section, table, task_card, video.
|
|
136
|
+
- **All block elements**: button, icon_button, workflow_button, feedback_buttons, plain_text / email / url / number inputs, datepicker, datetimepicker, timepicker, file_input, rich_text_input, checkboxes, radio_buttons, image, overflow, url source, and all 5 single + 5 multi-select menu variants.
|
|
137
|
+
- **All 9 composition objects**: text (plain_text + mrkdwn), confirm, option (3 contextual variants), option_group, slack_file, dispatch_action_config, conversation_filter, trigger, workflow.
|
|
138
|
+
- **Rich text**: 4 container kinds (section, list, preformatted, quote) + 10 leaf kinds (text, link, user, usergroup, team, channel, emoji, broadcast, color, date) with style flags.
|
|
139
|
+
- **View envelopes**: `modal_view` + `home_view` under `$defs`.
|
|
140
|
+
- **Cross-payload rules** (via helpers): dup `block_id`, cumulative markdown, single-table, `focus_on_load` uniqueness, surface compatibility.
|
|
141
|
+
|
|
142
|
+
Every documented `maxLength`, regex (date / time / user ID / channel ID / team ID format), enum value, and array cardinality limit is enforced structurally.
|
|
143
|
+
|
|
144
|
+
## What isn't enforced
|
|
145
|
+
|
|
146
|
+
Server-side rules that need app-config context or deep equality checks the schema doesn't attempt:
|
|
147
|
+
|
|
148
|
+
- Slack OAuth scope requirements (e.g. `links.embed:write` for video blocks).
|
|
149
|
+
- Initial-value matching (e.g. `radio_buttons.initial_option` must equal one of `options` by deep equality).
|
|
150
|
+
- `video_url` must match the app's configured unfurl domains.
|
|
151
|
+
- 10 MB per-file limit on `file_input` uploads.
|
|
152
|
+
- Slack's `block_id`-must-not-start-with `block_` rule is folklore (not stated on any current docs page) and intentionally not enforced.
|
|
153
|
+
|
|
154
|
+
## Note on `undefined` properties
|
|
155
|
+
|
|
156
|
+
`validateBlockKit` strips properties whose value is `undefined` before running Ajv. `JSON.stringify` drops these before the payload reaches Slack, so common builder patterns like `value: foo ?? undefined` are no-ops on the wire but would otherwise trip the schema's `additionalProperties: false`. Explicit `null` values are preserved — they survive `JSON.stringify` and may legitimately fail the schema.
|
|
157
|
+
|
|
158
|
+
## Runtime & bundle considerations
|
|
159
|
+
|
|
160
|
+
Ajv v8 + ajv-formats weigh ~50–60 KB gzipped, which is fine for Node test runs but noticeable in a Cloudflare Worker bundle. If you want runtime validation inside a Worker, use the package's `compile:standalone` script to emit a self-contained validator that doesn't import Ajv at runtime:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
pnpm dlx ajv compile --spec=draft2020 --strict=false -c ajv-formats \
|
|
164
|
+
-s node_modules/@tightknitai/slack-block-kit-validator/dist/slack-block-kit.schema.json \
|
|
165
|
+
-o standalone-validator.js
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For most apps the recommended pattern is **validate in tests only** — use `validateBlockKit` in unit tests next to your block builders, and let CI catch regressions before they hit production.
|
|
169
|
+
|
|
170
|
+
## Keeping up with Slack
|
|
171
|
+
|
|
172
|
+
Slack adds new block types and fields over time. To update:
|
|
173
|
+
|
|
174
|
+
1. Diff the latest at <https://docs.slack.dev/reference/block-kit> against the commit history of `src/slack-block-kit.schema.json`.
|
|
175
|
+
2. Add or amend the relevant `$defs/*` entry, including required / optional / maxLength / enum.
|
|
176
|
+
3. For new top-level blocks, add to `$defs/block.oneOf` and the `modal_view` / `home_view` inner arrays if applicable.
|
|
177
|
+
4. For new elements, add to the relevant parent-container whitelist (`actions_block_element`, `section_accessory_element`, `input_block_element`, `context_block_element`, or `context_actions_block_element`).
|
|
178
|
+
5. Add fixtures to `test/` covering both valid and invalid payloads.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT. See [LICENSE](./LICENSE).
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
Maintained by the [Tightknit](https://tightknit.ai) team.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maximum number of action buttons allowed inside a single `card` block.
|
|
3
|
+
*/
|
|
4
|
+
export declare const CARD_ACTIONS_MAX = 2;
|
|
5
|
+
/**
|
|
6
|
+
* Checks that every `card` block contains at most {@link CARD_ACTIONS_MAX}
|
|
7
|
+
* action buttons. Slack's card surface only renders up to two action buttons
|
|
8
|
+
* in the action row; additional buttons are clipped or rejected.
|
|
9
|
+
* @param blocks - array of Block Kit blocks
|
|
10
|
+
* @returns array of error messages (empty when no card exceeds the limit)
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkCardActionsMax(blocks: readonly {
|
|
13
|
+
type?: string;
|
|
14
|
+
actions?: readonly unknown[];
|
|
15
|
+
}[]): string[];
|
|
16
|
+
//# sourceMappingURL=check-card-actions-max.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-card-actions-max.d.ts","sourceRoot":"","sources":["../../src/helpers/check-card-actions-max.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;CAAE,EAAE,GAAG,MAAM,EAAE,CAchH"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maximum number of action buttons allowed inside a single `card` block.
|
|
3
|
+
*/
|
|
4
|
+
export const CARD_ACTIONS_MAX = 2;
|
|
5
|
+
/**
|
|
6
|
+
* Checks that every `card` block contains at most {@link CARD_ACTIONS_MAX}
|
|
7
|
+
* action buttons. Slack's card surface only renders up to two action buttons
|
|
8
|
+
* in the action row; additional buttons are clipped or rejected.
|
|
9
|
+
* @param blocks - array of Block Kit blocks
|
|
10
|
+
* @returns array of error messages (empty when no card exceeds the limit)
|
|
11
|
+
*/
|
|
12
|
+
export function checkCardActionsMax(blocks) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
blocks.forEach((block, i) => {
|
|
15
|
+
if (block?.type !== "card") {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const actions = block.actions;
|
|
19
|
+
if (Array.isArray(actions) && actions.length > CARD_ACTIONS_MAX) {
|
|
20
|
+
errors.push(`blocks[${i}].actions card blocks allow at most ${CARD_ACTIONS_MAX} action buttons — found ${actions.length}`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return errors;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=check-card-actions-max.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-card-actions-max.js","sourceRoot":"","sources":["../../src/helpers/check-card-actions-max.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAkE;IACpG,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1B,IAAI,KAAK,EAAE,IAAI,KAAK,MAAM,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;YAChE,MAAM,CAAC,IAAI,CACT,UAAU,CAAC,uCAAuC,gBAAgB,2BAA2B,OAAO,CAAC,MAAM,EAAE,CAC9G,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks the cumulative character length across all `markdown` blocks in a
|
|
3
|
+
* payload. Slack caps it at 12,000 chars per payload; the schema's per-block
|
|
4
|
+
* `maxLength` can't express this across siblings.
|
|
5
|
+
* @param blocks - array of Block Kit blocks
|
|
6
|
+
* @returns array of error messages (empty when under the limit)
|
|
7
|
+
*/
|
|
8
|
+
export declare function checkCumulativeMarkdownLength(blocks: readonly {
|
|
9
|
+
type?: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
}[]): string[];
|
|
12
|
+
//# sourceMappingURL=check-cumulative-markdown-length.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-cumulative-markdown-length.d.ts","sourceRoot":"","sources":["../../src/helpers/check-cumulative-markdown-length.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,MAAM,EAAE,CAa3G"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const CUMULATIVE_MARKDOWN_LIMIT = 12_000;
|
|
2
|
+
/**
|
|
3
|
+
* Checks the cumulative character length across all `markdown` blocks in a
|
|
4
|
+
* payload. Slack caps it at 12,000 chars per payload; the schema's per-block
|
|
5
|
+
* `maxLength` can't express this across siblings.
|
|
6
|
+
* @param blocks - array of Block Kit blocks
|
|
7
|
+
* @returns array of error messages (empty when under the limit)
|
|
8
|
+
*/
|
|
9
|
+
export function checkCumulativeMarkdownLength(blocks) {
|
|
10
|
+
const total = blocks.reduce((sum, b) => {
|
|
11
|
+
if (b?.type !== "markdown" || typeof b.text !== "string") {
|
|
12
|
+
return sum;
|
|
13
|
+
}
|
|
14
|
+
return sum + b.text.length;
|
|
15
|
+
}, 0);
|
|
16
|
+
if (total > CUMULATIVE_MARKDOWN_LIMIT) {
|
|
17
|
+
return [
|
|
18
|
+
`cumulative markdown block text is ${total} chars — maximum across a payload is ${CUMULATIVE_MARKDOWN_LIMIT}`,
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=check-cumulative-markdown-length.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-cumulative-markdown-length.js","sourceRoot":"","sources":["../../src/helpers/check-cumulative-markdown-length.ts"],"names":[],"mappings":"AAAA,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B,CAAC,MAAmD;IAC/F,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QACrC,IAAI,CAAC,EAAE,IAAI,KAAK,UAAU,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACzD,OAAO,GAAG,CAAC;QACb,CAAC;QACD,OAAO,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;IAC7B,CAAC,EAAE,CAAC,CAAC,CAAC;IACN,IAAI,KAAK,GAAG,yBAAyB,EAAE,CAAC;QACtC,OAAO;YACL,qCAAqC,KAAK,wCAAwC,yBAAyB,EAAE;SAC9G,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack allows at most one interactive element with `focus_on_load: true` per
|
|
3
|
+
* view/payload. The walker recursively descends into every nested object and
|
|
4
|
+
* array and counts any node whose `focus_on_load` is literally `true` — it
|
|
5
|
+
* doesn't gate on element type, so for well-formed payloads this is
|
|
6
|
+
* equivalent to "one focused element per view." The walk is depth-limited so
|
|
7
|
+
* pathological inputs can't blow the stack.
|
|
8
|
+
* @param blocks - array of Block Kit blocks or anything with a nested shape
|
|
9
|
+
* @returns array of error messages (empty when zero or one node has `focus_on_load: true`)
|
|
10
|
+
*/
|
|
11
|
+
export declare function checkFocusOnLoadUniqueness(blocks: readonly unknown[]): string[];
|
|
12
|
+
//# sourceMappingURL=check-focus-on-load-uniqueness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-focus-on-load-uniqueness.d.ts","sourceRoot":"","sources":["../../src/helpers/check-focus-on-load-uniqueness.ts"],"names":[],"mappings":"AAQA;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,EAAE,CAoC/E"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cap for recursive traversal of block payloads. Real Slack payloads are
|
|
3
|
+
* bounded by their structural limits (≤100 blocks, ≤25 elements per block,
|
|
4
|
+
* etc.), so genuine nesting never comes close to 50. Stop there to guard
|
|
5
|
+
* against adversarial or malformed inputs.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_WALK_DEPTH = 50;
|
|
8
|
+
/**
|
|
9
|
+
* Slack allows at most one interactive element with `focus_on_load: true` per
|
|
10
|
+
* view/payload. The walker recursively descends into every nested object and
|
|
11
|
+
* array and counts any node whose `focus_on_load` is literally `true` — it
|
|
12
|
+
* doesn't gate on element type, so for well-formed payloads this is
|
|
13
|
+
* equivalent to "one focused element per view." The walk is depth-limited so
|
|
14
|
+
* pathological inputs can't blow the stack.
|
|
15
|
+
* @param blocks - array of Block Kit blocks or anything with a nested shape
|
|
16
|
+
* @returns array of error messages (empty when zero or one node has `focus_on_load: true`)
|
|
17
|
+
*/
|
|
18
|
+
export function checkFocusOnLoadUniqueness(blocks) {
|
|
19
|
+
const paths = [];
|
|
20
|
+
const walk = (node, path, depth) => {
|
|
21
|
+
if (node == null || depth > MAX_WALK_DEPTH) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(node)) {
|
|
25
|
+
node.forEach((item, i) => {
|
|
26
|
+
walk(item, `${path}[${i}]`, depth + 1);
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (typeof node !== "object") {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const obj = node;
|
|
34
|
+
if (obj.focus_on_load === true) {
|
|
35
|
+
paths.push(path || "(root)");
|
|
36
|
+
}
|
|
37
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
38
|
+
if (key === "focus_on_load") {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
walk(value, `${path}.${key}`, depth + 1);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
blocks.forEach((block, i) => {
|
|
45
|
+
walk(block, `blocks[${i}]`, 0);
|
|
46
|
+
});
|
|
47
|
+
if (paths.length > 1) {
|
|
48
|
+
return [`only one element per view may have 'focus_on_load: true' — found ${paths.length} at ${paths.join(", ")}`];
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=check-focus-on-load-uniqueness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-focus-on-load-uniqueness.js","sourceRoot":"","sources":["../../src/helpers/check-focus-on-load-uniqueness.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B,CAAC,MAA0B;IACnE,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,MAAM,IAAI,GAAG,CAAC,IAAa,EAAE,IAAY,EAAE,KAAa,EAAQ,EAAE;QAChE,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,GAAG,cAAc,EAAE,CAAC;YAC3C,OAAO;QACT,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;gBACvB,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;YACzC,CAAC,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,IAA+B,CAAC;QAC5C,IAAI,GAAG,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YAC/B,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAC;QAC/B,CAAC;QACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/C,IAAI,GAAG,KAAK,eAAe,EAAE,CAAC;gBAC5B,SAAS;YACX,CAAC;YACD,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,IAAI,GAAG,EAAE,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1B,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,oEAAoE,KAAK,CAAC,MAAM,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack's number_input docs say:
|
|
3
|
+
* "The minimum value, cannot be greater than max_value."
|
|
4
|
+
* "The maximum value, cannot be less than min_value."
|
|
5
|
+
* JSON Schema can't compare two sibling string-encoded numbers natively, so
|
|
6
|
+
* this helper walks the blocks array (bounded by MAX_WALK_DEPTH) and reports
|
|
7
|
+
* any number_input where min_value > max_value.
|
|
8
|
+
* @param blocks - array of Block Kit blocks or anything with a nested shape
|
|
9
|
+
* @returns array of human-readable error messages (empty when valid)
|
|
10
|
+
*/
|
|
11
|
+
export declare function checkNumberInputBounds(blocks: readonly unknown[]): string[];
|
|
12
|
+
//# sourceMappingURL=check-number-input-bounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-number-input-bounds.d.ts","sourceRoot":"","sources":["../../src/helpers/check-number-input-bounds.ts"],"names":[],"mappings":"AAwEA;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,EAAE,CA2B3E"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cap for recursive traversal of block payloads. Real Slack payloads are
|
|
3
|
+
* bounded by their structural limits (≤100 blocks, ≤25 elements per block,
|
|
4
|
+
* etc.), so genuine nesting never comes close to 50. Stop there to guard
|
|
5
|
+
* against adversarial or malformed inputs.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_WALK_DEPTH = 50;
|
|
8
|
+
/**
|
|
9
|
+
* Inspect a single node: if it looks like a `number_input` element whose
|
|
10
|
+
* `min_value` exceeds its `max_value`, push a human-readable error onto the
|
|
11
|
+
* accumulator. Non-matching nodes are ignored — recursion is the walker's job.
|
|
12
|
+
* @param node - the possibly-number_input node to inspect
|
|
13
|
+
* @param path - dotted path to the node for use in error messages
|
|
14
|
+
* @param errors - accumulator mutated in place with any error string
|
|
15
|
+
*/
|
|
16
|
+
const reportBoundsIssue = (node, path, errors) => {
|
|
17
|
+
if (node?.type !== "number_input") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (typeof node.min_value !== "string" || typeof node.max_value !== "string") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const min = Number(node.min_value);
|
|
24
|
+
const max = Number(node.max_value);
|
|
25
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (min > max) {
|
|
29
|
+
errors.push(`${path} number_input min_value (${node.min_value}) cannot be greater than max_value (${node.max_value})`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Inspect a `number_input` node with `is_decimal_allowed: false` and flag any
|
|
34
|
+
* decimal string in `min_value`, `max_value`, or `initial_value`. Slack rejects
|
|
35
|
+
* decimal bounds/initial values on integer-only number inputs server-side, even
|
|
36
|
+
* though the docs don't spell out the constraint in plain language.
|
|
37
|
+
* @param node - the possibly-number_input node to inspect
|
|
38
|
+
* @param path - dotted path to the node for use in error messages
|
|
39
|
+
* @param errors - accumulator mutated in place with any error string
|
|
40
|
+
*/
|
|
41
|
+
const reportIntegerOnlyIssue = (node, path, errors) => {
|
|
42
|
+
if (node?.type !== "number_input") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (node.is_decimal_allowed !== false) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const integerOnlyFields = ["min_value", "max_value", "initial_value"];
|
|
49
|
+
for (const field of integerOnlyFields) {
|
|
50
|
+
const raw = node[field];
|
|
51
|
+
if (typeof raw !== "string") {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const parsed = Number(raw);
|
|
55
|
+
if (Number.isFinite(parsed) && !Number.isInteger(parsed)) {
|
|
56
|
+
errors.push(`${path} number_input ${field} ('${raw}') must be an integer when is_decimal_allowed is false`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Slack's number_input docs say:
|
|
62
|
+
* "The minimum value, cannot be greater than max_value."
|
|
63
|
+
* "The maximum value, cannot be less than min_value."
|
|
64
|
+
* JSON Schema can't compare two sibling string-encoded numbers natively, so
|
|
65
|
+
* this helper walks the blocks array (bounded by MAX_WALK_DEPTH) and reports
|
|
66
|
+
* any number_input where min_value > max_value.
|
|
67
|
+
* @param blocks - array of Block Kit blocks or anything with a nested shape
|
|
68
|
+
* @returns array of human-readable error messages (empty when valid)
|
|
69
|
+
*/
|
|
70
|
+
export function checkNumberInputBounds(blocks) {
|
|
71
|
+
const errors = [];
|
|
72
|
+
const walk = (node, path, depth) => {
|
|
73
|
+
if (node == null || depth > MAX_WALK_DEPTH) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(node)) {
|
|
77
|
+
node.forEach((item, i) => {
|
|
78
|
+
walk(item, `${path}[${i}]`, depth + 1);
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (typeof node !== "object") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
reportBoundsIssue(node, path, errors);
|
|
86
|
+
reportIntegerOnlyIssue(node, path, errors);
|
|
87
|
+
for (const [key, value] of Object.entries(node)) {
|
|
88
|
+
walk(value, `${path}.${key}`, depth + 1);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
blocks.forEach((block, i) => {
|
|
92
|
+
walk(block, `blocks[${i}]`, 0);
|
|
93
|
+
});
|
|
94
|
+
return errors;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=check-number-input-bounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-number-input-bounds.js","sourceRoot":"","sources":["../../src/helpers/check-number-input-bounds.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Surface } from "./check-surface-compatibility";
|
|
2
|
+
type SelectLike = {
|
|
3
|
+
type?: string;
|
|
4
|
+
response_url_enabled?: boolean;
|
|
5
|
+
};
|
|
6
|
+
type BlockLike = {
|
|
7
|
+
type?: string;
|
|
8
|
+
accessory?: SelectLike;
|
|
9
|
+
element?: SelectLike;
|
|
10
|
+
elements?: SelectLike[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Enforces Slack's `response_url_enabled` restriction:
|
|
14
|
+
* "This field only works with menus in input blocks in modals."
|
|
15
|
+
* (Docs: conversations_select / channels_select elements.)
|
|
16
|
+
*
|
|
17
|
+
* Flags any usage on a non-modal surface, and any usage outside an `input`
|
|
18
|
+
* block (e.g. as a section `accessory` or inside an `actions` block).
|
|
19
|
+
* @param blocks - array of Block Kit blocks
|
|
20
|
+
* @param surface - target surface (used to reject home/message usage)
|
|
21
|
+
* @returns array of human-readable error messages (empty when valid)
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkResponseUrlEnabledContext(blocks: readonly BlockLike[], surface: Surface | undefined): string[];
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=check-response-url-enabled-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-response-url-enabled-context.d.ts","sourceRoot":"","sources":["../../src/helpers/check-response-url-enabled-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAE7D,KAAK,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AACpE,KAAK,SAAS,GAAG;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;CACzB,CAAC;AAUF;;;;;;;;;;GAUG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,EAAE,CAgCnH"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const SELECT_TYPES_WITH_RESPONSE_URL = new Set(["conversations_select", "channels_select"]);
|
|
2
|
+
const hasResponseUrlEnabled = (node) => !!node &&
|
|
3
|
+
node.response_url_enabled === true &&
|
|
4
|
+
typeof node.type === "string" &&
|
|
5
|
+
SELECT_TYPES_WITH_RESPONSE_URL.has(node.type);
|
|
6
|
+
/**
|
|
7
|
+
* Enforces Slack's `response_url_enabled` restriction:
|
|
8
|
+
* "This field only works with menus in input blocks in modals."
|
|
9
|
+
* (Docs: conversations_select / channels_select elements.)
|
|
10
|
+
*
|
|
11
|
+
* Flags any usage on a non-modal surface, and any usage outside an `input`
|
|
12
|
+
* block (e.g. as a section `accessory` or inside an `actions` block).
|
|
13
|
+
* @param blocks - array of Block Kit blocks
|
|
14
|
+
* @param surface - target surface (used to reject home/message usage)
|
|
15
|
+
* @returns array of human-readable error messages (empty when valid)
|
|
16
|
+
*/
|
|
17
|
+
export function checkResponseUrlEnabledContext(blocks, surface) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
blocks.forEach((block, i) => {
|
|
20
|
+
// Section accessory — never a valid host.
|
|
21
|
+
if (hasResponseUrlEnabled(block.accessory)) {
|
|
22
|
+
errors.push(`blocks[${i}].accessory sets response_url_enabled — only valid on select menus inside input blocks in modals`);
|
|
23
|
+
}
|
|
24
|
+
// Actions block elements — never a valid host.
|
|
25
|
+
if (Array.isArray(block.elements)) {
|
|
26
|
+
block.elements.forEach((el, j) => {
|
|
27
|
+
if (hasResponseUrlEnabled(el)) {
|
|
28
|
+
errors.push(`blocks[${i}].elements[${j}] sets response_url_enabled — only valid on select menus inside input blocks in modals`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Input block element — valid ONLY when surface is explicitly 'modal'.
|
|
33
|
+
// An undefined surface means we have no confirmation this is a modal
|
|
34
|
+
// payload, so we flag it conservatively rather than assume modal intent.
|
|
35
|
+
if (block.type === "input" && hasResponseUrlEnabled(block.element) && surface !== "modal") {
|
|
36
|
+
const surfaceLabel = surface ?? "unspecified";
|
|
37
|
+
errors.push(`blocks[${i}].element sets response_url_enabled on surface '${surfaceLabel}' — only valid when surface is explicitly 'modal'`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return errors;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=check-response-url-enabled-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-response-url-enabled-context.js","sourceRoot":"","sources":["../../src/helpers/check-response-url-enabled-context.ts"],"names":[],"mappings":"AAUA,MAAM,8BAA8B,GAAG,IAAI,GAAG,CAAC,CAAC,sBAAsB,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAE5F,MAAM,qBAAqB,GAAG,CAAC,IAA4B,EAAW,EAAE,CACtE,CAAC,CAAC,IAAI;IACN,IAAI,CAAC,oBAAoB,KAAK,IAAI;IAClC,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;IAC7B,8BAA8B,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,8BAA8B,CAAC,MAA4B,EAAE,OAA4B;IACvG,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1B,0CAA0C;QAC1C,IAAI,qBAAqB,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3C,MAAM,CAAC,IAAI,CACT,UAAU,CAAC,kGAAkG,CAC9G,CAAC;QACJ,CAAC;QACD,+CAA+C;QAC/C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;gBAC/B,IAAI,qBAAqB,CAAC,EAAE,CAAC,EAAE,CAAC;oBAC9B,MAAM,CAAC,IAAI,CACT,UAAU,CAAC,cAAc,CAAC,wFAAwF,CACnH,CAAC;gBACJ,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QACD,uEAAuE;QACvE,qEAAqE;QACrE,yEAAyE;QACzE,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,qBAAqB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;YAC1F,MAAM,YAAY,GAAG,OAAO,IAAI,aAAa,CAAC;YAC9C,MAAM,CAAC,IAAI,CACT,UAAU,CAAC,mDAAmD,YAAY,mDAAmD,CAC9H,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks that a payload contains at most one `table` block. Slack rejects
|
|
3
|
+
* multi-table messages with `invalid_attachments` / `only_one_table_allowed`.
|
|
4
|
+
* @param blocks - array of Block Kit blocks
|
|
5
|
+
* @returns array of error messages (empty when zero or one `table` block present)
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkSingleTableBlock(blocks: readonly {
|
|
8
|
+
type?: string;
|
|
9
|
+
}[]): string[];
|
|
10
|
+
//# sourceMappingURL=check-single-table-block.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-single-table-block.d.ts","sourceRoot":"","sources":["../../src/helpers/check-single-table-block.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,MAAM,EAAE,CAWpF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks that a payload contains at most one `table` block. Slack rejects
|
|
3
|
+
* multi-table messages with `invalid_attachments` / `only_one_table_allowed`.
|
|
4
|
+
* @param blocks - array of Block Kit blocks
|
|
5
|
+
* @returns array of error messages (empty when zero or one `table` block present)
|
|
6
|
+
*/
|
|
7
|
+
export function checkSingleTableBlock(blocks) {
|
|
8
|
+
const indices = [];
|
|
9
|
+
blocks.forEach((block, i) => {
|
|
10
|
+
if (block?.type === "table") {
|
|
11
|
+
indices.push(i);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
if (indices.length > 1) {
|
|
15
|
+
return [`only one 'table' block is allowed per message — found ${indices.length} at indices ${indices.join(", ")}`];
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=check-single-table-block.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-single-table-block.js","sourceRoot":"","sources":["../../src/helpers/check-single-table-block.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAoC;IACxE,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1B,IAAI,KAAK,EAAE,IAAI,KAAK,OAAO,EAAE,CAAC;YAC5B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IACH,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,yDAAyD,OAAO,CAAC,MAAM,eAAe,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surfaces where Block Kit payloads are rendered. Each has a different set of
|
|
3
|
+
* blocks/elements it accepts.
|
|
4
|
+
*/
|
|
5
|
+
export type Surface = "message" | "modal" | "home";
|
|
6
|
+
/**
|
|
7
|
+
* Reports any blocks whose type is not allowed on the given surface. Slack's
|
|
8
|
+
* surface compatibility matrix is documented at
|
|
9
|
+
* https://docs.slack.dev/surfaces/. Element-level restrictions (e.g. `file_input`
|
|
10
|
+
* only valid in modals) are checked here by looking at each input block's inner
|
|
11
|
+
* `element.type`.
|
|
12
|
+
* @param blocks - array of Block Kit blocks
|
|
13
|
+
* @param surface - target surface
|
|
14
|
+
* @returns array of error messages (empty when all blocks are surface-compatible)
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkSurfaceCompatibility(blocks: readonly {
|
|
17
|
+
type?: string;
|
|
18
|
+
element?: {
|
|
19
|
+
type?: string;
|
|
20
|
+
};
|
|
21
|
+
}[], surface: Surface): string[];
|
|
22
|
+
//# sourceMappingURL=check-surface-compatibility.d.ts.map
|