@tightknitai/block-kitchen 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/helpers.cjs CHANGED
@@ -65,7 +65,10 @@ var HREF_KEYS = /* @__PURE__ */ new Set(["url"]);
65
65
  var IMAGE_KEYS = /* @__PURE__ */ new Set(["image_url"]);
66
66
  var RETRIEVAL_ONLY_KEYS = /* @__PURE__ */ new Map([
67
67
  // Covers both the image block and the image element — both `type: 'image'`.
68
- ["image", /* @__PURE__ */ new Set(["image_width", "image_height", "image_bytes", "fallback", "is_animated"])]
68
+ ["image", /* @__PURE__ */ new Set(["image_width", "image_height", "image_bytes", "fallback", "is_animated"])],
69
+ // Slack renders the chart server-side and attaches the rendered previews on
70
+ // retrieval; sending them back is rejected as `unknown property 'preview_images'`.
71
+ ["data_visualization", /* @__PURE__ */ new Set(["preview_images"])]
69
72
  ]);
70
73
  function sanitizeValue(value) {
71
74
  if (value === null || typeof value !== "object") {
@@ -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.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 * Read-only metadata Slack attaches when a message is *retrieved* via the\n * API, but rejects on *send*, keyed by the `type` of the object it appears\n * on. Blocks loaded from an existing message carry these; drop them only\n * from the matching object type — some are common field names (`fallback`)\n * that are valid elsewhere — so a round-tripped payload stays send-valid\n * without over-scrubbing. Add an entry to extend this to other block types.\n */\nconst RETRIEVAL_ONLY_KEYS = new Map<string, Set<string>>([\n // Covers both the image block and the image element — both `type: 'image'`.\n ['image', new Set(['image_width', 'image_height', 'image_bytes', 'fallback', 'is_animated'])]\n]);\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 const dropKeys = typeof src.type === 'string' ? RETRIEVAL_ONLY_KEYS.get(src.type) : undefined;\n let copy: Record<string, unknown> | null = null;\n for (const key of Object.keys(src)) {\n if (dropKeys?.has(key)) {\n copy ??= { ...src };\n delete copy[key];\n continue;\n }\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 * Read-only metadata Slack attaches when a message is *retrieved* via the\n * API, but rejects on *send*, keyed by the `type` of the object it appears\n * on. Blocks loaded from an existing message carry these; drop them only\n * from the matching object type — some are common field names (`fallback`)\n * that are valid elsewhere — so a round-tripped payload stays send-valid\n * without over-scrubbing. Add an entry to extend this to other block types.\n */\nconst RETRIEVAL_ONLY_KEYS = new Map<string, Set<string>>([\n // Covers both the image block and the image element — both `type: 'image'`.\n ['image', new Set(['image_width', 'image_height', 'image_bytes', 'fallback', 'is_animated'])],\n // Slack renders the chart server-side and attaches the rendered previews on\n // retrieval; sending them back is rejected as `unknown property 'preview_images'`.\n ['data_visualization', new Set(['preview_images'])]\n]);\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 const dropKeys = typeof src.type === 'string' ? RETRIEVAL_ONLY_KEYS.get(src.type) : undefined;\n let copy: Record<string, unknown> | null = null;\n for (const key of Object.keys(src)) {\n if (dropKeys?.has(key)) {\n copy ??= { ...src };\n delete copy[key];\n continue;\n }\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/helpers.js CHANGED
@@ -63,7 +63,10 @@ var HREF_KEYS = /* @__PURE__ */ new Set(["url"]);
63
63
  var IMAGE_KEYS = /* @__PURE__ */ new Set(["image_url"]);
64
64
  var RETRIEVAL_ONLY_KEYS = /* @__PURE__ */ new Map([
65
65
  // Covers both the image block and the image element — both `type: 'image'`.
66
- ["image", /* @__PURE__ */ new Set(["image_width", "image_height", "image_bytes", "fallback", "is_animated"])]
66
+ ["image", /* @__PURE__ */ new Set(["image_width", "image_height", "image_bytes", "fallback", "is_animated"])],
67
+ // Slack renders the chart server-side and attaches the rendered previews on
68
+ // retrieval; sending them back is rejected as `unknown property 'preview_images'`.
69
+ ["data_visualization", /* @__PURE__ */ new Set(["preview_images"])]
67
70
  ]);
68
71
  function sanitizeValue(value) {
69
72
  if (value === null || typeof value !== "object") {
@@ -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` 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 * Read-only metadata Slack attaches when a message is *retrieved* via the\n * API, but rejects on *send*, keyed by the `type` of the object it appears\n * on. Blocks loaded from an existing message carry these; drop them only\n * from the matching object type — some are common field names (`fallback`)\n * that are valid elsewhere — so a round-tripped payload stays send-valid\n * without over-scrubbing. Add an entry to extend this to other block types.\n */\nconst RETRIEVAL_ONLY_KEYS = new Map<string, Set<string>>([\n // Covers both the image block and the image element — both `type: 'image'`.\n ['image', new Set(['image_width', 'image_height', 'image_bytes', 'fallback', 'is_animated'])]\n]);\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 const dropKeys = typeof src.type === 'string' ? RETRIEVAL_ONLY_KEYS.get(src.type) : undefined;\n let copy: Record<string, unknown> | null = null;\n for (const key of Object.keys(src)) {\n if (dropKeys?.has(key)) {\n copy ??= { ...src };\n delete copy[key];\n continue;\n }\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 * Read-only metadata Slack attaches when a message is *retrieved* via the\n * API, but rejects on *send*, keyed by the `type` of the object it appears\n * on. Blocks loaded from an existing message carry these; drop them only\n * from the matching object type — some are common field names (`fallback`)\n * that are valid elsewhere — so a round-tripped payload stays send-valid\n * without over-scrubbing. Add an entry to extend this to other block types.\n */\nconst RETRIEVAL_ONLY_KEYS = new Map<string, Set<string>>([\n // Covers both the image block and the image element — both `type: 'image'`.\n ['image', new Set(['image_width', 'image_height', 'image_bytes', 'fallback', 'is_animated'])],\n // Slack renders the chart server-side and attaches the rendered previews on\n // retrieval; sending them back is rejected as `unknown property 'preview_images'`.\n ['data_visualization', new Set(['preview_images'])]\n]);\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 const dropKeys = typeof src.type === 'string' ? RETRIEVAL_ONLY_KEYS.get(src.type) : undefined;\n let copy: Record<string, unknown> | null = null;\n for (const key of Object.keys(src)) {\n if (dropKeys?.has(key)) {\n copy ??= { ...src };\n delete copy[key];\n continue;\n }\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
@@ -1919,7 +1919,10 @@ var HREF_KEYS = /* @__PURE__ */ new Set(["url"]);
1919
1919
  var IMAGE_KEYS = /* @__PURE__ */ new Set(["image_url"]);
1920
1920
  var RETRIEVAL_ONLY_KEYS = /* @__PURE__ */ new Map([
1921
1921
  // Covers both the image block and the image element — both `type: 'image'`.
1922
- ["image", /* @__PURE__ */ new Set(["image_width", "image_height", "image_bytes", "fallback", "is_animated"])]
1922
+ ["image", /* @__PURE__ */ new Set(["image_width", "image_height", "image_bytes", "fallback", "is_animated"])],
1923
+ // Slack renders the chart server-side and attaches the rendered previews on
1924
+ // retrieval; sending them back is rejected as `unknown property 'preview_images'`.
1925
+ ["data_visualization", /* @__PURE__ */ new Set(["preview_images"])]
1923
1926
  ]);
1924
1927
  function sanitizeValue(value) {
1925
1928
  if (value === null || typeof value !== "object") {