Description
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  into an actual image.
Verified on main at 42dfbc4628f45c2d006a2408a3b656a949680abb:
INLINE_SRCrecognizes[label](url)but has no image token; leading!is treated as plain text.renderInlinecreates<a>elements for safe links but never<img>.packages/dewee-webchat/test/markdown.test.tsexplicitly 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.tshas 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
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 withdocument.createElement, assignsrc/text safely, and never useinnerHTMLor arbitrary HTML supplied by the model. - Set
alt,loading="lazy",decoding="async"andreferrerPolicy="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
safeHrefunmodified 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.tsfor 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
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, andpnpm buildpass; add tests inpackages/dewee-webchat/test/markdown.test.tsand a widget-level regression test where useful.
Implementation approach
- Introduce a dedicated image token/parser branch that precedes link parsing, without sacrificing the renderer's current safe DOM construction.
- Centralize URL validation and widget host policy rather than relying on regex alone; normalize URLs using
URLand compare normalized origins against a site-owned allowlist. - Render with restricted attributes and CSS classes, then add coverage for adversarial inputs and view-mode parity.
- 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.
Activity
mrgoonie commentedon Nov 8, 2026
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.
mrgoonie commentedon Nov 8, 2026
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_unavailablebecause 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.mrgoonie commentedon Nov 8, 2026
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.
mrgoonie commentedon Nov 8, 2026
Release verification:
0.5.0-beta.4is available on npm'sbetatag and as a GitHub prerelease. The PHP release asset matches the tagged source. The CDN beta.4 JavaScript matches npm byte-for-byte, and CDNlatestremains identical to stable 0.4.2. Release workflow: https://github.com/nextlevelbuilder/dewee-webchat/actions/runs/42497212390The Windows PHP locking limitation remains open.
mrgoonie commentedon Nov 8, 2026
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.
mrgoonie commentedon Nov 8, 2026
The beta.4 tagged-ticket widget regression is fixed by #51 and released as
0.5.0-beta.5(npmbeta, 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, andlatestremains 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.