Skip to content

FE-1500: Add oEmbed discovery and framing security - #9363

Merged
kube merged 1 commit into
claude/fe-1500-embedfrom
codex/fe-1500-oembed
Sep 1, 2026
Merged

FE-1500: Add oEmbed discovery and framing security#9363
kube merged 1 commit into
claude/fe-1500-embedfrom
codex/fe-1500-oembed

Conversation

@kube

@kube kube commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Adds oEmbed support for the example pages: a /api/oembed Vercel 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?

  • New api/oembed.ts fetch handler: validates canonical https://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 a rich oEmbed response whose iframe targets /embed/examples/... with sandbox="allow-scripts allow-same-origin", no referrer, and lazy loading. allow-same-origin is 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/selection entry, so the function bundles neither the editor nor the model.
  • Example pages render an application/json+oembed discovery <link> declaratively; React 19 hoists it into document.head. The site is a client-rendered SPA, so discovery works for consumers that execute JavaScript; this limitation is by design.
  • vercel.json framing policy: frame-ancestors 'none' plus X-Frame-Options: DENY for the whole site except /embed/examples/*, which allows all ancestors and adds nosniff.
  • Generalizes the Vite dev API plugin from a hardcoded /api/chat middleware to a table over /api/chat and /api/oembed, so development and production run the same handlers.
  • Protocol conformance: format=xml answers 501 as oEmbed 1.0 requires (a spec-following consumer probes and falls back), and HEAD is served like GET with an empty body.
  • Documents the embed and oEmbed URL contract in the website README.
  • Review fixes (second round): moved api/oembed.test.ts to src/examples/oembed-endpoint.test.ts. Vercel deploys every module under api/ as a function, and staging confirmed it: /api/oembed.test answered 500 while an unknown path answered 404, so the test file was live and crashing on invocation. Also: a blank format=, maxwidth= or maxheight= now means "not specified" rather than an error, which a consumer that always appends the optional params was hitting; maxwidth/maxheight are parsed as integers (0x10 was read as 16) and the 16:9 ratio survives clamping (?maxwidth=1 returned 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; nosniff now covers /api/*, the one response that reflects input, and /embed/examples/* carries X-Robots-Tag: noindex.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

🛡 What tests cover this?

  • New: api/oembed.test.ts (URL validation, state sanitization including multi-item selections, dimension fitting, response headers, method and format handling) and oembed-discovery.test.ts. The selection allowlist no longer needs a drift guard: there is one list, in core.

❓ How to test this?

  1. Checkout the branch and run yarn workspace @apps/petrinaut-website dev.
  2. curl "http://localhost:5173/api/oembed?url=https%3A%2F%2Fdemo.petrinaut.org%2Fexamples%2Fgases-1-pn&maxwidth=600".
  3. Confirm a JSON oEmbed response whose html iframe targets /embed/examples/gases-1-pn, and open an example page to see the discovery <link> in document.head.

@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hash Ready Ready Preview Sep 1, 2026 11:10am UTC
petrinaut Ready Ready Preview Sep 1, 2026 11:10am UTC
petrinaut-docs Ready Ready Preview Sep 1, 2026 11:10am UTC
1 Skipped Deployment
Project Deployment Actions Updated
hashdotdesign-tokens Ignored Ignored Preview Sep 1, 2026 11:10am UTC

Request Review

@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
New public API reflects user-supplied URLs into HTML iframes, but input is heavily validated and escaped; framing policy changes affect how the site can be embedded sitewide.

Overview
Adds JSON oEmbed 1.0 so third parties can embed canonical /examples/<slug> demos. A new /api/oembed Vercel handler accepts a production example URL, validates it against shared catalog metadata, sanitizes query state through the existing example-search contract, fits maxwidth/maxheight without upscaling, and returns a rich response with a sandboxed iframe pointing at /embed/examples/....

Example pages now inject an application/json+oembed discovery <link> (via getOEmbedDiscoveryUrl, always using the demo origin and canonical params). vercel.json blocks framing on the main site (frame-ancestors 'none', X-Frame-Options: DENY) while /embed/examples/* allows embedding and is marked noindex. The Vite dev plugin serves both /api/chat and /api/oembed like production.

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.

Comment thread apps/petrinaut-website/api/oembed.ts
@semgrep-code-hashintel

Copy link
Copy Markdown

Semgrep found 3 detect-replaceall-sanitization findings:

Detected a call to replaceAll() in an attempt to HTML escape the string value .replaceAll("&", "&amp;") .replaceAll('"', "&quot;") .replaceAll("'", "&#39;") .replaceAll("<", "&lt;"). Manually sanitizing input through a manually built list can be circumvented in many situations, and it's better to use a well known sanitization library such as sanitize-html or DOMPurify.

Comment thread apps/petrinaut-website/api/oembed.ts
Comment thread apps/petrinaut-website/api/oembed.ts
Comment thread apps/petrinaut-website/api/oembed.ts
Comment thread apps/petrinaut-website/api/oembed.ts
Comment thread apps/petrinaut-website/api/oembed.ts Outdated
Comment thread apps/petrinaut-website/api/oembed.ts
Comment thread apps/petrinaut-website/api/oembed.ts
@semgrep-code-hashintel

Copy link
Copy Markdown

Semgrep found 1 regex_dos finding:

  • apps/petrinaut-website/src/main/app/brunch-demo/brunch-endpoint.ts

Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.

CiaranMn
CiaranMn previously approved these changes Sep 1, 2026
CiaranMn
CiaranMn previously approved these changes Sep 1, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread apps/petrinaut-website/vercel.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps area/infra Relates to version control, CI, CD or IaC (area)

Development

Successfully merging this pull request may close these issues.

4 participants