@tightknitai/block-kitchen 0.10.12 → 0.10.14

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/index.js CHANGED
@@ -1838,6 +1838,7 @@ function groupValidatorErrors(errors, blocks) {
1838
1838
  var SAFE_LINK_PROTOCOLS = /^(https?|ircs?|mailto|tel|sms|xmpp)$/i;
1839
1839
  var SAFE_IMAGE_PROTOCOLS = /^(https?)$/i;
1840
1840
  var SAFE_IMAGE_DATA_PREFIX = /^data:image\/(?:png|jpeg|jpg|gif|webp|avif|x-icon|vnd\.microsoft\.icon)(?:;[a-z0-9-]+(?:=[^,;]*)?)*,/i;
1841
+ var SAFE_EMBED_PROTOCOLS = /^(https?)$/i;
1841
1842
  function getScheme(value) {
1842
1843
  const trimmed = value.replace(/^\s+/, "");
1843
1844
  const colon = trimmed.indexOf(":");
@@ -1893,16 +1894,54 @@ function isSafeImageSrc(value) {
1893
1894
  }
1894
1895
  return SAFE_IMAGE_PROTOCOLS.test(scheme);
1895
1896
  }
1897
+ function isSafeEmbedSrc(value) {
1898
+ if (typeof value !== "string") {
1899
+ return false;
1900
+ }
1901
+ if (value.length === 0) {
1902
+ return true;
1903
+ }
1904
+ const scheme = getScheme(value);
1905
+ if (scheme === null) {
1906
+ return false;
1907
+ }
1908
+ return SAFE_EMBED_PROTOCOLS.test(scheme);
1909
+ }
1896
1910
  function sanitizeHref(value) {
1897
1911
  return isSafeHref(value) ? value ?? "" : "";
1898
1912
  }
1899
1913
  function sanitizeImageSrc(value) {
1900
1914
  return isSafeImageSrc(value) ? value ?? "" : "";
1901
1915
  }
1916
+ function sanitizeEmbedSrc(value) {
1917
+ return isSafeEmbedSrc(value) ? value ?? "" : "";
1918
+ }
1902
1919
 
1903
1920
  // src/lib/sanitize-blocks.ts
1904
- var HREF_KEYS = /* @__PURE__ */ new Set(["url"]);
1905
- var IMAGE_KEYS = /* @__PURE__ */ new Set(["image_url"]);
1921
+ var EMBED_KEYS = /* @__PURE__ */ new Set(["video_url"]);
1922
+ var URL_KEY = /(^|_)(url|uri|link|href|src)$/;
1923
+ var MAYBE_URL_KEY = /url|uri|link|href|src/i;
1924
+ var IMAGE_KEY_HINT = /image|thumb|icon|avatar|logo|photo|picture/;
1925
+ var PROP_BAG_KEY = /(^|_)props$/;
1926
+ function normalizeKey(key) {
1927
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
1928
+ }
1929
+ function isRendererPropBag(key) {
1930
+ return PROP_BAG_KEY.test(normalizeKey(key));
1931
+ }
1932
+ function classifyUrlKey(key) {
1933
+ if (!MAYBE_URL_KEY.test(key)) {
1934
+ return null;
1935
+ }
1936
+ const normalized = normalizeKey(key);
1937
+ if (EMBED_KEYS.has(normalized)) {
1938
+ return "embed";
1939
+ }
1940
+ if (!URL_KEY.test(normalized)) {
1941
+ return null;
1942
+ }
1943
+ return IMAGE_KEY_HINT.test(normalized) ? "image" : "href";
1944
+ }
1906
1945
  var RETRIEVAL_ONLY_KEYS = /* @__PURE__ */ new Map([
1907
1946
  // Covers both the image block and the image element — both `type: 'image'`.
1908
1947
  ["image", /* @__PURE__ */ new Set(["image_width", "image_height", "image_bytes", "fallback", "is_animated"])],
@@ -1930,7 +1969,7 @@ function sanitizeValue(value) {
1930
1969
  const dropKeys = typeof src.type === "string" ? RETRIEVAL_ONLY_KEYS.get(src.type) : void 0;
1931
1970
  let copy = null;
1932
1971
  for (const key of Object.keys(src)) {
1933
- if (dropKeys?.has(key)) {
1972
+ if (dropKeys?.has(key) || isRendererPropBag(key)) {
1934
1973
  copy ??= { ...src };
1935
1974
  delete copy[key];
1936
1975
  continue;
@@ -1938,10 +1977,13 @@ function sanitizeValue(value) {
1938
1977
  const original = src[key];
1939
1978
  let next = original;
1940
1979
  if (typeof original === "string") {
1941
- if (HREF_KEYS.has(key)) {
1980
+ const kind = classifyUrlKey(key);
1981
+ if (kind === "href") {
1942
1982
  next = sanitizeHref(original);
1943
- } else if (IMAGE_KEYS.has(key)) {
1983
+ } else if (kind === "image") {
1944
1984
  next = sanitizeImageSrc(original);
1985
+ } else if (kind === "embed") {
1986
+ next = sanitizeEmbedSrc(original);
1945
1987
  }
1946
1988
  } else if (typeof original === "object" && original !== null) {
1947
1989
  next = sanitizeValue(original);
@@ -3254,6 +3296,7 @@ function ChannelCombobox({
3254
3296
  )
3255
3297
  ] });
3256
3298
  }
3299
+ var EMBED_SELECTOR = "iframe[src], embed[src], object[data], source[src], video[src], audio[src], track[src]";
3257
3300
  function SlackBlockPreview({
3258
3301
  block,
3259
3302
  hooks,
@@ -3286,6 +3329,17 @@ function SlackBlockPreview({
3286
3329
  img.setAttribute("data-bk-blocked-src", "1");
3287
3330
  }
3288
3331
  }
3332
+ for (const el of root.querySelectorAll(EMBED_SELECTOR)) {
3333
+ const attr = el.tagName === "OBJECT" ? "data" : "src";
3334
+ if (!isSafeEmbedSrc(el.getAttribute(attr))) {
3335
+ el.removeAttribute(attr);
3336
+ el.setAttribute("data-bk-blocked-src", "1");
3337
+ }
3338
+ }
3339
+ for (const frame of root.querySelectorAll("iframe[srcdoc]")) {
3340
+ frame.removeAttribute("srcdoc");
3341
+ frame.setAttribute("data-bk-blocked-srcdoc", "1");
3342
+ }
3289
3343
  });
3290
3344
  return (
3291
3345
  // The library scopes all of its CSS under `#slack_blocks_to_jsx` and
@@ -9138,7 +9192,14 @@ function SourcesField({
9138
9192
  ] })
9139
9193
  ] });
9140
9194
  }
9195
+ function UnsafeUrlNote({ children }) {
9196
+ return /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-destructive", children });
9197
+ }
9141
9198
  function VideoEditor({ block, onChange }) {
9199
+ const videoUrlIsUnsafe = (block.video_url ?? "").length > 0 && !isSafeEmbedSrc(block.video_url);
9200
+ const thumbnailIsUnsafe = (block.thumbnail_url ?? "").length > 0 && !isSafeImageSrc(block.thumbnail_url);
9201
+ const titleUrlIsUnsafe = (block.title_url ?? "").length > 0 && !isSafeHref(block.title_url);
9202
+ const providerIconIsUnsafe = (block.provider_icon_url ?? "").length > 0 && !isSafeImageSrc(block.provider_icon_url);
9142
9203
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
9143
9204
  /* @__PURE__ */ jsx(EditorField, { label: "Title", help: "Up to 199 characters. Shown above the embedded player.", htmlFor: "video-title", children: /* @__PURE__ */ jsx(
9144
9205
  Input,
@@ -9162,50 +9223,62 @@ function VideoEditor({ block, onChange }) {
9162
9223
  onChange: (e) => onChange({ ...block, alt_text: e.target.value })
9163
9224
  }
9164
9225
  ) }),
9165
- /* @__PURE__ */ jsx(
9226
+ /* @__PURE__ */ jsxs(
9166
9227
  EditorField,
9167
9228
  {
9168
9229
  label: "Video URL",
9169
9230
  help: "HTTPS embed URL. Must match an unfurl domain on your Slack app.",
9170
9231
  htmlFor: "video-url",
9171
- children: /* @__PURE__ */ jsx(
9172
- Input,
9173
- {
9174
- id: "video-url",
9175
- type: "url",
9176
- value: block.video_url ?? "",
9177
- placeholder: "e.g. https://www.youtube.com/embed/dQw4w9WgXcQ",
9178
- onChange: (e) => onChange({ ...block, video_url: e.target.value })
9179
- }
9180
- )
9232
+ children: [
9233
+ /* @__PURE__ */ jsx(
9234
+ Input,
9235
+ {
9236
+ id: "video-url",
9237
+ type: "url",
9238
+ value: block.video_url ?? "",
9239
+ placeholder: "e.g. https://www.youtube.com/embed/dQw4w9WgXcQ",
9240
+ onChange: (e) => onChange({ ...block, video_url: e.target.value }),
9241
+ "aria-invalid": videoUrlIsUnsafe || void 0
9242
+ }
9243
+ ),
9244
+ videoUrlIsUnsafe && /* @__PURE__ */ jsx(UnsafeUrlNote, { children: "The embedded player only accepts a full http(s) URL. This URL will be stripped before send and preview." })
9245
+ ]
9181
9246
  }
9182
9247
  ),
9183
- /* @__PURE__ */ jsx(EditorField, { label: "Thumbnail URL", help: "Preview image shown before the video loads.", htmlFor: "video-thumbnail", children: /* @__PURE__ */ jsx(
9184
- Input,
9185
- {
9186
- id: "video-thumbnail",
9187
- type: "url",
9188
- value: block.thumbnail_url ?? "",
9189
- placeholder: "e.g. https://example.com/thumb.png",
9190
- onChange: (e) => onChange({ ...block, thumbnail_url: e.target.value })
9191
- }
9192
- ) }),
9193
- /* @__PURE__ */ jsx(
9248
+ /* @__PURE__ */ jsxs(EditorField, { label: "Thumbnail URL", help: "Preview image shown before the video loads.", htmlFor: "video-thumbnail", children: [
9249
+ /* @__PURE__ */ jsx(
9250
+ Input,
9251
+ {
9252
+ id: "video-thumbnail",
9253
+ type: "url",
9254
+ value: block.thumbnail_url ?? "",
9255
+ placeholder: "e.g. https://example.com/thumb.png",
9256
+ onChange: (e) => onChange({ ...block, thumbnail_url: e.target.value }),
9257
+ "aria-invalid": thumbnailIsUnsafe || void 0
9258
+ }
9259
+ ),
9260
+ thumbnailIsUnsafe && /* @__PURE__ */ jsx(UnsafeUrlNote, { children: "Only http(s) image URLs are allowed. This URL will be stripped before send and preview." })
9261
+ ] }),
9262
+ /* @__PURE__ */ jsxs(
9194
9263
  EditorField,
9195
9264
  {
9196
9265
  label: "Title link URL",
9197
9266
  help: "Optional. HTTPS link the title text opens (the non-embed page).",
9198
9267
  htmlFor: "video-title-url",
9199
- children: /* @__PURE__ */ jsx(
9200
- Input,
9201
- {
9202
- id: "video-title-url",
9203
- type: "url",
9204
- value: block.title_url ?? "",
9205
- placeholder: "e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ",
9206
- onChange: (e) => onChange({ ...block, title_url: e.target.value || void 0 })
9207
- }
9208
- )
9268
+ children: [
9269
+ /* @__PURE__ */ jsx(
9270
+ Input,
9271
+ {
9272
+ id: "video-title-url",
9273
+ type: "url",
9274
+ value: block.title_url ?? "",
9275
+ placeholder: "e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ",
9276
+ onChange: (e) => onChange({ ...block, title_url: e.target.value || void 0 }),
9277
+ "aria-invalid": titleUrlIsUnsafe || void 0
9278
+ }
9279
+ ),
9280
+ titleUrlIsUnsafe && /* @__PURE__ */ jsx(UnsafeUrlNote, { children: "Only http(s), mailto, tel, sms, and xmpp links are allowed. This URL will be stripped before send and preview." })
9281
+ ]
9209
9282
  }
9210
9283
  ),
9211
9284
  /* @__PURE__ */ jsx(EditorField, { label: "Description", help: "Optional. Up to 199 characters.", htmlFor: "video-description", children: /* @__PURE__ */ jsx(
@@ -9249,22 +9322,26 @@ function VideoEditor({ block, onChange }) {
9249
9322
  )
9250
9323
  }
9251
9324
  ),
9252
- /* @__PURE__ */ jsx(
9325
+ /* @__PURE__ */ jsxs(
9253
9326
  EditorField,
9254
9327
  {
9255
9328
  label: "Provider icon URL",
9256
9329
  help: "Optional. Small icon shown next to the provider name.",
9257
9330
  htmlFor: "video-provider-icon",
9258
- children: /* @__PURE__ */ jsx(
9259
- Input,
9260
- {
9261
- id: "video-provider-icon",
9262
- type: "url",
9263
- value: block.provider_icon_url ?? "",
9264
- placeholder: "e.g. https://example.com/youtube-favicon.png",
9265
- onChange: (e) => onChange({ ...block, provider_icon_url: e.target.value || void 0 })
9266
- }
9267
- )
9331
+ children: [
9332
+ /* @__PURE__ */ jsx(
9333
+ Input,
9334
+ {
9335
+ id: "video-provider-icon",
9336
+ type: "url",
9337
+ value: block.provider_icon_url ?? "",
9338
+ placeholder: "e.g. https://example.com/youtube-favicon.png",
9339
+ onChange: (e) => onChange({ ...block, provider_icon_url: e.target.value || void 0 }),
9340
+ "aria-invalid": providerIconIsUnsafe || void 0
9341
+ }
9342
+ ),
9343
+ providerIconIsUnsafe && /* @__PURE__ */ jsx(UnsafeUrlNote, { children: "Only http(s) image URLs are allowed. This URL will be stripped before send and preview." })
9344
+ ]
9268
9345
  }
9269
9346
  )
9270
9347
  ] });