@tightknitai/block-kitchen 0.8.4 → 0.8.6

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
@@ -116,8 +116,11 @@ export function MyBuilderPage() {
116
116
  | `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. |
117
117
  | `paletteSearchPlaceholder` | `string` | no | Placeholder text for the palette search input. Defaults to `'Search blocks…'`. Useful for localization. |
118
118
  | `allowedSurfaces` | `PreviewSurface[]` | no | Allowlist of preview surfaces (`'message'`, `'modal'`, `'app_home'`). Defaults to `['message']` — surface dropdown is hidden when only one surface is allowed. The first entry is the initial selection. |
119
- | `showThemeControl` | `boolean` | no | Defaults to `true`. When `false`, the theme is locked to `'light'`. |
120
- | `defaultPreviewTheme` | `'light' \| 'dark'` | no | Pass the host app's current theme so the preview opens matched to the consuming app's appearance. |
119
+ | `showThemeControl` | `boolean` | no | Defaults to `true`. When `false`, the toolbar's light/dark toggle is hidden and the theme stays at `defaultPreviewTheme`. Ignored when `previewTheme` is set (a controlled theme always hides the toggle). |
120
+ | `defaultPreviewTheme` | `'light' \| 'dark'` | no | Initial (uncontrolled) preview theme. Pass the host app's current theme so the preview opens matched to the consuming app's appearance. Ignored when `previewTheme` is provided. |
121
+ | `previewTheme` | `'light' \| 'dark'` | no | Controlled preview theme. When set, the preview renders in this theme, follows it reactively, and the toolbar's light/dark toggle is hidden so the host app fully owns the theme. Leave unset to keep the preview uncontrolled (seeded from `defaultPreviewTheme`, toggle shown). |
122
+ | `sendButtonLabel` | `string` | no | Label and accessible name for the toolbar's Send button (which opens the send dialog). Defaults to `'Send'`. Use it to signal that a configuration step follows, e.g. `'Send to channel…'`. |
123
+ | `confirmSendLabel` | `string` | no | Label for the send dialog's final confirm button. Defaults to `'Send'` (shows `'Sending…'` while in flight). |
121
124
  | `theme` | `BrandTheme \| BrandPreset` | no | Branding tokens applied to the builder chrome (toolbar, palette, popovers, dialogs). Accepts a `Partial<BrandTokens>` map and optional `light`/`dark` overrides. See [Styling](#styling) below. |
122
125
 
123
126
  ## Customizing the palette
@@ -222,7 +225,7 @@ const brand: BrandTheme = {
222
225
  - `tokens` applies in both light and dark contexts.
223
226
  - `light` and `dark` override per mode; the dark variant kicks in under a standard `.dark` ancestor class (next-themes default).
224
227
  - Color tokens take HSL component strings (`"262 83% 58%"`), matching the underlying CSS variable contract; `radius` takes a CSS length.
225
- - Scope is the builder chrome only. The embedded Slack preview keeps its native Slack styling regardless of `theme`; use `defaultPreviewTheme` for the preview's light/dark toggle.
228
+ - Scope is the builder chrome only. The embedded Slack preview keeps its native Slack styling regardless of `theme`; use `previewTheme` (controlled) or `defaultPreviewTheme` (uncontrolled) for the preview's light/dark appearance.
226
229
 
227
230
  The lower-level CSS-variable contract above keeps working; the `theme` prop simply layers on top of it.
228
231
 
@@ -1 +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":";;;AAqBA,IAAM,mBAAA,GAAsB,uCAAA;AAQ5B,IAAM,oBAAA,GAAuB,aAAA;AAK7B,IAAM,sBAAA,GACJ,uGAAA;AAWF,SAAS,UAAU,KAAA,EAA8B;AAI/C,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACxC,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACjC,EAAA,IAAI,UAAU,EAAA,EAAI;AAChB,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACjC,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AACpC,EAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AAIhC,EAAA,IAAK,KAAA,KAAU,EAAA,IAAM,KAAA,GAAQ,KAAA,IAAW,QAAA,KAAa,EAAA,IAAM,QAAA,GAAW,KAAA,IAAW,IAAA,KAAS,EAAA,IAAM,IAAA,GAAO,KAAA,EAAQ;AAC7G,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,KAAK,EAAE,WAAA,EAAY;AAC7C;AAeO,SAAS,WAAW,KAAA,EAA2C;AACpE,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AACzC,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAA,GAAS,UAAU,KAAK,CAAA;AAC9B,EAAA,IAAI,WAAW,IAAA,EAAM;AACnB,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO,mBAAA,CAAoB,KAAK,MAAM,CAAA;AACxC;AAUO,SAAS,eAAe,KAAA,EAA2C;AACxE,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AACzC,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAA,GAAS,UAAU,KAAK,CAAA;AAC9B,EAAA,IAAI,WAAW,IAAA,EAAM;AACnB,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,WAAW,MAAA,EAAQ;AACrB,IAAA,OAAO,sBAAA,CAAuB,KAAK,KAAK,CAAA;AAAA,EAC1C;AACA,EAAA,OAAO,oBAAA,CAAqB,KAAK,MAAM,CAAA;AACzC;AASO,SAAS,aAAa,KAAA,EAA0C;AACrE,EAAA,OAAO,UAAA,CAAW,KAAK,CAAA,GAAK,KAAA,IAAS,EAAA,GAAM,EAAA;AAC7C;AAQO,SAAS,iBAAiB,KAAA,EAA0C;AACzE,EAAA,OAAO,cAAA,CAAe,KAAK,CAAA,GAAK,KAAA,IAAS,EAAA,GAAM,EAAA;AACjD;;;AC/HA,IAAM,SAAA,mBAAY,IAAI,GAAA,CAAI,CAAC,KAAK,CAAC,CAAA;AACjC,IAAM,UAAA,mBAAa,IAAI,GAAA,CAAI,CAAC,WAAW,CAAC,CAAA;AAWxC,SAAS,cAAc,KAAA,EAAyB;AAC9C,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,OAAO,KAAA,KAAU,QAAA,EAAU;AAC/C,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,MAAM,GAAA,GAAM,IAAI,KAAA,CAAM,KAAA,CAAM,MAAM,CAAA;AAClC,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,MAAA,MAAM,IAAA,GAAO,aAAA,CAAc,KAAA,CAAM,CAAC,CAAC,CAAA;AACnC,MAAA,IAAI,IAAA,KAAS,KAAA,CAAM,CAAC,CAAA,EAAG;AACrB,QAAA,OAAA,GAAU,IAAA;AAAA,MACZ;AACA,MAAA,GAAA,CAAI,CAAC,CAAA,GAAI,IAAA;AAAA,IACX;AACA,IAAA,OAAO,UAAU,GAAA,GAAM,KAAA;AAAA,EACzB;AACA,EAAA,MAAM,GAAA,GAAM,KAAA;AACZ,EAAA,IAAI,IAAA,GAAuC,IAAA;AAC3C,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAAG;AAClC,IAAA,MAAM,QAAA,GAAW,IAAI,GAAG,CAAA;AACxB,IAAA,IAAI,IAAA,GAAgB,QAAA;AACpB,IAAA,IAAI,OAAO,aAAa,QAAA,EAAU;AAChC,MAAA,IAAI,SAAA,CAAU,GAAA,CAAI,GAAG,CAAA,EAAG;AACtB,QAAA,IAAA,GAAO,aAAa,QAAQ,CAAA;AAAA,MAC9B,CAAA,MAAA,IAAW,UAAA,CAAW,GAAA,CAAI,GAAG,CAAA,EAAG;AAC9B,QAAA,IAAA,GAAO,iBAAiB,QAAQ,CAAA;AAAA,MAClC;AAAA,IACF,CAAA,MAAA,IAAW,OAAO,QAAA,KAAa,QAAA,IAAY,aAAa,IAAA,EAAM;AAC5D,MAAA,IAAA,GAAO,cAAc,QAAQ,CAAA;AAAA,IAC/B;AACA,IAAA,IAAI,SAAS,QAAA,EAAU;AACrB,MAAA,IAAI,CAAC,IAAA,EAAM;AACT,QAAA,IAAA,GAAO,EAAE,GAAG,GAAA,EAAI;AAAA,MAClB;AACA,MAAA,IAAA,CAAK,GAAG,CAAA,GAAI,IAAA;AAAA,IACd;AAAA,EACF;AACA,EAAA,OAAO,IAAA,IAAQ,GAAA;AACjB;AAQO,SAAS,cAAwC,KAAA,EAAa;AACnE,EAAA,OAAO,cAAc,KAAK,CAAA;AAC5B;;;AC9DO,SAAS,cAAc,MAAA,EAA4C;AACxE,EAAA,OAAO,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,KAAU;AAC3B,IAAA,MAAM,IAAA,GAAO,cAAc,KAAK,CAAA;AAChC,IAAA,IAAI,IAAA,CAAK,IAAA,KAAS,QAAA,IAAY,OAAA,IAAW,IAAA,EAAM;AAC7C,MAAA,MAAM,EAAE,KAAA,EAAO,KAAA,EAAO,GAAG,MAAK,GAAI,IAAA;AAClC,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA;AAAA,EACT,CAAC,CAAA;AACH;;;ACdA,IAAM,oBAAoB,IAAA,GAAO,IAAA;AAW1B,SAAS,qBAAqB,MAAA,EAAkC;AACrE,EAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACvB,IAAA,OAAO,EAAA;AAAA,EACT;AACA,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,MAAM,CAAA;AAClC,EAAA,MAAM,SAAA,GAAY,IAAI,WAAA,EAAY,CAAE,OAAO,IAAI,CAAA;AAC/C,EAAA,MAAM,KAAA,GAAQ,IAAI,KAAA,CAAc,SAAA,CAAU,MAAM,CAAA;AAChD,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,SAAA,CAAU,QAAQ,CAAA,EAAA,EAAK;AACzC,IAAA,KAAA,CAAM,CAAC,CAAA,GAAI,MAAA,CAAO,YAAA,CAAa,SAAA,CAAU,CAAC,CAAC,CAAA;AAAA,EAC7C;AACA,EAAA,OAAO,YAAY,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,EAAE,CAAC,CAAC,CAAA;AACzC;AAQO,SAAS,uBAAuB,OAAA,EAA6D;AAClG,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,OAAA,CAAQ,SAAS,iBAAA,EAAmB;AACtC,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,aAAA,CAAc,OAAO,CAAC,CAAA;AAC1C,IAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAA,CAAO,MAAM,CAAA;AAC1C,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,CAAO,QAAQ,CAAA,EAAA,EAAK;AACtC,MAAA,KAAA,CAAM,CAAC,CAAA,GAAI,MAAA,CAAO,UAAA,CAAW,CAAC,CAAA;AAAA,IAChC;AACA,IAAA,MAAM,IAAA,GAAO,IAAI,WAAA,EAAY,CAAE,OAAO,KAAK,CAAA;AAC3C,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC9B,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAG;AAC1B,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAOA,SAAS,YAAY,GAAA,EAAqB;AACxC,EAAA,OAAO,GAAA,CAAI,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AACtE;AAOA,SAAS,cAAc,MAAA,EAAwB;AAC7C,EAAA,MAAM,GAAA,GAAM,OAAO,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA,CAAE,OAAA,CAAQ,MAAM,GAAG,CAAA;AACvD,EAAA,MAAM,MAAA,GAAA,CAAU,CAAA,GAAK,GAAA,CAAI,MAAA,GAAS,CAAA,IAAM,CAAA;AACxC,EAAA,OAAO,GAAA,GAAM,GAAA,CAAI,MAAA,CAAO,MAAM,CAAA;AAChC","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"]}
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` carries an explicit, safe URL scheme\n * (e.g. `https://example.com`, `mailto:a@b.com`).\n *\n * Used to gate the TipTap link extension's autolinker. TipTap's default\n * `shouldAutoLink` links any token containing a dot whose host isn't a\n * bare IPv4 — so `2.xyz`, `report.zip`, or `logo.png` all become links\n * just because the suffix happens to be a real gTLD. Requiring an\n * explicit scheme means only URLs the user actually typed as links\n * (`https://…`) auto-link; bare host-like text stays plain text, and\n * deliberate links can still be added via the link button or by typing\n * the scheme. Schemes outside the safe-link allowlist are rejected here\n * too, so an unsafe scheme can never be auto-linked.\n * @param value - the candidate URL (the linkifier's matched token text)\n * @returns true when the value has an explicit, allowlisted scheme\n */\nexport function hasExplicitSafeScheme(value: string | null | undefined): boolean {\n if (typeof value !== 'string' || value.length === 0) {\n return false;\n }\n return getScheme(value) !== null && isSafeHref(value);\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"]}
@@ -1,5 +1,5 @@
1
- import { u as SupportedBlock } from './palette-Dco-CVMi.cjs';
2
- export { v as SupportedBlockType } from './palette-Dco-CVMi.cjs';
1
+ import { u as SupportedBlock } from './palette-DToUE_3J.cjs';
2
+ export { v as SupportedBlockType } from './palette-DToUE_3J.cjs';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5
 
package/dist/helpers.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { u as SupportedBlock } from './palette-Dco-CVMi.js';
2
- export { v as SupportedBlockType } from './palette-Dco-CVMi.js';
1
+ import { u as SupportedBlock } from './palette-DToUE_3J.js';
2
+ export { v as SupportedBlockType } from './palette-DToUE_3J.js';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5
 
@@ -1 +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.js","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"]}
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.js","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` carries an explicit, safe URL scheme\n * (e.g. `https://example.com`, `mailto:a@b.com`).\n *\n * Used to gate the TipTap link extension's autolinker. TipTap's default\n * `shouldAutoLink` links any token containing a dot whose host isn't a\n * bare IPv4 — so `2.xyz`, `report.zip`, or `logo.png` all become links\n * just because the suffix happens to be a real gTLD. Requiring an\n * explicit scheme means only URLs the user actually typed as links\n * (`https://…`) auto-link; bare host-like text stays plain text, and\n * deliberate links can still be added via the link button or by typing\n * the scheme. Schemes outside the safe-link allowlist are rejected here\n * too, so an unsafe scheme can never be auto-linked.\n * @param value - the candidate URL (the linkifier's matched token text)\n * @returns true when the value has an explicit, allowlisted scheme\n */\nexport function hasExplicitSafeScheme(value: string | null | undefined): boolean {\n if (typeof value !== 'string' || value.length === 0) {\n return false;\n }\n return getScheme(value) !== null && isSafeHref(value);\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"]}
package/dist/index.cjs CHANGED
@@ -1670,6 +1670,12 @@ function isSafeHref(value) {
1670
1670
  }
1671
1671
  return SAFE_LINK_PROTOCOLS.test(scheme);
1672
1672
  }
