Skip to content

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

Open
kube wants to merge 1 commit into
claude/fe-1500-embedfrom
codex/fe-1500-oembed
Open

FE-1500: Add oEmbed discovery and framing security#9363
kube wants to merge 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/... URLs against catalog metadata, pins unversioned URLs to revision 1, strips query state down to the embed-supported scenario/subnet/selection keys, fits requested dimensions without upscaling, and returns a rich oEmbed response whose iframe targets /embed/examples/... with sandbox="allow-scripts", no referrer, and lazy loading. The handler imports only catalog metadata so the function never bundles the browser React code; the embed-supported key set is duplicated there for that reason.
  • Example pages inject an application/json+oembed discovery <link> after mount (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.
  • Documents the embed and oEmbed URL contract in the website README.

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 (23 cases: URL validation, revision pinning, state sanitization, dimension fitting, response headers) and oembed-discovery.test.ts.

❓ 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/versions/1, 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 (UTC)
hash Ready Ready Preview Aug 30, 2026 3:43am
petrinaut Ready Ready Preview Aug 30, 2026 3:43am
petrinaut-docs Ready Ready Preview Aug 30, 2026 3:43am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Aug 30, 2026 3:43am

Request Review

@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Public oEmbed output and CSP framing rules affect how example content can be embedded and clicked; input sanitization and HTML escaping are covered by tests but the endpoint is security-sensitive surface area.

Overview
Adds JSON oEmbed discovery for Petrinaut demo example pages so third parties can embed revision-pinned, sandboxed iframes of /embed/examples/....

A new /api/oembed Vercel function accepts canonical https://demo.petrinaut.org/examples/... URLs (validated against catalog metadata), strips query params down to embed-safe scenario/subnet/selection state, honors maxwidth/maxheight without upscaling, and returns oEmbed 1.0 rich HTML with escaped attributes, CORS, and cache headers. FullExamplePage injects an application/json+oembed <link> after mount (production URLs via getOEmbedDiscoveryUrl), since the SPA cannot put per-route discovery in static HTML.

vercel.json now denies framing site-wide (frame-ancestors 'none', X-Frame-Options: DENY) while allowing /embed/examples/* to be framed; the Vite dev plugin serves both /api/chat and /api/oembed. README documents the contract; test:oembed and Vitest suites cover the handler and discovery URL builder.

Reviewed by Cursor Bugbot for commit 85f0b57. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment on lines +86 to +91
value
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
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.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by detect-replaceall-sanitization.

You can view more details about this finding in the Semgrep AppSec Platform.

@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 on lines +86 to +87
value
.replaceAll("&", "&amp;")
Comment on lines +86 to +88
value
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
Comment on lines +86 to +90
value
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;")
.replaceAll("<", "&lt;")
Comment on lines +86 to +91
value
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
Comment thread apps/petrinaut-website/api/oembed.ts
Comment thread apps/petrinaut-website/api/oembed.ts
@github-actions github-actions Bot removed area/deps Relates to third-party dependencies (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team area/apps > hash.design Affects the `hash.design` design site (app) labels Aug 29, 2026
}

return sanitized;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

oEmbed drops multi-item selection

Medium Severity

sanitizePreviewSearch forwards scenario, subnet, and the single-item itemType/itemId pair, but never copies the items query that the Viewer uses for multi-selection. Canonical URLs with two or more selected elements therefore lose that selection in the returned iframe.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7836613. Configure here.

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

There are 2 total unresolved issues (including 1 from previous review).

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 ff0b3d3. Configure here.

}

return sanitized;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Multi-selection state dropped from embeds

Medium Severity

sanitizeEmbedSearch copies scenario, subnet, itemType, and itemId but never the items query key. Embed routes already treat items as the canonical multi-selection encoding, so an oEmbed iframe silently drops any multi-item selection from the source URL.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ff0b3d3. Configure here.

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.

2 participants