Inline media embeds: remaining providers (low priority) #200

Open
opened 2026-09-17 23:24:15 -04:00 by jared · 0 comments
Owner

Migrated from LOTUS_TODO.md on 2026-09-17 (the file is now reference-only).

[ ] Inline media embeds — remaining providers (LOW PRIORITY)

The inline embed system (videoEmbed.ts) covers 18 providers (16 + Mixcloud/Deezer); three more were deliberately deferred (verified against 2026 docs by review agents):

  • Bandcamp (highest-value audio add) — needs an oEmbed round-trip: the player URL requires numeric album/track item ids that aren't in the page URL (bandcamp.com/oembed is the resolver; mirror the TikTokEmbedCard on-click oEmbed pattern). CSP frame-src: bandcamp.com. Classify kind: 'audio'.
  • SoundCloud on.soundcloud.com short links — the w.soundcloud widget resolver does not follow the redirect; needs the same on-click oEmbed resolve (soundcloud.com/oembed, CORS-enabled) to get the canonical URL. (Canonical soundcloud.com/{user}/{track} links already work.)
  • Vimeo event/{id} (live events) + ondemand/… — event embed host is vimeo.com (not player.vimeo.com, so it needs a new CSP frame-src host); on-demand is paywalled and doesn't embed for non-purchasers. Low ROI — only do the event case if vimeo.com is widened for another reason.

Also open (from the quality review): a real onError/error-state fallback for iframes that fail to load (deleted post / region lock / X login-wall) — cross-origin frames don't fire onError reliably, so this needs a load-timeout heuristic; the Close button + badge link are the current escape hatch.

Steam detailed embed (2026-07, 2-agent review) — ef82650c. store.steampowered.com content URLs get rich cards: app pages → OG capsule header + click-to-play facade → Steam's official /widget/{id} store iframe (live region-aware price / discount % / Buy on Steam, gated by inlineMediaEmbeds); news/announcement → banner + headline + body-preview card; bundle/sub/dlc → OG store card. getSteamTarget/steamWidgetEmbedUrl in videoEmbed.ts (+tests). Grounded in prod CSP (frame-src https: allows the widget with no infra change; images via homeserver mxc; NO client-side Steam API — connect-src + Steam CORS both block it, which is the honest ceiling: no review scores/genres/screenshots client-side). Needs on-device QA: the live widget iframe height/fit (can't render headlessly) — verify the price/Buy stay visible on desktop-wide and phone.