1673
+ function hasExplicitSafeScheme(value) {
1674
+ if (typeof value !== "string" || value.length === 0) {
1675
+ return false;
1676
+ }
1677
+ return getScheme(value) !== null && isSafeHref(value);
1678
+ }
1673
1679
  function isSafeImageSrc(value) {
1674
1680
  if (value === null || value === void 0) {
1675
1681
  return false;
@@ -2268,6 +2274,18 @@ function IssuesSheet({
2268
2274
  ] })
2269
2275
  ] }) });
2270
2276
  }
2277
+
2278
+ // src/lib/parse-blocks-input.ts
2279
+ var BLOCKS_INPUT_SHAPE_ERROR = "Expected an array of blocks, or an object with a `blocks` array.";
2280
+ function unwrapBlocksInput(parsed) {
2281
+ if (Array.isArray(parsed)) {
2282
+ return parsed;
2283
+ }
2284
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.blocks)) {
2285
+ return parsed.blocks;
2286
+ }
2287
+ return null;
2288
+ }
2271
2289
  var MAX_JSON_BYTES = 1024 * 1024;
2272
2290
  function JsonDrawer({
2273
2291
  open,
@@ -2306,14 +2324,15 @@ function JsonDrawer({
2306
2324
  setValidationErrors([]);
2307
2325
  return;
2308
2326
  }
2309
- if (!Array.isArray(parsed)) {
2310
- setParseError("Top-level value must be an array of blocks.");
2327
+ const blocks2 = unwrapBlocksInput(parsed);
2328
+ if (!blocks2) {
2329
+ setParseError(BLOCKS_INPUT_SHAPE_ERROR);
2311
2330
  setValidationErrors([]);
2312
2331
  return;
2313
2332
  }
2314
2333
  setParseError(null);
2315
- onApply(parsed);
2316
- const result = slackBlockKitValidator.validateBlockKit(parsed, {
2334
+ onApply(blocks2);
2335
+ const result = slackBlockKitValidator.validateBlockKit(blocks2, {
2317
2336
  target: "blocks",
2318
2337
  surface: "message"
2319
2338
  });
@@ -2691,6 +2710,7 @@ function SendDialog({
2691
2710
  loadChannels,
2692
2711
  loadSendAsUserStatus,
2693
2712
  onSend,
2713
+ confirmSendLabel = "Send",
2694
2714
  errorCount,
2695
2715
  onShowIssues
2696
2716
  }) {
@@ -2864,7 +2884,7 @@ function SendDialog({
2864
2884
  type: "button",
2865
2885
  onClick: handleSubmit,
2866
2886
  disabled: status.kind === "sending" || !channelId || blocks.length === 0 || errorCount > 0,
2867
- children: status.kind === "sending" ? "Sending\u2026" : "Send"
2887
+ children: status.kind === "sending" ? "Sending\u2026" : confirmSendLabel
2868
2888
  }
2869
2889
  )
2870
2890
  ] })
@@ -5402,6 +5422,17 @@ function blockElementToPM(el, resolveEmoji) {
5402
5422
  }
5403
5423
  return null;
5404
5424
  }
5425
+ function pushTextWithHardBreaks(out, text, marks) {
5426
+ const segments = text.split("\n");
5427
+ segments.forEach((segment, i) => {
5428
+ if (i > 0) {
5429
+ out.push({ type: "hardBreak" });
5430
+ }
5431
+ if (segment) {
5432
+ out.push({ type: "text", text: segment, marks });
5433
+ }
5434
+ });
5435
+ }
5405
5436
  function inlinesToPM(inlines, resolveEmoji) {
5406
5437
  const out = [];
5407
5438
  for (const inline of inlines) {
@@ -5409,8 +5440,7 @@ function inlinesToPM(inlines, resolveEmoji) {
5409
5440
  if (!inline.text) {
5410
5441
  continue;
5411
5442
  }
5412
- const marks = styleToMarks(inline.style);
5413
- out.push({ type: "text", text: inline.text, marks });
5443
+ pushTextWithHardBreaks(out, inline.text, styleToMarks(inline.style));
5414
5444
  } else if (inline.type === "emoji") {
5415
5445
  const attrs = resolveEmoji(inline);
5416
5446
  if (!attrs.name) {
@@ -5547,6 +5577,10 @@ function proseMirrorInlinesToRichTextElements(nodes) {
5547
5577
  out.push(emoji);
5548
5578
  continue;
5549
5579
  }
5580
+ if (node.type === "hardBreak") {
5581
+ out.push({ type: "text", text: "\n" });
5582
+ continue;
5583
+ }
5550
5584
  if (node.type !== "text" || !node.text) {
5551
5585
  continue;
5552
5586
  }
@@ -6234,6 +6268,43 @@ function LossyBanner({ reasons }) {
6234
6268
  ] })
6235
6269
  ] });
6236
6270
  }
6271
+ var RICH_TEXT_INPUT_RULE_ALLOWLIST = ["bold", "italic", "strike", "code"];
6272
+ function buildRichTextExtensions() {
6273
+ return [
6274
+ StarterKit__default.default.configure({
6275
+ heading: false,
6276
+ horizontalRule: false,
6277
+ // StarterKit bundles its own Link extension (enabled by default),
6278
+ // which would register a second, unconfigured `link` mark and a
6279
+ // duplicate autolinker alongside the hardened one we add below.
6280
+ // Disable it so our single, security-configured Link is the only
6281
+ // link extension in the schema.
6282
+ link: false
6283
+ }),
6284
+ EmojiNode,
6285
+ Link__default.default.configure({
6286
+ openOnClick: false,
6287
+ autolink: true,
6288
+ defaultProtocol: "https",
6289
+ // Pin TipTap's link allowlist to the same set as our shared
6290
+ // `isSafeHref` helper. The upstream default also includes
6291
+ // ftp/cid/callto which we don't expect inside Slack content.
6292
+ protocols: ["http", "https", "mailto", "tel", "sms", "xmpp"],
6293
+ // Belt-and-suspenders: even if a downstream contributor widens
6294
+ // `protocols` later, our isSafeHref guard rejects everything
6295
+ // outside the safe-link set. setLink and toggleLink both gate
6296
+ // on this hook before applying the mark.
6297
+ isAllowedUri: (url) => isSafeHref(url),
6298
+ // Only auto-link tokens that carry an explicit, safe scheme. TipTap's
6299
+ // default would link any `host.tld` (e.g. `2.xyz`, `report.zip`,
6300
+ // `logo.png`) because the suffix is a real gTLD, which is wrong for
6301
+ // free-form Slack message text. Deliberate links can still be added
6302
+ // via the link button.
6303
+ shouldAutoLink: hasExplicitSafeScheme,
6304
+ HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
6305
+ })
6306
+ ];
6307
+ }
6237
6308
  function RichTextWysiwygEditor({ block, onChange }) {
6238
6309
  const lastEmittedRef = React.useRef(null);
6239
6310
  const { byName: customByName } = useCustomEmojis();
@@ -6259,28 +6330,8 @@ function RichTextWysiwygEditor({ block, onChange }) {
6259
6330
  [onChange]
6260
6331
  );
6261
6332
  const editor = react.useEditor({
6262
- extensions: [
6263
- StarterKit__default.default.configure({
6264
- heading: false,
6265
- horizontalRule: false
6266
- }),
6267
- EmojiNode,
6268
- Link__default.default.configure({
6269
- openOnClick: false,
6270
- autolink: true,
6271
- defaultProtocol: "https",
6272
- // Pin TipTap's link allowlist to the same set as our shared
6273
- // `isSafeHref` helper. The upstream default also includes
6274
- // ftp/cid/callto which we don't expect inside Slack content.
6275
- protocols: ["http", "https", "mailto", "tel", "sms", "xmpp"],
6276
- // Belt-and-suspenders: even if a downstream contributor widens
6277
- // `protocols` later, our isSafeHref guard rejects everything
6278
- // outside the safe-link set. setLink and toggleLink both gate
6279
- // on this hook before applying the mark.
6280
- isAllowedUri: (url) => isSafeHref(url),
6281
- HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
6282
- })
6283
- ],
6333
+ extensions: buildRichTextExtensions(),
6334
+ enableInputRules: RICH_TEXT_INPUT_RULE_ALLOWLIST,
6284
6335
  content: richTextToProseMirror(block, resolveEmoji),
6285
6336
  editorProps: {
6286
6337
  attributes: {
@@ -7907,7 +7958,8 @@ function Toolbar2({
7907
7958
  allowedSurfaces,
7908
7959
  showThemeControl = true,
7909
7960
  docsLink,
7910
- errorCount
7961
+ errorCount,
7962
+ sendButtonLabel = "Send"
7911
7963
  }) {
7912
7964
  const activeTheme = THEME_OPTIONS.find((t) => t.value === previewTheme) ?? THEME_OPTIONS[0];
7913
7965
  const activeSurface = SURFACE_OPTIONS.find((s) => s.value === previewSurface) ?? SURFACE_OPTIONS[0];
@@ -7997,7 +8049,7 @@ function Toolbar2({
7997
8049
  variant: "ghost",
7998
8050
  size: "sm",
7999
8051
  onClick: onOpenIssues,
8000
- className: "text-destructive hover:text-destructive",
8052
+ className: "border border-red-200! bg-red-50 text-red-700 hover:bg-red-100 hover:text-red-700",
8001
8053
  "aria-label": `${errorCount} ${errorCount === 1 ? "issue" : "issues"}`,
8002
8054
  children: [
8003
8055
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "h-3.5 w-3.5" }),
@@ -8090,9 +8142,9 @@ function Toolbar2({
8090
8142
  ) : null
8091
8143
  ] }) })
8092
8144
  ] }),
8093
- /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: onOpenSend, disabled: !canSend, "aria-label": "Send", children: [
8145
+ /* @__PURE__ */ jsxRuntime.jsxs(Button, { type: "button", size: "sm", onClick: onOpenSend, disabled: !canSend, "aria-label": sendButtonLabel, children: [
8094
8146
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Send, { className: "h-3.5 w-3.5" }),
8095
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Send" })
8147
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: sendButtonLabel })
8096
8148
  ] })
