Skip to content

feat(studio-server): preview variable injection + render variables forwarding#2049

Merged
jrusso1020 merged 1 commit into
mainfrom
07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding
Jul 9, 2026
Merged

feat(studio-server): preview variable injection + render variables forwarding#2049
jrusso1020 merged 1 commit into
mainfrom
07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What

Fourth PR of the template-variables stack: the HTTP plumbing.

  • Preview routes (/preview and /preview/comp/*) accept ?variables=<url-encoded json> and inject window.__hfVariables = {...} before the runtime and any composition script — the exact global the engine sets via evaluateOnNewDocument at render time, so preview-with-values cannot diverge from render output
  • POST /projects/:id/render accepts variables and forwards them through StudioApiAdapter.startRender into the producer's RenderConfig.variables — the same channel hyperframes render --variables uses. Wired in both adapters (CLI embedded server + vite dev adapter)

Why

Render-time injection existed; preview had no equivalent, so "preview with values" was impossible. Mirroring the engine's exact injection mechanism (rather than a bespoke postMessage protocol) is what makes the Studio preview render-truthful.

How

Safety/correctness details worth reviewing:

  • Values are <-escaped against </script> breakout
  • Malformed payloads 400 instead of silently previewing defaults (the param is new, so no existing consumer regresses; empty param still serves normally)
  • The ETag is salted with a hash of the payload so cached previews revalidate when values change (verified: three distinct ETags for none/A/B values)
  • Injection insertion is quirks-mode-safe: <head…><html…> → after doctype → prepend (review fix — the original bare-<head> regex could land the tag before the doctype)
  • Payload validation is one shared helper (helpers/variablesPayload.ts) used by both routes

Test plan

  • Route tests: injection placement + escaping, 400s on invalid/array payloads, ETag variance, sub-composition injection, render forwarding/omission/rejection (56 tests in the two route files)
  • Live HTTP smoke test against a running server: injection present with correct values, 400s, ETag variance (see fix(studio): code-review and live-test fixes for the variables stack #2052)
  • Wire-level check: Studio's Export click POSTs variables to this route

🤖 Generated with Claude Code

@jrusso1020 jrusso1020 marked this pull request as ready for review July 8, 2026 05:15
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from a4cd1b5 to a2085a5 Compare July 8, 2026 06:08
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 5870b13 to c843527 Compare July 8, 2026 06:08
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from c843527 to dc24770 Compare July 8, 2026 08:00
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch 2 times, most recently from 6acf033 to 39e9b70 Compare July 8, 2026 16:17
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch 2 times, most recently from 2c0283f to e0f52e3 Compare July 8, 2026 16:24
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from 39e9b70 to cd67511 Compare July 8, 2026 16:24
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from e0f52e3 to aa20eba Compare July 8, 2026 17:10
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from cd67511 to 5e26843 Compare July 8, 2026 17:10
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from aa20eba to 59929fb Compare July 8, 2026 21:38
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from 5e26843 to 47e6506 Compare July 8, 2026 21:38

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — clean server-side plumbing.

The shared variablesPayload.ts helper keeps the preview and render route contracts in sync, the <-escaping prevents script breakout, and the ETag salting ensures correct cache invalidation per variable set. Test coverage is thorough across both routes.

One minor observation (not blocking): parsePreviewVariablesParam and previewVariablesFromRequest are thin wrappers over each other — but the layering is justified since previewVariablesFromRequest packages the raw string for ETag salting alongside the parsed values, which the caller needs as a bundle. Clean separation.

— Miga

miguel-heygen
miguel-heygen previously approved these changes Jul 9, 2026

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Stamped after Miga's stack review. GitHub state checked: mergeable, no red checks visible; #2046 base conflict still needs resolution separately.

@james-russo-rames-d-jusso james-russo-rames-d-jusso 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.

Reviewed at 59929fb. Stack context: render-path lane (#2049 / #2054 / #2084) — 3-of-12 in template-variables stack.

🟢 What's good

  • <-escape defends against </script> breakoutJSON.stringify(values).replace(/</g, "\\u003c") is the right one-liner, and the test at preview.test.ts:57-70 exercises exactly the breakout payload ("</script><script>alert(1)</script>") and asserts the escaped form. Nice.
  • ETag salting (variablesEtagSalt) — SHA-1 of the raw query string, 12-hex slice — is the right shape and the three-distinct-ETag test at preview.test.ts:85-99 proves cache coherency across none / A / B. Cheap and correct.
  • Quirks-mode-safe insertion chain — the <head…><html…> → after-doctype → prepend fallback is defensible; the PR body flags this as a review fix over a bare <head> regex, and that catch was worth it.
  • Malformed payload → 400, not silent-defaults on both ?variables= and the render POST body. The rubric in the render.ts comment ("Unlike fps/quality (lenient with safe fallbacks)…") justifies the asymmetry well.
  • Shared payload helper (variablesPayload.ts) enforces one contract, one error string across both routes — preview and render can't drift on what counts as a valid variables payload.
  • CLI + Vite adapter parity — both StudioApiAdapter.startRender call sites forward variables symmetrically; no drift between the CLI-embedded server and the Vite dev adapter.

🟡 Nits / questions

  • <head[^>]*> regex matches inside HTML comments. An authored HTML with <!-- ... <head> ... --> in the doctype-preamble (rare but legal) would land the injection inside a comment → window.__hfVariables never set → preview silently uses defaults. Not a security issue; a robustness one. A one-line "reject match inside a <!--…--> span" would harden this, but low-priority; almost no real composition has this.
  • Preview vs render mechanism divergence. Preview injects a literal object via JS syntax (window.__hfVariables={"a":1}); render passes a JSON string through evaluateOnNewDocument and does JSON.parse inside the page (frameCapture.ts:901-909). For pure JSON values these are semantically identical, but the PR body's "cannot diverge from render output" claim glosses over one edge: values containing U+2028 / U+2029 are valid JSON strings but historically illegal in JS string/statement contexts (ES2019 fixed this in modern engines). Any customer still shipping a pre-2019 engine on the render side (unlikely) would see a divergence. Not worth blocking on, but worth a note somewhere.
  • ?variables= raw string feeds the ETag salt. Two clients sending the same JSON with different URL-encodings (e.g. %20 vs +) get different ETags — cache duplication, not correctness bugs. Minor.
  • No payload size limit on ?variables= or body.variables. A megabyte-scale payload is inefficient but doesn't cause an obvious DoS on this route. Consider a cap (e.g. 64KB) — future-proofing.
  • Sub-composition preview at /preview/comp/* injects __hfVariables but not __hfVariablesByComp — bindings inside a standalone sub-comp preview will resolve against the top-level global. Probably intentional (previewing a sub-comp standalone means the sub-comp IS the composition), but if any existing consumer previews a sub-comp expecting per-instance scoping, they'll see the top-level values instead. Docs / route contract could clarify.

🟠 Hold

  • No blockers on this PR, but a cross-cutting observation: #2054 (the declarative bindings PR sibling) will consume this payload in the DOM without any URL-protocol filtering. That's the layer where sanitization needs to live, not here. The server payload plumbing is correctly permissive (a variable value is just JSON); the DOM sink is where the safety story lives. Flagging on #2054 in detail.

🔴 Blockers

None.

CI: green minus the pre-existing Perf: matrix.shard template-eval failure (not tied to this diff). Graphite mergeability pending — normal.

Review by Rames D Jusso

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

R1 — hyperframes #2049 at 59929fbe

🟢 LGTM concurring with Miga and with Rames-D's 🟢/🟡 assessment.

Verified independently:

  • </script> escape hygiene. JSON.stringify(vars).replace(/</g, "\\u003c") — right one-liner, and preview.test.ts:57-70 exercises the exact breakout payload.
  • ETag salting via variablesEtagSalt (SHA-1 of raw query, 12-hex slice); three-distinct-tag test at :85-99 proves cache coherency.
  • Quirks-mode-safe insertion chain: <head…><html…> → after-doctype → prepend. Defensible.
  • Malformed payload → 400 on both ?variables= (preview) and body-json (render). No silent-defaults. The rubric contrast in render.ts ("Unlike fps/quality (lenient with safe fallbacks)…") justifies the asymmetry.
  • Shared variablesPayload.ts helper across both routes — preview and render can't drift on the payload contract.
  • CLI + Vite adapter parity — both StudioApiAdapter.startRender sites forward variables symmetrically.
  • Global name matches downstream. window.__hfVariables matches #2054's variableScope.ts lookup exactly.
  • 56 tests + CI green.

Concurring with Miga's LGTM.

Concurring with Rames-D's 🟡 nits — they're all fair calls, none blocking:

  • <head[^>]*> regex could match inside HTML comments. Rare authored input, but a one-line "reject match inside <!--…-->" is cheap hardening.
  • __hfVariablesByComp not populated on standalone sub-comp preview at /preview/comp/*. Bindings inside the standalone sub-comp will resolve against the top-level global. Probably intentional (previewing sub-comp standalone = the sub-comp IS the composition), but the route contract could clarify.
  • U+2028 / U+2029 legality in pre-ES2019 engines. Preview injects literal JS; render uses JSON.parse. Modern engines converged in 2019, so this is theoretical for prod but worth a note.
  • No payload size cap. A future 64KB ?variables= guard would be future-proofing.
  • ?variables= raw string feeds ETag — url-encoding variance produces different ETags (cache duplication, not correctness). Minor.

Rames-D's 🟠 cross-cutting observation is exactly right: the DOM sink safety story lives on #2054, not here. Payload plumbing is correctly permissive. I've concurred on his four defense-in-depth holds over on #2054.

No unique blockers.

R1 by Via

@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from 47e6506 to 4f96eef Compare July 9, 2026 06:48
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch 2 times, most recently from 476b923 to fbc61e4 Compare July 9, 2026 07:57
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from 4f96eef to f6b03f7 Compare July 9, 2026 07:57
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from fbc61e4 to e909dcc Compare July 9, 2026 13:17
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch 2 times, most recently from dc29e00 to 6d6b162 Compare July 9, 2026 18:06
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from e909dcc to 265f388 Compare July 9, 2026 18:06

@james-russo-rames-d-jusso james-russo-rames-d-jusso 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.

R2 at 265f388.

R1 was 🟢 with 🟡 nits (regex-inside-comments edge, U+2028/2029 in JSON.parse-vs-literal-obj corner, no payload size limit). Nits stand as fine-tuning items rather than merge holds — none surface as regressions in the diff.

The CSP compat concern that was cross-stack (this PR emits the inline script the CSP note references) is now documented at docs/concepts/variables.mdx:236-244 — explicit callout of the fallback behavior + nonce/hash workaround.

No residuals from my side. CI green.

Review by Rames D Jusso

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟢 LGTM concurring with Rames-D — R2 at 265f388.

R1 had no 🟠 holds — only 🟡 nits (regex-in-HTML-comment, U+2028/2029 in JSON.parse vs literal-obj, no payload size limit, ETag salt URL-encoding variance, sub-comp __hfVariablesByComp gap). All stand as optional fine-tuning items rather than merge holds.

The cross-stack CSP-compat concern I flagged (this PR emits the inline <script>window.__hfVariables=…</script> that the CSP fallback rule references) is now documented at docs/concepts/variables.mdx:236-244 — explicit callout of the fallback-to-defaults behavior + nonce/hash workaround. Closes the cross-stack loop with #2054.

No residuals from my side.

R2 by Via

@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 265f388 to afd70d5 Compare July 9, 2026 19:34
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from 6d6b162 to 2f5d730 Compare July 9, 2026 19:34
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from afd70d5 to 7631965 Compare July 9, 2026 19:46
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from 2f5d730 to 11d547a Compare July 9, 2026 19:46

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟢 LGTM at R3 — afd70d5.

No R2 holds. R2 verdict stands.

Cross-stack CSP compat docs at docs/concepts/variables.mdx still landed on #2054 through the rebase.

R3 by Via

@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 7631965 to 901a425 Compare July 9, 2026 20:16
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from 11d547a to 193eb5c Compare July 9, 2026 20:16
…rwarding

Fourth PR of the template-variables Studio stack — the HTTP plumbing.

- preview routes (/preview and /preview/comp/*) accept
  ?variables=<url-encoded json> and inject
  `window.__hfVariables = {...}` into <head>, before the runtime and any
  composition script — the exact global the engine sets via
  evaluateOnNewDocument at render time, so preview-with-values cannot
  diverge from render output. Values are escaped against </script>
  breakout, malformed payloads 400 instead of silently previewing
  defaults, and the ETag is salted with a hash of the payload so cached
  previews revalidate when values change.
- POST /projects/:id/render accepts variables ({variableId: value}) and
  forwards them through StudioApiAdapter.startRender into the producer's
  RenderConfig.variables — the same channel `hyperframes render
  --variables` uses. Wired in both adapters (CLI embedded server + vite
  dev adapter).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam branch from 193eb5c to 91831f3 Compare July 9, 2026 20:31
@jrusso1020 jrusso1020 force-pushed the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch from 901a425 to 2e0b884 Compare July 9, 2026 20:31
Base automatically changed from 07-07-feat_sdk_variable_usage_scan_preview-values_adapter_seam to main July 9, 2026 20:32
@jrusso1020 jrusso1020 dismissed miguel-heygen’s stale review July 9, 2026 20:32

The base branch was changed.

@jrusso1020 jrusso1020 merged commit 0da2937 into main Jul 9, 2026
42 of 49 checks passed
@jrusso1020 jrusso1020 deleted the 07-07-feat_studio-server_preview_variable_injection_render_variables_forwarding branch July 9, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants