Skip to content

feat(widget): safely render Markdown images for product photos and payment QR #9

Open

Description

@zuey-cloud-harness

Context

The current safe Markdown renderer in packages/dewee-webchat/src/widget/markdown.ts builds DOM nodes directly (no innerHTML), supporting text, links, tables, code blocks, etc., but does not parse the Markdown image syntax ![alt](url) into an actual image.

Verified on main at 42dfbc4628f45c2d006a2408a3b656a949680abb:

  • INLINE_SRC recognizes [label](url) but has no image token; leading ! is treated as plain text.
  • renderInline creates <a> elements for safe links but never <img>.
  • packages/dewee-webchat/test/markdown.test.ts explicitly asserts that raw untrusted <img src=... onerror=...> must remain escaped. This is a security property to preserve, not a test to delete.
  • packages/dewee-webchat/src/widget/styles.ts has no responsive Markdown image styles.

Why it matters

A Dewee sales agent using a Storefront MCP server should be able to show product images alongside names, prices, descriptions, and comparison tables. A Payment/SePay/VietQR MCP tool may return a payment QR image URL. Today, the chat can show image URLs as text/links but not render them natively in Markdown.

This issue covers safe Markdown image rendering (first usable step). Interactive product cards, product carousels, and a richer checkout/QR message component should be built later on a typed rich-message contract rather than embedding executable HTML in LLM output.

Requirements

1. Markdown image parsing and rendering

  • Add support for ![alt text](https://example.com/product.jpg) and relevant safe relative URLs while preserving existing text/link syntax. Nested/escaped syntax should be handled deliberately and covered by tests.
  • Create <img> DOM elements with document.createElement, assign src/text safely, and never use innerHTML or arbitrary HTML supplied by the model.
  • Set alt, loading="lazy", decoding="async" and referrerPolicy="no-referrer"; avoid content jumping via sensible container sizing if feasible.
  • Product and payment QR images must fit the chat bubble on mobile and desktop, retain correct aspect ratio, and not overflow horizontally.
  • Broken/rejected images should fall back gracefully to alt text or a safe clickable link.

2. Image URL trust/privacy policy

  • Images are fetched by the visitor's browser; untrusted remote images can track visitor IPs or send requests to unexpected origins. Do not reuse safeHref unmodified as the image policy.
  • Reject unsafe schemes (javascript:, data:, blob:, file:, etc.), protocol-relative URLs, control/obfuscated schemes, and suspicious inputs. Support approved HTTPS origins and safe same-origin URLs.
  • Provide a host-configurable image origin allowlist / opt-in for third-party images so a site's trusted Storefront CDN and payment-QR host can be explicitly approved; fail safely for unapproved sources.
  • Never allow onerror/event handlers, inline SVG, arbitrary HTML attributes, or user-supplied script. Consider SVG file/image handling explicitly in the threat model.
  • Apply the policy consistently to streamed replies, conversation history, inline widget mode, and floating mode.

3. Public API and styling

  • Expose the required image-host configuration through the appropriate public widget options and wrappers (vanilla/script, React, Vue, Astro) without breaking existing options.
  • Add styles in src/widget/styles.ts for responsive images, rounded corners, sensible max-size, and accessible alt/fallbacks.
  • Document the Markdown syntax, security policy, and how to allow a Storefront CDN or VietQR image host in the package README.
  • Keep the bundle lightweight and do not add a large Markdown/HTML framework just for images.

4. Tests / acceptance criteria

  • Valid trusted ![Product](https://trusted-shop.example/photo.jpg) renders an <img> with correct alt/source and privacy attributes when the host is allowed.
  • Valid QR image URL from an explicitly allowed payment origin displays without distortion.
  • Plain links [text](url), autolinks, fenced/inline code, bold/italic, tables, and lists still render as before.
  • An untrusted literal <img src=x onerror=...> remains escaped and never becomes an image.
  • URLs with disallowed schemes, origins, malformed encoding, or injected attributes never lead to unsafe DOM nodes or outbound image fetches.
  • Markdown image syntax inside code spans/fences stays literal.
  • Failure/unavailable image has an accessible fallback; mobile widths do not overflow.
  • Streaming / history rerenders have consistent behavior and do not duplicate nodes.
  • pnpm test, pnpm typecheck, and pnpm build pass; add tests in packages/dewee-webchat/test/markdown.test.ts and a widget-level regression test where useful.

Implementation approach

  1. Introduce a dedicated image token/parser branch that precedes link parsing, without sacrificing the renderer's current safe DOM construction.
  2. Centralize URL validation and widget host policy rather than relying on regex alone; normalize URLs using URL and compare normalized origins against a site-owned allowlist.
  3. Render with restricted attributes and CSS classes, then add coverage for adversarial inputs and view-mode parity.
  4. Document approved CDN/QR host examples as configuration placeholders, not hard-coded trusted services.

Explicit non-goals for this issue

  • No direct integration with Storefront MCP, SePay, or VietQR API in this repository.
  • No order creation, payment verification, checkout state changes, or business logic in the renderer.
  • No arbitrary HTML/JS execution or generalized remote content embed.
  • Typed product_card, product_comparison, payment_qr/checkout components: propose separately once image support is stable.

References

Activity

  1. self-assigned this
    on Nov 8, 2026
  2. added
    in reviewImplementation under review
    in progressClaimed; implementation underway
    and removed
    in progressClaimed; implementation underway
    in reviewImplementation under review
    on Nov 8, 2026
  3. mrgoonie commented on Nov 8, 2026

    @mrgoonie
    Contributor

    The bounded image proxy and widget rendering were merged in #38. The issue remains open for the documented platform gaps: Cloudflare Workers currently refuse sources on the endpoint's same site to avoid bypassing same-zone protections, and PHP image fetching on Windows needs safe private temporary storage (#33). Focused tests passed; PR CI passed on Linux. The production release has not yet been made.

  4. mrgoonie commented on Nov 8, 2026

    @mrgoonie
    Contributor

    PR #43 added an opt-in for exact same-origin public Cloudflare Workers Assets, with a separate path allowlist, origin-bound tickets and no fallback to network fetch. The configured assets must be public because the binding bypasses website middleware. It is available in 0.5.0-beta.2. Sibling same-zone sources remain restricted; use a separately approved image domain for those.

    Windows PHP image fetching still returns 503 image_unavailable because the shared image-slot lock requires verified private storage and PHP cannot safely establish the necessary Windows ACL with its POSIX checks. Issue #33 is closed on its explicit fail-closed acceptance criterion for the limiter; this image limitation remains open here. Verification: independent security review; Workers Assets runtime smoke (public 200, disallowed 403, missing 502); Windows package suite, Linux CI and post-merge CI passed.

  5. added
    in reviewImplementation under review
    in progressClaimed; implementation underway
    and removed
    in progressClaimed; implementation underway
    in reviewImplementation under review
    on Nov 8, 2026
  6. mrgoonie commented on Nov 8, 2026

    @mrgoonie
    Contributor

    PR #49 adds opt-in public Worker service bindings for approved sibling-host image paths. The binding receives only the approved path, and its service ID and the endpoint origin are bound to the short-lived encrypted ticket. Prefix overlap is rejected; redirects, invalid raster data and binding failures fail closed without a network fallback. Binding targets must expose public images only because hostname security rules are bypassed; rotate the ID when repointing a binding.

    Verification: independent security review; 13 focused route tests; full Windows suite 407 passed / 5 environment skips; typecheck, build, bundler checks; two-Worker Miniflare/workerd service-binding smoke (ticket 200, image 200, forbidden path 403); PR and main CI passed. The beta.4 release workflow succeeded; npm propagation and CDN publication are being verified. Public docs follow-up: nextlevelbuilder/dewee-web-v2#134.

    The issue remains open: PHP on Windows still fails closed for image fetching until safe shared image-slot locking is available.

  7. mrgoonie commented on Nov 8, 2026

    @mrgoonie
    Contributor

    Release verification: 0.5.0-beta.4 is available on npm's beta tag and as a GitHub prerelease. The PHP release asset matches the tagged source. The CDN beta.4 JavaScript matches npm byte-for-byte, and CDN latest remains identical to stable 0.4.2. Release workflow: https://github.com/nextlevelbuilder/dewee-webchat/actions/runs/42497212390

    The Windows PHP locking limitation remains open.

  8. added
    in reviewImplementation under review
    and removed
    in progressClaimed; implementation underway
    on Nov 8, 2026
  9. mrgoonie commented on Nov 8, 2026

    @mrgoonie
    Contributor

    A beta.4 regression was found: service-binding image tickets use s.<id>.<ciphertext>, but the widget accepts only untagged base64url tickets and therefore falls back instead of loading the image. The GitHub beta.4 release notes now warn about this.

    PR #51 fixes the ticket parser. The failure was reproduced in inline and floating mode before the fix; handler-to-client image redemption and malformed ticket tests now pass. Independent review found no remaining issues; CI is running. A corrected beta release will follow. The Windows PHP lock remains a separate unresolved part of this issue.

  10. added
    in progressClaimed; implementation underway
    and removed
    in reviewImplementation under review
    on Nov 8, 2026
  11. mrgoonie commented on Nov 8, 2026

    @mrgoonie
    Contributor

    The beta.4 tagged-ticket widget regression is fixed by #51 and released as 0.5.0-beta.5 (npm beta, GitHub prerelease, and CDN). Release workflow 42500853671 and both PR/main CI passed; PHP asset matches the tag, CDN beta.5 matches npm byte-for-byte, and latest remains stable 0.4.2.

    In addition to the focused regression tests, an isolated real Chromium session loaded the published beta.5 CDN bundle, requested a tagged ticket from the actual handler using a service binding, and displayed the returned PNG: image HTTP 200, one binding call, nonzero intrinsic width. The Windows PHP locking limitation remains open.

    Public docs follow-up: nextlevelbuilder/dewee-web-v2#134.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

ai-handleAI-assisted implementation readyenhancementNew feature or requestin progressClaimed; implementation underway

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions