FE-1500: Add oEmbed discovery and framing security - #9363
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
PR SummaryMedium Risk Overview Example pages now inject an Coverage includes endpoint behavior (CORS, caching, HEAD, 404 vs 400, XSS-safe iframe HTML) and discovery URL stability. Reviewed by Cursor Bugbot for commit dea7ff2. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
Semgrep found 3 Detected a call to |
21dbd64 to
02e7685
Compare
02e7685 to
798bd3d
Compare
798bd3d to
91c9ccb
Compare
91c9ccb to
e371efe
Compare
ff0b3d3 to
85f0b57
Compare
85f0b57 to
512b666
Compare
512b666 to
692ba20
Compare
692ba20 to
80c03be
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit dea7ff2. Configure here.

🌟 What is the purpose of this PR?
Adds oEmbed support for the example pages: a
/api/oembedVercel function that turns a canonical example URL into a sandboxed iframe embed, runtime discovery links on the example pages, and site-wide framing security headers. Stacked on the embed-route PR.🔗 Related links
🔍 What does this change?
api/oembed.tsfetch handler: validates canonicalhttps://demo.petrinaut.org/examples/<slug>URLs against catalog metadata, strips query state down to the embed-supported scenario/subnet/selection keys (single and multi-item selections), fits requested dimensions without upscaling, and returns arichoEmbed response whose iframe targets/embed/examples/...withsandbox="allow-scripts allow-same-origin", no referrer, and lazy loading.allow-same-originis required: without it the framed document runs with an opaque origin, module-script fetches fail CORS, and the embed stays blank; the framed content is this site's own app, and framing policy is enforced server-side by the headers below. The sanitizer decodes the canonical URL through the shared contract (FE-1500: Add example models and the read-only example page #9362) and re-encodes it, so the endpoint cannot drift from what the embed page accepts. That module is React-free by design, and the selection vocabulary it uses comes from a dependency-free@hashintel/petrinaut-core/selectionentry, so the function bundles neither the editor nor the model.application/json+oembeddiscovery<link>declaratively; React 19 hoists it intodocument.head. The site is a client-rendered SPA, so discovery works for consumers that execute JavaScript; this limitation is by design.vercel.jsonframing policy:frame-ancestors 'none'plusX-Frame-Options: DENYfor the whole site except/embed/examples/*, which allows all ancestors and addsnosniff./api/chatmiddleware to a table over/api/chatand/api/oembed, so development and production run the same handlers.format=xmlanswers 501 as oEmbed 1.0 requires (a spec-following consumer probes and falls back), and HEAD is served like GET with an empty body.api/oembed.test.tstosrc/examples/oembed-endpoint.test.ts. Vercel deploys every module underapi/as a function, and staging confirmed it:/api/oembed.testanswered 500 while an unknown path answered 404, so the test file was live and crashing on invocation. Also: a blankformat=,maxwidth=ormaxheight=now means "not specified" rather than an error, which a consumer that always appends the optional params was hitting;maxwidth/maxheightare parsed as integers (0x10was read as 16) and the 16:9 ratio survives clamping (?maxwidth=1returned a 1x1 embed); a well-formed URL this provider cannot embed answers 404 rather than 400, per oEmbed 1.0 section 2.3.1; HEAD is bodiless on the error paths too; the discovery link is built from the slug and the validated search, so a tracking parameter no longer advertises a second endpoint URL for an identical response;nosniffnow covers/api/*, the one response that reflects input, and/embed/examples/*carriesX-Robots-Tag: noindex.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
🛡 What tests cover this?
api/oembed.test.ts(URL validation, state sanitization including multi-item selections, dimension fitting, response headers, method and format handling) andoembed-discovery.test.ts. The selection allowlist no longer needs a drift guard: there is one list, in core.❓ How to test this?
yarn workspace @apps/petrinaut-website dev.curl "http://localhost:5173/api/oembed?url=https%3A%2F%2Fdemo.petrinaut.org%2Fexamples%2Fgases-1-pn&maxwidth=600".htmliframe targets/embed/examples/gases-1-pn, and open an example page to see the discovery<link>indocument.head.