GIF previews now animate + Mixcloud/Deezer embeds (2026-07, 2-agent review) — 4154cae5. Reported live: a media.giphy.com link "shows the gif's image but doesn't play it." Root cause: Synapse's /thumbnail endpoint flattens animated GIFs to a still first frame, and every preview image went through it. GifCard (Giphy/Tenor) + the generic OG card now request the original via /download (mxcUrlToHttp with no width/height) when the preview is a GIF (og:image:type === 'image/gif' or a .gif pathname). Guarded: shouldServeGifOriginal() keeps the frozen thumbnail past a 10 MB matrix:image:size cap, and the generic card's eager <img> gained the loading="lazy" it was the only preview image missing. Also added Mixcloud + Deezer audio embeds, and fixed Deezer podcasts (they live at /show/<id>, not /podcast/<id> — the latter 404s on Deezer's own oEmbed; verified against the live API). Needs on-device QA: confirm a large GIF still animates and doesn't stall the timeline.

Embed bug hunt (2026-07, 3 survey agents + 2-agent review) — f2673eff. Core posture verified sound (iframe sandbox, useIframeAutoHeight postMessage origin+source trust, no XSS/dangerouslySetInnerHTML, rel="noreferrer" on all 21 links, oEmbed no-SSRF, the whole facade→iframe/abort/observer lifecycle). Fixed: Twitch/Kick/SoundCloud/Streamable reserved-path over-match (utility pages rendered as broken players), Vimeo hash over-capture ([0-9a-f]{6,}), Spotify/Steam/Discord/IMDb og:image now via mxcUrlToHttp (was a broken raw mxc:// <img> + a pre-click 3p-request facade bypass), wide class follows the og:url-resolved embed, Twitter host alignment (mobile.twitter.com//statuses/), URL de-dupe.

Deferred / surfaced from the hunt (not fixed — decide before doing):

  • Security-vs-functionality tradeoff (needs a call): drop allow-popups-to-escape-sandbox and/or clipboard-write from EMBED_SANDBOX/allow= on embed iframes — real hardening against a compromised provider (phishing popup / clipboard hijack), but risks breaking a legit provider popup/copy on the trusted major providers we embed. Low marginal value; not shipped blindly.
  • Defense-in-depth: encodeURIComponent the Bluesky authority + Apple Music path/search interpolated into the embed src (not currently exploitable — host is fixed and value comes from URL.pathname; React escapes the attribute).
  • Out of embed scope (real, low-sev): LotusDenoiseFeature (ClientNonUIFeatures.tsx) has a window message listener with no origin/source check → any frame/window can post {type:'lotus-denoise-status', error} and pop a forged "System" toast (text only, no XSS). Validate event.source.
  • Lifecycle Lows (cosmetic/latent): a re-fetch flips a playing embed back to the spinner (latent — url is keyed); auto-height retained across close→reopen; extractEmbedHeight generic .height fallback accepts any allowed-origin message; TweetEmbed theme is a one-time matchMedia snapshot (no live theme switch); host-normalization gaps (vt.tiktok.com misses StackOnMobile, m.instagram.com, www.youtu.be).
_Migrated from `LOTUS_TODO.md` on 2026-09-17 (the file is now reference-only)._ ### [ ] Inline media embeds — remaining providers (LOW PRIORITY) The inline embed system (`videoEmbed.ts`) covers 18 providers (16 + Mixcloud/Deezer); three more were **deliberately deferred** (verified against 2026 docs by review agents): - **Bandcamp** (highest-value audio add) — needs an **oEmbed** round-trip: the player URL requires numeric `album`/`track` item ids that aren't in the page URL (`bandcamp.com/oembed` is the resolver; mirror the `TikTokEmbedCard` on-click oEmbed pattern). CSP `frame-src`: `bandcamp.com`. Classify `kind: 'audio'`. - **SoundCloud `on.soundcloud.com` short links** — the `w.soundcloud` widget resolver does **not** follow the redirect; needs the same on-click oEmbed resolve (`soundcloud.com/oembed`, CORS-enabled) to get the canonical URL. (Canonical `soundcloud.com/{user}/{track}` links already work.) - **Vimeo `event/{id}` (live events) + `ondemand/…`** — event embed host is `vimeo.com` (**not** `player.vimeo.com`, so it needs a new CSP `frame-src` host); on-demand is paywalled and doesn't embed for non-purchasers. Low ROI — only do the event case if `vimeo.com` is widened for another reason. Also open (from the quality review): a real `onError`/error-state fallback for iframes that fail to load (deleted post / region lock / X login-wall) — cross-origin frames don't fire `onError` reliably, so this needs a load-timeout heuristic; the Close button + badge link are the current escape hatch. **✅ Steam detailed embed (2026-07, 2-agent review) — `ef82650c`.** `store.steampowered.com` content URLs get rich cards: **app** pages → OG capsule header + click-to-play facade → Steam's official `/widget/{id}` store iframe (live region-aware price / discount % / Buy on Steam, gated by `inlineMediaEmbeds`); **news/announcement** → banner + headline + body-preview card; **bundle/sub/dlc** → OG store card. `getSteamTarget`/`steamWidgetEmbedUrl` in `videoEmbed.ts` (+tests). Grounded in prod CSP (`frame-src https:` allows the widget with no infra change; images via homeserver `mxc`; NO client-side Steam API — `connect-src` + Steam CORS both block it, which is the honest ceiling: no review scores/genres/screenshots client-side). **Needs on-device QA:** the live widget iframe height/fit (can't render headlessly) — verify the price/Buy stay visible on desktop-wide and phone. **✅ GIF previews now animate + Mixcloud/Deezer embeds (2026-07, 2-agent review) — `4154cae5`.** Reported live: a `media.giphy.com` link "shows the gif's image but doesn't play it." Root cause: **Synapse's `/thumbnail` endpoint flattens animated GIFs to a still first frame**, and every preview image went through it. `GifCard` (Giphy/Tenor) + the generic OG card now request the **original** via `/download` (`mxcUrlToHttp` with no width/height) when the preview is a GIF (`og:image:type === 'image/gif'` or a `.gif` pathname). Guarded: `shouldServeGifOriginal()` keeps the frozen thumbnail past a **10 MB** `matrix:image:size` cap, and the generic card's eager `<img>` gained the `loading="lazy"` it was the only preview image missing. Also added **Mixcloud + Deezer** audio embeds, and fixed Deezer podcasts (they live at `/show/<id>`, **not** `/podcast/<id>` — the latter 404s on Deezer's own oEmbed; verified against the live API). **Needs on-device QA:** confirm a large GIF still animates and doesn't stall the timeline. **✅ Embed bug hunt (2026-07, 3 survey agents + 2-agent review) — `f2673eff`.** Core posture verified **sound** (iframe sandbox, `useIframeAutoHeight` postMessage origin+source trust, no XSS/`dangerouslySetInnerHTML`, `rel="noreferrer"` on all 21 links, oEmbed no-SSRF, the whole facade→iframe/abort/observer lifecycle). Fixed: Twitch/Kick/SoundCloud/Streamable reserved-path over-match (utility pages rendered as broken players), Vimeo hash over-capture (`[0-9a-f]{6,}`), Spotify/Steam/Discord/IMDb `og:image` now via `mxcUrlToHttp` (was a broken raw `mxc://` `<img>` + a pre-click 3p-request facade bypass), `wide` class follows the og:url-resolved embed, Twitter host alignment (`mobile.twitter.com`/`/statuses/`), URL de-dupe. **Deferred / surfaced from the hunt (not fixed — decide before doing):** - **Security-vs-functionality tradeoff (needs a call):** drop `allow-popups-to-escape-sandbox` and/or `clipboard-write` from `EMBED_SANDBOX`/`allow=` on embed iframes — real hardening against a _compromised_ provider (phishing popup / clipboard hijack), but risks breaking a legit provider popup/copy on the trusted major providers we embed. Low marginal value; not shipped blindly. - **Defense-in-depth:** `encodeURIComponent` the Bluesky authority + Apple Music path/search interpolated into the embed `src` (not currently exploitable — host is fixed and value comes from `URL.pathname`; React escapes the attribute). - **Out of embed scope (real, low-sev):** `LotusDenoiseFeature` (`ClientNonUIFeatures.tsx`) has a `window` `message` listener with **no origin/source check** → any frame/window can post `{type:'lotus-denoise-status', error}` and pop a forged **"System"** toast (text only, no XSS). Validate `event.source`. - **Lifecycle Lows (cosmetic/latent):** a re-fetch flips a playing embed back to the spinner (latent — url is keyed); auto-height retained across close→reopen; `extractEmbedHeight` generic `.height` fallback accepts any allowed-origin message; `TweetEmbed` theme is a one-time `matchMedia` snapshot (no live theme switch); host-normalization gaps (`vt.tiktok.com` misses `StackOnMobile`, `m.instagram.com`, `www.youtu.be`).
jared added this to the Features 2026-Q4 milestone 2026-09-17 23:24:15 -04:00
jared added the enhancementpriority: lowarea: media labels 2026-09-17 23:24:15 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#200