8097
8149
  ] })
8098
8150
  ] });
@@ -8218,6 +8270,9 @@ function BlockKitchen(props) {
8218
8270
  showThemeControl = true,
8219
8271
  docsLink,
8220
8272
  defaultPreviewTheme = "light",
8273
+ previewTheme: controlledPreviewTheme,
8274
+ sendButtonLabel,
8275
+ confirmSendLabel,
8221
8276
  theme
8222
8277
  } = props;
8223
8278
  const paletteSections = React.useMemo(() => {
@@ -8245,7 +8300,9 @@ function BlockKitchen(props) {
8245
8300
  const [issuesOpen, setIssuesOpen] = React.useState(false);
8246
8301
  const [paletteOpen, setPaletteOpen] = React.useState(false);
8247
8302
  const [openBlockId, setOpenBlockId] = React.useState(null);
8248
- const [previewTheme, setPreviewTheme] = React.useState(defaultPreviewTheme);
8303
+ const [uncontrolledPreviewTheme, setUncontrolledPreviewTheme] = React.useState(defaultPreviewTheme);
8304
+ const isPreviewThemeControlled = controlledPreviewTheme !== void 0;
8305
+ const previewTheme = controlledPreviewTheme ?? uncontrolledPreviewTheme;
8249
8306
  const [previewSurface, setPreviewSurface] = React.useState(allowedSurfaces[0]);
8250
8307
  const [activePaletteVariantId, setActivePaletteVariantId] = React.useState(null);
8251
8308
  const isMobile = useIsMobile();
@@ -8327,12 +8384,13 @@ function BlockKitchen(props) {
8327
8384
  canClear: blocks.length > 0,
8328
8385
  errorCount: validation.total,
8329
8386
  previewTheme,
8330
- onPreviewThemeChange: setPreviewTheme,
8387
+ onPreviewThemeChange: setUncontrolledPreviewTheme,
8331
8388
  previewSurface,
8332
8389
  onPreviewSurfaceChange: setPreviewSurface,
8333
8390
  allowedSurfaces,
8334
- showThemeControl,
8335
- docsLink
8391
+ showThemeControl: isPreviewThemeControlled ? false : showThemeControl,
8392
+ docsLink,
8393
+ sendButtonLabel
8336
8394
  }
8337
8395
  ),
8338
8396
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-0 flex-1 items-stretch", children: [
@@ -8408,6 +8466,7 @@ function BlockKitchen(props) {
8408
8466
  loadChannels,
8409
8467
  loadSendAsUserStatus,
8410
8468
  onSend,
8469
+ confirmSendLabel,
8411
8470
  errorCount: validation.total,
8412
8471
  onShowIssues: () => {
8413
8472
  setSendOpen(false);