@tightknitai/block-kitchen 0.6.1-alpha.0 → 0.8.2

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/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  [![CI](https://github.com/TightknitAI/block-kitchen/actions/workflows/ci.yml/badge.svg)](https://github.com/TightknitAI/block-kitchen/actions/workflows/ci.yml)
8
8
  [![npm version](https://img.shields.io/npm/v/@tightknitai/block-kitchen.svg)](https://www.npmjs.com/package/@tightknitai/block-kitchen)
9
+ [![minzipped size](https://img.shields.io/bundlephobia/minzip/@tightknitai/block-kitchen)](https://bundlephobia.com/package/@tightknitai/block-kitchen)
9
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
10
11
  [![Live demo](https://img.shields.io/badge/demo-live-6366f1)](https://block-kitchen.tightknit.dev)
11
12
 
@@ -19,7 +20,7 @@ Inspired by [Slack's Block Kit Builder](https://app.slack.com/block-kit-builder)
19
20
  <img src="./assets/screenshot.png" alt="block-kitchen demo screenshot" width="900" />
20
21
  </p>
21
22
 
22
- The package owns the entire builder UX — palette, sortable preview surface, per-block popover editors, send dialog. It knows nothing about how channels are listed, who the user is, or how messages are sent. The consumer wires those concerns through callback props. A working end-to-end app is shown in [block-kit-builder-template](https://github.com/TightknitAI/block-kit-builder-template).
23
+ The package owns the entire builder UX — palette, sortable preview surface, per-block popover editors, send dialog. It knows nothing about how channels are listed, who the user is, or how messages are sent. The consumer wires those concerns through callback props. A working end-to-end app is shown in [block-kitchen-template](https://github.com/TightknitAI/block-kitchen-template).
23
24
 
24
25
  ## Install
25
26
 
@@ -27,10 +28,48 @@ The package owns the entire builder UX — palette, sortable preview surface, pe
27
28
  pnpm add @tightknitai/block-kitchen
28
29
  ```
29
30
 
30
- Peer deps: `react`, `react-dom`.
31
+ Modern package managers (pnpm 8+, npm 7+, yarn berry) auto-install peer
32
+ dependencies — no extra command needed. The peer set is deliberately
33
+ narrow: only the dependencies a typical consumer is likely to *already*
34
+ have, where deduplication gives them a real win:
35
+
36
+ - `react` `^18 || ^19`, `react-dom` `^18 || ^19`
37
+ - `@radix-ui/react-{dialog,label,popover,radio-group,slot,tooltip}` —
38
+ the dialog/popover/tooltip/sheet primitives the builder uses. If
39
+ you already use shadcn/ui you already have these, and peer
40
+ deduplication avoids two Radix copies (which would split React
41
+ context — a `<TooltipProvider>` from your copy can't reach a
42
+ `<Tooltip>` from ours).
43
+ - `lucide-react` — every icon in the toolbar, palette, and editors.
44
+ Universal in modern React UI stacks; peer keeps a single copy.
45
+
46
+ Everything else (`@dnd-kit/*`, `@tiptap/*`, `slack-blocks-to-jsx`,
47
+ `@tightknitai/slack-block-kit-validator`) is a regular dependency.
48
+ Most consumers don't already have these, so there's no dedupe benefit
49
+ to forcing them into a peer position — and you don't have to think
50
+ about their versions.
51
+
52
+ If your package manager doesn't auto-install peers:
53
+
54
+ ```bash
55
+ pnpm add @tightknitai/block-kitchen \
56
+ react react-dom \
57
+ @radix-ui/react-dialog @radix-ui/react-label \
58
+ @radix-ui/react-popover @radix-ui/react-radio-group \
59
+ @radix-ui/react-slot @radix-ui/react-tooltip \
60
+ lucide-react
61
+ ```
31
62
 
32
63
  ## Usage
33
64
 
65
+ > **Import the stylesheet once, at app root.** The builder mounts
66
+ > dialogs, popovers, tooltips, and the mobile palette sheet via React
67
+ > portals, which render to `document.body` — outside the route tree.
68
+ > If you import `styles.css` inside a single route module, those
69
+ > portal contents will be unstyled on every other route that doesn't
70
+ > import it. Put the import in your root layout (Next.js `app/layout.tsx`,
71
+ > Remix `app/root.tsx`, Vite `src/main.tsx`, etc.).
72
+
34
73
  ```tsx
35
74
  import { BlockKitchen } from "@tightknitai/block-kitchen";
36
75
  import "@tightknitai/block-kitchen/styles.css";
@@ -71,6 +110,7 @@ export function MyBuilderPage() {
71
110
  | `onSend` | `(payload) => Promise<{ ok: boolean; error?: string }>` | yes | Called when the user submits the send dialog. Payload is `{ channelId, blocks, sendAsUser }`. |
72
111
  | `previewHooks` | `PreviewHooks` | no | Hooks forwarded to `slack-blocks-to-jsx`'s `<Message>` for resolving user / channel / emoji directives. |
73
112
  | `palette` | `PaletteSection[]` | no | The left-hand palette of draggable variants. Defaults to `defaultPalette`. Spread it to filter, reorder, or add your own pre-configured variants — see [Customizing the palette](#customizing-the-palette). |
113
+ | `disabledBlockTypes` | `SupportedBlockType[]` | no | Block types to hide from the palette without rebuilding it. Filters at the variant level — a section keeps any variants whose block types aren't disabled; sections that end up empty are dropped. Convenient when you want the default palette minus a few types (e.g. `['image', 'table']` for a text-only builder). |
74
114
  | `defaultOpenSections` | `boolean \| string[]` | no | Which palette section headers are expanded on first paint. `true` (default) opens all sections; `false` collapses all (Slack-style); an array opens only sections whose `name` is in the list (e.g. `['Section', 'Actions']`). The palette also has a built-in search input that expands matching sections on demand. |
75
115
  | `showPaletteSearch` | `boolean` | no | Whether the palette renders the quick-search input above the section list. Defaults to `true`. Set `false` for compact palettes (e.g. when you've passed a small custom `palette`) where scanning by eye is faster than typing. |
76
116
  | `paletteSearchPlaceholder` | `string` | no | Placeholder text for the palette search input. Defaults to `'Search blocks…'`. Useful for localization. |
@@ -143,7 +183,7 @@ import type {
143
183
 
144
184
  ## Backend
145
185
 
146
- The builder is frontend-only. For a full app that handles OAuth, channel listing, and `chat.postMessage`, see [block-kit-builder-template](https://github.com/TightknitAI/block-kit-builder-template) — a Vite + React SPA on Cloudflare Workers that wires this package to [slack-hono](https://github.com/TightknitAI/slack-hono) on the backend.
186
+ The builder is frontend-only. For a full app that handles OAuth, channel listing, and `chat.postMessage`, see [block-kitchen-template](https://github.com/TightknitAI/block-kitchen-template) — a Vite + React SPA on Cloudflare Workers that wires this package to [slack-hono](https://github.com/TightknitAI/slack-hono) on the backend.
147
187
 
148
188
  ## Validation
149
189
 
@@ -183,6 +223,85 @@ The lower-level CSS-variable contract above keeps working; the `theme` prop simp
183
223
 
184
224
  Fonts are deliberately not part of `BrandTheme`. The builder sets no `font-family` of its own (aside from `font-mono` on the JSON viewer, which is intentional), so it inherits whatever the host page declares on `<html>` or `<body>`. Set your brand typography globally and the builder will pick it up automatically — no additional configuration needed. The Slack preview surface continues to render with Slack's own typography via `slack-blocks-to-jsx`.
185
225
 
226
+ ## Frameworks & SSR
227
+
228
+ The builder is client-only by design — it uses drag sensors, contentEditable
229
+ (TipTap), portals, and `useEffect`-driven state. It cannot be statically
230
+ rendered on the server. The component still ships fine inside SSR/SSG
231
+ frameworks; just mark its tree as client-side.
232
+
233
+ - **Next.js (App Router)** — put `'use client'` at the top of the file
234
+ that renders `<BlockKitchen>`. Import `styles.css` from
235
+ `app/layout.tsx` (the root layout) so portal content stays styled
236
+ on every route.
237
+ - **Next.js (Pages Router)** — render the component inside a page
238
+ module; the bundled-client default works. Import `styles.css` from
239
+ `pages/_app.tsx`.
240
+ - **Remix / React Router** — render inside any route component;
241
+ put the `styles.css` import in `app/root.tsx`. If you ship the
242
+ builder on a single route, import the stylesheet there *and*
243
+ in `root.tsx` so portals on other routes don't render unstyled.
244
+ - **Vite SPA** — import `styles.css` once from `src/main.tsx`.
245
+ - **Astro** — load the React component with `client:only="react"`.
246
+
247
+ The package exports React JSX with the automatic runtime, so any
248
+ JSX-transform-aware bundler from the last few years works without
249
+ extra configuration.
250
+
251
+ ## Subpath exports
252
+
253
+ Three import paths are published:
254
+
255
+ ```ts
256
+ // 1. Full builder (default)
257
+ import { BlockKitchen } from "@tightknitai/block-kitchen";
258
+
259
+ // 2. Headless helpers — no React component tree, safe for backends.
260
+ // Use this when you only need to round-trip / validate / encode
261
+ // blocks (e.g. inside a Worker that calls Slack's chat.postMessage).
262
+ import {
263
+ toSlackBlocks,
264
+ encodeBlocksToString,
265
+ decodeBlocksFromString,
266
+ } from "@tightknitai/block-kitchen/helpers";
267
+
268
+ // 3. Palette catalog — for tooling that needs the default variants
269
+ // (e.g. a Storybook story or a config generator) without pulling
270
+ // in the builder.
271
+ import {
272
+ defaultPalette,
273
+ legacyInputVariants,
274
+ extraAlertVariant,
275
+ } from "@tightknitai/block-kitchen/palette";
276
+ ```
277
+
278
+ The root entry (`.`) still re-exports everything from `./helpers` and
279
+ `./palette`, so existing imports keep working — the subpaths are a tree-
280
+ shaking-friendly shortcut, not a breaking split.
281
+
282
+ ## Cascade layer ordering (Tailwind users)
283
+
284
+ The stylesheet emits all utility classes into a named cascade layer:
285
+
286
+ ```css
287
+ @layer bk-theme, bk-utilities;
288
+ ```
289
+
290
+ Per CSS Cascade Level 5, unlayered rules and rules in later-declared
291
+ layers win over `bk-utilities`. In practice this means a consumer who
292
+ imports both this package's `styles.css` and their own Tailwind output
293
+ will see *their* utilities win on any class-name collision (e.g. they
294
+ define `bg-background` differently). No action needed for that
295
+ common case.
296
+
297
+ If you want explicit control — for example, to make this package's
298
+ utilities win, or to layer them alongside a `shadcn/ui` token stack —
299
+ declare the order at the top of your own root stylesheet:
300
+
301
+ ```css
302
+ @layer bk-theme, bk-utilities, theme, base, components, utilities;
303
+ ```
304
+
186
305
  ## License
187
306
 
188
307
  MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ // src/lib/url-safety.ts
4
+ var SAFE_LINK_PROTOCOLS = /^(https?|ircs?|mailto|tel|sms|xmpp)$/i;
5
+ var SAFE_IMAGE_PROTOCOLS = /^(https?)$/i;
6
+ var SAFE_IMAGE_DATA_PREFIX = /^data:image\/(?:png|jpeg|jpg|gif|webp|avif|x-icon|vnd\.microsoft\.icon)(?:;[a-z0-9-]+(?:=[^,;]*)?)*,/i;
7
+ function getScheme(value) {
8
+ const trimmed = value.replace(/^\s+/, "");
9
+ const colon = trimmed.indexOf(":");
10
+ if (colon === -1) {
11
+ return null;
12
+ }
13
+ const slash = trimmed.indexOf("/");
14
+ const question = trimmed.indexOf("?");
15
+ const hash = trimmed.indexOf("#");
16
+ if (slash !== -1 && slash < colon || question !== -1 && question < colon || hash !== -1 && hash < colon) {
17
+ return null;
18
+ }
19
+ return trimmed.slice(0, colon).toLowerCase();
20
+ }
21
+ function isSafeHref(value) {
22
+ if (value === null || value === void 0) {
23
+ return false;
24
+ }
25
+ if (typeof value !== "string") {
26
+ return false;
27
+ }
28
+ if (value.length === 0) {
29
+ return true;
30
+ }
31
+ const scheme = getScheme(value);
32
+ if (scheme === null) {
33
+ return true;
34
+ }
35
+ return SAFE_LINK_PROTOCOLS.test(scheme);
36
+ }
37
+ function isSafeImageSrc(value) {
38
+ if (value === null || value === void 0) {
39
+ return false;
40
+ }
41
+ if (typeof value !== "string") {
42
+ return false;
43
+ }
44
+ if (value.length === 0) {
45
+ return true;
46
+ }
47
+ const scheme = getScheme(value);
48
+ if (scheme === null) {
49
+ return true;
50
+ }
51
+ if (scheme === "data") {
52
+ return SAFE_IMAGE_DATA_PREFIX.test(value);
53
+ }
54
+ return SAFE_IMAGE_PROTOCOLS.test(scheme);
55
+ }
56
+ function sanitizeHref(value) {
57
+ return isSafeHref(value) ? value ?? "" : "";
58
+ }
59
+ function sanitizeImageSrc(value) {
60
+ return isSafeImageSrc(value) ? value ?? "" : "";
61
+ }
62
+
63
+ // src/lib/sanitize-blocks.ts
64
+ var HREF_KEYS = /* @__PURE__ */ new Set(["url"]);
65
+ var IMAGE_KEYS = /* @__PURE__ */ new Set(["image_url"]);
66
+ function sanitizeValue(value) {
67
+ if (value === null || typeof value !== "object") {
68
+ return value;
69
+ }
70
+ if (Array.isArray(value)) {
71
+ let changed = false;
72
+ const out = new Array(value.length);
73
+ for (let i = 0; i < value.length; i++) {
74
+ const next = sanitizeValue(value[i]);
75
+ if (next !== value[i]) {
76
+ changed = true;
77
+ }
78
+ out[i] = next;
79
+ }
80
+ return changed ? out : value;
81
+ }
82
+ const src = value;
83
+ let copy = null;
84
+ for (const key of Object.keys(src)) {
85
+ const original = src[key];
86
+ let next = original;
87
+ if (typeof original === "string") {
88
+ if (HREF_KEYS.has(key)) {
89
+ next = sanitizeHref(original);
90
+ } else if (IMAGE_KEYS.has(key)) {
91
+ next = sanitizeImageSrc(original);
92
+ }
93
+ } else if (typeof original === "object" && original !== null) {
94
+ next = sanitizeValue(original);
95
+ }
96
+ if (next !== original) {
97
+ if (!copy) {
98
+ copy = { ...src };
99
+ }
100
+ copy[key] = next;
101
+ }
102
+ }
103
+ return copy ?? src;
104
+ }
105
+ function sanitizeBlock(block) {
106
+ return sanitizeValue(block);
107
+ }
108
+
109
+ // src/lib/to-slack-blocks.ts
110
+ function toSlackBlocks(blocks) {
111
+ return blocks.map((block) => {
112
+ const safe = sanitizeBlock(block);
113
+ if (safe.type === "header" && "level" in safe) {
114
+ const { level: _omit, ...rest } = safe;
115
+ return rest;
116
+ }
117
+ return safe;
118
+ });
119
+ }
120
+
121
+ // src/lib/url-state.ts
122
+ var MAX_ENCODED_BYTES = 1024 * 1024;
123
+ function encodeBlocksToString(blocks) {
124
+ if (blocks.length === 0) {
125
+ return "";
126
+ }
127
+ const json = JSON.stringify(blocks);
128
+ const utf8Bytes = new TextEncoder().encode(json);
129
+ const chars = new Array(utf8Bytes.length);
130
+ for (let i = 0; i < utf8Bytes.length; i++) {
131
+ chars[i] = String.fromCharCode(utf8Bytes[i]);
132
+ }
133
+ return toBase64Url(btoa(chars.join("")));
134
+ }
135
+ function decodeBlocksFromString(encoded) {
136
+ if (!encoded) {
137
+ return null;
138
+ }
139
+ if (encoded.length > MAX_ENCODED_BYTES) {
140
+ return null;
141
+ }
142
+ try {
143
+ const binary = atob(fromBase64Url(encoded));
144
+ const bytes = new Uint8Array(binary.length);
145
+ for (let i = 0; i < binary.length; i++) {
146
+ bytes[i] = binary.charCodeAt(i);
147
+ }
148
+ const json = new TextDecoder().decode(bytes);
149
+ const parsed = JSON.parse(json);
150
+ if (!Array.isArray(parsed)) {
151
+ return null;
152
+ }
153
+ return parsed;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+ function toBase64Url(b64) {
159
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
160
+ }
161
+ function fromBase64Url(b64url) {
162
+ const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
163
+ const padLen = (4 - b64.length % 4) % 4;
164
+ return b64 + "=".repeat(padLen);
165
+ }
166
+
167
+ exports.decodeBlocksFromString = decodeBlocksFromString;
168
+ exports.encodeBlocksToString = encodeBlocksToString;
169
+ exports.toSlackBlocks = toSlackBlocks;
170
+ //# sourceMappingURL=helpers.cjs.map
171
+ //# sourceMappingURL=helpers.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/url-safety.ts","../src/lib/sanitize-blocks.ts","../src/lib/to-slack-blocks.ts","../src/lib/url-state.ts"],"names":[],"mappings":"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","file":"helpers.cjs","sourcesContent":["/**\n * URL-scheme allowlists used as a defense-in-depth layer against\n * DOM-XSS via user-supplied URLs that flow into `<a href>` and `<img src>`\n * in the preview (rendered by `slack-blocks-to-jsx`, which does not\n * filter URI schemes itself) or in the builder's own chrome (e.g. the\n * \"Sign in with Slack\" link in `SendDialog`).\n *\n * `format: \"uri\"` on the validator schema (AJV / ajv-formats) accepts\n * any RFC-3986 URI including `javascript:` and `data:`, so the validator\n * cannot be relied on for this purpose.\n *\n * Mirrors react-markdown 9's `defaultUrlTransform` approach: parse the\n * scheme, allow the safe set, and treat anything without a recognized\n * scheme as a relative URL (preserved as-is).\n */\n\n/**\n * Schemes safe to use as an `<a href>` target.\n * Mirrors react-markdown's `safeProtocol` set (`https?|ircs?|mailto|xmpp`)\n * plus `tel` and `sms` (Slack's documented link types).\n */\nconst SAFE_LINK_PROTOCOLS = /^(https?|ircs?|mailto|tel|sms|xmpp)$/i;\n\n/**\n * Schemes safe to use as an `<img src>` source. We allow `data:image/*`\n * because Slack itself emits inline images that way for some emoji and\n * file thumbnails; everything else (e.g. `data:text/html`, `data:image/svg+xml`\n * — which can carry script in some contexts) is rejected.\n */\nconst SAFE_IMAGE_PROTOCOLS = /^(https?)$/i;\n// Allow `data:image/<safe-mime>` followed by any sequence of media-type\n// parameters. Each parameter is `;<name>` or `;<name>=<value>` (RFC 2397\n// allows the bare `;base64` token, which has no `=`). The comma\n// terminates the parameter list and starts the payload.\nconst SAFE_IMAGE_DATA_PREFIX =\n /^data:image\\/(?:png|jpeg|jpg|gif|webp|avif|x-icon|vnd\\.microsoft\\.icon)(?:;[a-z0-9-]+(?:=[^,;]*)?)*,/i;\n\n/**\n * Splits a URL into its scheme prefix and tail without invoking the\n * `URL` constructor (which throws on relative URLs and varies in how\n * it normalizes whitespace and unicode). Matches react-markdown's\n * approach: look for the first `:`, `?`, `#`, or `/`; if `:` comes\n * first, that prefix is the scheme.\n * @param value - the candidate URL\n * @returns the lowercase scheme if the URL is absolute, or `null` if relative\n */\nfunction getScheme(value: string): string | null {\n // Strip leading whitespace so `\\tjavascript:` and ` javascript:`\n // still resolve to the `javascript` scheme; browsers ignore leading\n // whitespace when following an href, so the sanitizer must too.\n const trimmed = value.replace(/^\\s+/, '');\n const colon = trimmed.indexOf(':');\n if (colon === -1) {\n return null;\n }\n const slash = trimmed.indexOf('/');\n const question = trimmed.indexOf('?');\n const hash = trimmed.indexOf('#');\n // If any of `/`, `?`, `#` appears before the `:`, then the `:` is\n // inside the path/query/fragment, not a scheme delimiter — treat the\n // URL as relative.\n if ((slash !== -1 && slash < colon) || (question !== -1 && question < colon) || (hash !== -1 && hash < colon)) {\n return null;\n }\n return trimmed.slice(0, colon).toLowerCase();\n}\n\n/**\n * Returns true when `value` is safe to use as an `<a href>` target.\n * Accepts:\n * - absolute URLs whose scheme is in the safe-link allowlist\n * (`http`, `https`, `ircs?`, `mailto`, `tel`, `sms`, `xmpp`).\n * - relative URLs (no scheme).\n * - empty string (caller decides whether to render).\n *\n * Rejects `javascript:`, `data:`, `vbscript:`, `file:`, `about:`,\n * and any other unrecognized scheme.\n * @param value - the candidate URL\n * @returns true when the URL is safe to render as a link\n */\nexport function isSafeHref(value: string | null | undefined): boolean {\n if (value === null || value === undefined) {\n return false;\n }\n if (typeof value !== 'string') {\n return false;\n }\n if (value.length === 0) {\n return true;\n }\n const scheme = getScheme(value);\n if (scheme === null) {\n return true;\n }\n return SAFE_LINK_PROTOCOLS.test(scheme);\n}\n\n/**\n * Returns true when `value` is safe to use as an `<img src>`.\n * Accepts http(s), relative URLs, empty string, and `data:image/*`\n * URLs whose MIME subtype is in a tight allowlist. Rejects\n * `data:image/svg+xml` (can carry script) and everything else.\n * @param value - the candidate URL\n * @returns true when the URL is safe to render as an image\n */\nexport function isSafeImageSrc(value: string | null | undefined): boolean {\n if (value === null || value === undefined) {\n return false;\n }\n if (typeof value !== 'string') {\n return false;\n }\n if (value.length === 0) {\n return true;\n }\n const scheme = getScheme(value);\n if (scheme === null) {\n return true;\n }\n if (scheme === 'data') {\n return SAFE_IMAGE_DATA_PREFIX.test(value);\n }\n return SAFE_IMAGE_PROTOCOLS.test(scheme);\n}\n\n/**\n * Returns `value` if it passes {@link isSafeHref}, otherwise an empty\n * string. Use at render time / payload boundaries so an unsafe URL\n * never reaches the DOM as an `href` attribute.\n * @param value - the candidate URL\n * @returns the URL if safe, or `''`\n */\nexport function sanitizeHref(value: string | null | undefined): string {\n return isSafeHref(value) ? (value ?? '') : '';\n}\n\n/**\n * Returns `value` if it passes {@link isSafeImageSrc}, otherwise an\n * empty string. Use at render time / payload boundaries.\n * @param value - the candidate URL\n * @returns the URL if safe, or `''`\n */\nexport function sanitizeImageSrc(value: string | null | undefined): string {\n return isSafeImageSrc(value) ? (value ?? '') : '';\n}\n","import type { SupportedBlock } from '../types';\nimport { sanitizeHref, sanitizeImageSrc } from './url-safety';\n\n/**\n * Recursively walks an unknown value and rewrites string values at the\n * specified keys via the matching sanitizer. Used to strip dangerous\n * URI schemes (e.g. `javascript:`) from `url`, `image_url`, and\n * rich-text link `url` fields before a Block Kit payload reaches a\n * renderer or is handed back to the consumer.\n *\n * Allocates a new object whenever a child is rewritten; otherwise\n * returns the input unchanged so unaffected payloads are reference-stable.\n *\n * Defensive against prototype-polluted shapes: copies are made via\n * `Object.assign({}, ...)` over own enumerable keys returned by\n * `Object.keys`, which skips inherited properties.\n */\nconst HREF_KEYS = new Set(['url']);\nconst IMAGE_KEYS = new Set(['image_url']);\n\n/**\n * Recursively sanitize all known URL-bearing string fields inside a\n * Slack Block Kit payload fragment. Returns a value with the same\n * structural shape, where any field whose name matches an href or\n * image-src key has been replaced by the safe variant (`''` if the\n * original scheme was unsafe).\n * @param value - any payload fragment (object, array, primitive)\n * @returns the sanitized payload fragment\n */\nfunction sanitizeValue(value: unknown): unknown {\n if (value === null || typeof value !== 'object') {\n return value;\n }\n if (Array.isArray(value)) {\n let changed = false;\n const out = new Array(value.length);\n for (let i = 0; i < value.length; i++) {\n const next = sanitizeValue(value[i]);\n if (next !== value[i]) {\n changed = true;\n }\n out[i] = next;\n }\n return changed ? out : value;\n }\n const src = value as Record<string, unknown>;\n let copy: Record<string, unknown> | null = null;\n for (const key of Object.keys(src)) {\n const original = src[key];\n let next: unknown = original;\n if (typeof original === 'string') {\n if (HREF_KEYS.has(key)) {\n next = sanitizeHref(original);\n } else if (IMAGE_KEYS.has(key)) {\n next = sanitizeImageSrc(original);\n }\n } else if (typeof original === 'object' && original !== null) {\n next = sanitizeValue(original);\n }\n if (next !== original) {\n if (!copy) {\n copy = { ...src };\n }\n copy[key] = next;\n }\n }\n return copy ?? src;\n}\n\n/**\n * Sanitize a single Block Kit block, scrubbing dangerous URI schemes\n * from `url` and `image_url` fields anywhere in the payload tree.\n * @param block - the block payload to sanitize\n * @returns the sanitized block (same reference if nothing changed)\n */\nexport function sanitizeBlock<T extends SupportedBlock>(block: T): T {\n return sanitizeValue(block) as T;\n}\n\n/**\n * Sanitize an array of Block Kit blocks. See {@link sanitizeBlock}.\n * @param blocks - the block payloads to sanitize\n * @returns the sanitized blocks\n */\nexport function sanitizeBlocks(blocks: SupportedBlock[]): SupportedBlock[] {\n let changed = false;\n const out = new Array<SupportedBlock>(blocks.length);\n for (let i = 0; i < blocks.length; i++) {\n const next = sanitizeBlock(blocks[i]);\n if (next !== blocks[i]) {\n changed = true;\n }\n out[i] = next;\n }\n return changed ? out : blocks;\n}\n","import type { SupportedBlock } from '../types';\nimport { sanitizeBlock } from './sanitize-blocks';\n\n/**\n * Strip builder-only fields from blocks so they conform to Slack's\n * Block Kit schema, and scrub dangerous URI schemes from any\n * `url`/`image_url` fields. Currently removes the cosmetic `level`\n * field from header blocks (a builder-only extension Slack would\n * reject) and routes every URL/image-url through the allowlist in\n * `lib/url-safety.ts` so a payload that round-trips through the\n * builder cannot carry `javascript:`/`data:text/html` URIs to a\n * downstream consumer or to the Slack API.\n * @param blocks - the working draft blocks\n * @returns blocks with builder-only fields removed and URLs scrubbed\n */\nexport function toSlackBlocks(blocks: SupportedBlock[]): SupportedBlock[] {\n return blocks.map((block) => {\n const safe = sanitizeBlock(block);\n if (safe.type === 'header' && 'level' in safe) {\n const { level: _omit, ...rest } = safe;\n return rest as SupportedBlock;\n }\n return safe;\n });\n}\n","import type { SupportedBlock } from '../types';\n\n/**\n * Upper bound on the encoded string length we accept on decode. Block\n * Kit JSON in practice is well under 100 KB; we cap at 1 MiB of encoded\n * input so a pathologically large URL hash cannot freeze the tab in\n * `atob` + `JSON.parse` before the validator gets a look. Encoded\n * (base64url) is ~33% larger than the binary, so this bounds the decoded\n * JSON at roughly 768 KiB.\n */\nconst MAX_ENCODED_BYTES = 1024 * 1024;\n\n/**\n * Encodes a block list to a base64url string suitable for use as a URL\n * search param. The consumer chooses where to put it (search param, hash,\n * localStorage, etc).\n * Returns an empty string for an empty list so the consumer can omit the\n * param entirely.\n * @param blocks - the blocks to encode\n * @returns base64url-encoded JSON, or '' if blocks is empty\n */\nexport function encodeBlocksToString(blocks: SupportedBlock[]): string {\n if (blocks.length === 0) {\n return '';\n }\n const json = JSON.stringify(blocks);\n const utf8Bytes = new TextEncoder().encode(json);\n const chars = new Array<string>(utf8Bytes.length);\n for (let i = 0; i < utf8Bytes.length; i++) {\n chars[i] = String.fromCharCode(utf8Bytes[i]);\n }\n return toBase64Url(btoa(chars.join('')));\n}\n\n/**\n * Decodes a string produced by {@link encodeBlocksToString} back to a\n * block list. Returns null on any decode/parse error.\n * @param encoded - the encoded string\n * @returns decoded blocks, or null if input is empty/invalid\n */\nexport function decodeBlocksFromString(encoded: string | undefined | null): SupportedBlock[] | null {\n if (!encoded) {\n return null;\n }\n if (encoded.length > MAX_ENCODED_BYTES) {\n return null;\n }\n try {\n const binary = atob(fromBase64Url(encoded));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const json = new TextDecoder().decode(bytes);\n const parsed = JSON.parse(json);\n if (!Array.isArray(parsed)) {\n return null;\n }\n return parsed as SupportedBlock[];\n } catch {\n return null;\n }\n}\n\n/**\n * Converts a standard base64 string to base64url (URL-safe alphabet, no padding).\n * @param b64 - standard base64-encoded string\n * @returns the base64url-encoded equivalent\n */\nfunction toBase64Url(b64: string): string {\n return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\n/**\n * Converts a base64url string back to standard base64 with padding restored.\n * @param b64url - base64url-encoded string\n * @returns the standard base64 equivalent\n */\nfunction fromBase64Url(b64url: string): string {\n const b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');\n const padLen = (4 - (b64.length % 4)) % 4;\n return b64 + '='.repeat(padLen);\n}\n"]}
@@ -0,0 +1,38 @@
1
+ import { t as SupportedBlock } from './palette-CTUROcUC.cjs';
2
+ export { u as SupportedBlockType } from './palette-CTUROcUC.cjs';
3
+ import 'react';
4
+ import 'slack-web-api-client';
5
+
6
+ /**
7
+ * Strip builder-only fields from blocks so they conform to Slack's
8
+ * Block Kit schema, and scrub dangerous URI schemes from any
9
+ * `url`/`image_url` fields. Currently removes the cosmetic `level`
10
+ * field from header blocks (a builder-only extension Slack would
11
+ * reject) and routes every URL/image-url through the allowlist in
12
+ * `lib/url-safety.ts` so a payload that round-trips through the
13
+ * builder cannot carry `javascript:`/`data:text/html` URIs to a
14
+ * downstream consumer or to the Slack API.
15
+ * @param blocks - the working draft blocks
16
+ * @returns blocks with builder-only fields removed and URLs scrubbed
17
+ */
18
+ declare function toSlackBlocks(blocks: SupportedBlock[]): SupportedBlock[];
19
+
20
+ /**
21
+ * Encodes a block list to a base64url string suitable for use as a URL
22
+ * search param. The consumer chooses where to put it (search param, hash,
23
+ * localStorage, etc).
24
+ * Returns an empty string for an empty list so the consumer can omit the
25
+ * param entirely.
26
+ * @param blocks - the blocks to encode
27
+ * @returns base64url-encoded JSON, or '' if blocks is empty
28
+ */
29
+ declare function encodeBlocksToString(blocks: SupportedBlock[]): string;
30
+ /**
31
+ * Decodes a string produced by {@link encodeBlocksToString} back to a
32
+ * block list. Returns null on any decode/parse error.
33
+ * @param encoded - the encoded string
34
+ * @returns decoded blocks, or null if input is empty/invalid
35
+ */
36
+ declare function decodeBlocksFromString(encoded: string | undefined | null): SupportedBlock[] | null;
37
+
38
+ export { SupportedBlock, decodeBlocksFromString, encodeBlocksToString, toSlackBlocks };
@@ -0,0 +1,38 @@
1
+ import { t as SupportedBlock } from './palette-CTUROcUC.js';
2
+ export { u as SupportedBlockType } from './palette-CTUROcUC.js';
3
+ import 'react';
4
+ import 'slack-web-api-client';
5
+
6
+ /**
7
+ * Strip builder-only fields from blocks so they conform to Slack's
8
+ * Block Kit schema, and scrub dangerous URI schemes from any
9
+ * `url`/`image_url` fields. Currently removes the cosmetic `level`
10
+ * field from header blocks (a builder-only extension Slack would
11
+ * reject) and routes every URL/image-url through the allowlist in
12
+ * `lib/url-safety.ts` so a payload that round-trips through the
13
+ * builder cannot carry `javascript:`/`data:text/html` URIs to a
14
+ * downstream consumer or to the Slack API.
15
+ * @param blocks - the working draft blocks
16
+ * @returns blocks with builder-only fields removed and URLs scrubbed
17
+ */
18
+ declare function toSlackBlocks(blocks: SupportedBlock[]): SupportedBlock[];
19
+
20
+ /**
21
+ * Encodes a block list to a base64url string suitable for use as a URL
22
+ * search param. The consumer chooses where to put it (search param, hash,
23
+ * localStorage, etc).
24
+ * Returns an empty string for an empty list so the consumer can omit the
25
+ * param entirely.
26
+ * @param blocks - the blocks to encode
27
+ * @returns base64url-encoded JSON, or '' if blocks is empty
28
+ */
29
+ declare function encodeBlocksToString(blocks: SupportedBlock[]): string;
30
+ /**
31
+ * Decodes a string produced by {@link encodeBlocksToString} back to a
32
+ * block list. Returns null on any decode/parse error.
33
+ * @param encoded - the encoded string
34
+ * @returns decoded blocks, or null if input is empty/invalid
35
+ */
36
+ declare function decodeBlocksFromString(encoded: string | undefined | null): SupportedBlock[] | null;
37
+
38
+ export { SupportedBlock, decodeBlocksFromString, encodeBlocksToString, toSlackBlocks };
@@ -0,0 +1,167 @@
1
+ // src/lib/url-safety.ts
2
+ var SAFE_LINK_PROTOCOLS = /^(https?|ircs?|mailto|tel|sms|xmpp)$/i;
3
+ var SAFE_IMAGE_PROTOCOLS = /^(https?)$/i;
4
+ var SAFE_IMAGE_DATA_PREFIX = /^data:image\/(?:png|jpeg|jpg|gif|webp|avif|x-icon|vnd\.microsoft\.icon)(?:;[a-z0-9-]+(?:=[^,;]*)?)*,/i;
5
+ function getScheme(value) {
6
+ const trimmed = value.replace(/^\s+/, "");
7
+ const colon = trimmed.indexOf(":");
8
+ if (colon === -1) {
9
+ return null;
10
+ }
11
+ const slash = trimmed.indexOf("/");
12
+ const question = trimmed.indexOf("?");
13
+ const hash = trimmed.indexOf("#");
14
+ if (slash !== -1 && slash < colon || question !== -1 && question < colon || hash !== -1 && hash < colon) {
15
+ return null;
16
+ }
17
+ return trimmed.slice(0, colon).toLowerCase();
18
+ }
19
+ function isSafeHref(value) {
20
+ if (value === null || value === void 0) {
21
+ return false;
22
+ }
23
+ if (typeof value !== "string") {
24
+ return false;
25
+ }
26
+ if (value.length === 0) {
27
+ return true;
28
+ }
29
+ const scheme = getScheme(value);
30
+ if (scheme === null) {
31
+ return true;
32
+ }
33
+ return SAFE_LINK_PROTOCOLS.test(scheme);
34
+ }
35
+ function isSafeImageSrc(value) {
36
+ if (value === null || value === void 0) {
37
+ return false;
38
+ }
39
+ if (typeof value !== "string") {
40
+ return false;
41
+ }
42
+ if (value.length === 0) {
43
+ return true;
44
+ }
45
+ const scheme = getScheme(value);
46
+ if (scheme === null) {
47
+ return true;
48
+ }
49
+ if (scheme === "data") {
50
+ return SAFE_IMAGE_DATA_PREFIX.test(value);
51
+ }
52
+ return SAFE_IMAGE_PROTOCOLS.test(scheme);
53
+ }
54
+ function sanitizeHref(value) {
55
+ return isSafeHref(value) ? value ?? "" : "";
56
+ }
57
+ function sanitizeImageSrc(value) {
58
+ return isSafeImageSrc(value) ? value ?? "" : "";
59
+ }
60
+
61
+ // src/lib/sanitize-blocks.ts
62
+ var HREF_KEYS = /* @__PURE__ */ new Set(["url"]);
63
+ var IMAGE_KEYS = /* @__PURE__ */ new Set(["image_url"]);
64
+ function sanitizeValue(value) {
65
+ if (value === null || typeof value !== "object") {
66
+ return value;
67
+ }
68
+ if (Array.isArray(value)) {
69
+ let changed = false;
70
+ const out = new Array(value.length);
71
+ for (let i = 0; i < value.length; i++) {
72
+ const next = sanitizeValue(value[i]);
73
+ if (next !== value[i]) {
74
+ changed = true;
75
+ }
76
+ out[i] = next;
77
+ }
78
+ return changed ? out : value;
79
+ }
80
+ const src = value;
81
+ let copy = null;
82
+ for (const key of Object.keys(src)) {
83
+ const original = src[key];
84
+ let next = original;
85
+ if (typeof original === "string") {
86
+ if (HREF_KEYS.has(key)) {
87
+ next = sanitizeHref(original);
88
+ } else if (IMAGE_KEYS.has(key)) {
89
+ next = sanitizeImageSrc(original);
90
+ }
91
+ } else if (typeof original === "object" && original !== null) {
92
+ next = sanitizeValue(original);
93
+ }
94
+ if (next !== original) {
95
+ if (!copy) {
96
+ copy = { ...src };
97
+ }
98
+ copy[key] = next;
99
+ }
100
+ }
101
+ return copy ?? src;
102
+ }
103
+ function sanitizeBlock(block) {
104
+ return sanitizeValue(block);
105
+ }
106
+
107
+ // src/lib/to-slack-blocks.ts
108
+ function toSlackBlocks(blocks) {
109
+ return blocks.map((block) => {
110
+ const safe = sanitizeBlock(block);
111
+ if (safe.type === "header" && "level" in safe) {
112
+ const { level: _omit, ...rest } = safe;
113
+ return rest;
114
+ }
115
+ return safe;
116
+ });
117
+ }
118
+
119
+ // src/lib/url-state.ts
120
+ var MAX_ENCODED_BYTES = 1024 * 1024;
121
+ function encodeBlocksToString(blocks) {
122
+ if (blocks.length === 0) {
123
+ return "";
124
+ }
125
+ const json = JSON.stringify(blocks);
126
+ const utf8Bytes = new TextEncoder().encode(json);
127
+ const chars = new Array(utf8Bytes.length);
128
+ for (let i = 0; i < utf8Bytes.length; i++) {
129
+ chars[i] = String.fromCharCode(utf8Bytes[i]);
130
+ }
131
+ return toBase64Url(btoa(chars.join("")));
132
+ }
133
+ function decodeBlocksFromString(encoded) {
134
+ if (!encoded) {
135
+ return null;
136
+ }
137
+ if (encoded.length > MAX_ENCODED_BYTES) {
138
+ return null;
139
+ }
140
+ try {
141
+ const binary = atob(fromBase64Url(encoded));
142
+ const bytes = new Uint8Array(binary.length);
143
+ for (let i = 0; i < binary.length; i++) {
144
+ bytes[i] = binary.charCodeAt(i);
145
+ }
146
+ const json = new TextDecoder().decode(bytes);
147
+ const parsed = JSON.parse(json);
148
+ if (!Array.isArray(parsed)) {
149
+ return null;
150
+ }
151
+ return parsed;
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+ function toBase64Url(b64) {
157
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
158
+ }
159
+ function fromBase64Url(b64url) {
160
+ const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
161
+ const padLen = (4 - b64.length % 4) % 4;
162
+ return b64 + "=".repeat(padLen);
163
+ }
164
+
165
+ export { decodeBlocksFromString, encodeBlocksToString, toSlackBlocks };
166
+ //# sourceMappingURL=helpers.js.map
167
+ //# sourceMappingURL=helpers.js.map