mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(parsers,sdk,studio-server,studio): unify hf-id space across preview, disk, and SDK session (#1981)
* fix(parsers,sdk,studio-server,studio): unify hf-id space across preview, disk, and SDK session Root-causes the setTiming element_not_found resolver-shadow divergence class: timeline edits carry hf-ids read from the live preview DOM, but the preview minted ids AFTER rewriting attributes (and never persisted them for sub-comps), while the SDK session mints from the raw file — content-keyed minting then yields different ids for the same element. Template-based comps were worse: the SDK excluded the whole <template> subtree, so the session had zero elements and every edit diverged. - parsers: ensureHfIds now descends into <template> subtrees (linkedom's querySelectorAll does not), minting and pinning inner ids - sdk: buildRoots/buildElement treat <template> as a transparent container, and resolution (resolveScoped, animation-id map) searches template subtrees via querySelectorAllDeep — template comps now model, resolve, and edit - studio-server: the sub-comp preview route persists hf-ids to the raw file BEFORE the rewrite pipeline (mirrors the main route), pinning one id space across served DOM, disk, and SDK session - studio: resolver-shadow skips structurally-empty sessions (no event, no attempt) and tags fail-open emissions with sourceReadFailed so read errors are distinguishable from unwired readers in telemetry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(parsers,sdk,studio-server,studio): scope template descent, guard persist route Addresses the 10 verified findings from the PR #1981 review: - Restrict template transparency to COMPOSITION templates (<template data-composition-id>) everywhere — ensureHfIds, SDK buildChildren, querySelectorAllDeep. A plain <template> (runtime clone-source) keeps its old fully-excluded behavior: stamping its interior would duplicate one persisted id across every runtime clone, and modeling it would show phantom timeline clips. - Guard the sub-comp persist: only .html files (the wildcard route can serve any project path — stamping an SVG corrupted it on disk), try/catch the read (file-removed race becomes 404, not 500), salt the etag (v2) so pre-fix cached clients don't 304 past the id pin, and thread the stamped content into buildSubCompositionHtml so served ids match the mint even when the disk write is skipped. - Rewrite querySelectorAllDeep as a document-order DOM walk — appending template matches after top-level matches made duplicate-id tiebreaks disagree with the preview's unwrapped DOM (wrong-element edits). - Recurse sourceMutation.querySelectorAllWithTemplates so server-side ops resolve ids at any template depth, matching SDK resolution. - Replace the empty-session silent skip with ONE tagged session_empty event per session — silence would blind the tripwire to exactly the modeling-gap class that exposed the template bug. Attempts stay uncounted (an unmodelable comp can't cut over). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(studio-server): close TOCTOU in sub-comp hf-id persist (CodeQL js/file-system-race) Replace the route-level stat/read/persist sequence with stampFileHfIds: validation (fstat), read, mint, and write-back all go through ONE open file descriptor (O_NOFOLLOW where supported), so the path cannot be swapped between validation and write. Falls back to read-only stamping when the file isn't writable — content-keyed minting means the SDK derives the same ids from the same bytes even without the disk write. Addresses miguel-heygen's blocking review on PR #1981. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(studio-server): linear-time template-attr match (CodeQL js/polynomial-redos) promoteTemplateCompositionId's single-pattern regex backtracked polynomially on crafted input. Two-step match: grab each <template> open tag linearly, then find data-composition-id within that short tag text. Same semantics (first template carrying the attr wins). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e9076324e7
commit
3a717fa719
@@ -9,9 +9,15 @@
|
||||
*/
|
||||
|
||||
import { parseHTML } from "linkedom";
|
||||
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
||||
import { ensureHfIds, isCompositionTemplate } from "@hyperframes/parsers/hf-ids";
|
||||
import { parseGsapScriptAcornForWrite } from "@hyperframes/core/gsap-parser-acorn";
|
||||
import { findRoot, getElementStyles, getOwnText, isNewHostBoundary } from "./engine/model.js";
|
||||
import {
|
||||
findRoot,
|
||||
getElementStyles,
|
||||
getOwnText,
|
||||
isNewHostBoundary,
|
||||
querySelectorAllDeep,
|
||||
} from "./engine/model.js";
|
||||
import type { HyperFramesElement, SdkDocument } from "./types.js";
|
||||
|
||||
// Tags that carry no editable content and must not enter the element tree.
|
||||
@@ -67,7 +73,7 @@ function buildAnimationIdMap(document: Document): Map<string, string[]> {
|
||||
if (!selector) continue;
|
||||
let matches: Element[] = [];
|
||||
try {
|
||||
matches = Array.from(document.querySelectorAll(selector));
|
||||
matches = querySelectorAllDeep(document, selector);
|
||||
} catch {
|
||||
continue; // selector not valid for querySelectorAll — skip
|
||||
}
|
||||
@@ -96,6 +102,35 @@ export function parsedAnimationIds(script: string): Set<string> {
|
||||
return new Set(parseLocatedCached(script).map(({ id }) => id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the element list for a parent's children, treating a COMPOSITION
|
||||
* template (`<template data-composition-id>`) as a TRANSPARENT container: its
|
||||
* inner elements are spliced in at the template's position, the template
|
||||
* itself gets no node. This mirrors the studio preview, which unwraps exactly
|
||||
* that pattern into the served body — so template-based sub-comps expose the
|
||||
* same elements (and hf-ids) here as the timeline reads from the live preview
|
||||
* DOM. A plain <template> (runtime clone-source) stays fully excluded: its
|
||||
* inert interior is not editable and its content is duplicated at runtime.
|
||||
*/
|
||||
function buildChildren(
|
||||
parent: Element,
|
||||
scopePrefix: string,
|
||||
animationIdsByHfId: Map<string, string[]>,
|
||||
): HyperFramesElement[] {
|
||||
const out: HyperFramesElement[] = [];
|
||||
for (const child of Array.from(parent.children)) {
|
||||
if (child.tagName.toLowerCase() === "template") {
|
||||
if (isCompositionTemplate(child)) {
|
||||
out.push(...buildChildren(child, scopePrefix, animationIdsByHfId));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const built = buildElement(child, scopePrefix, animationIdsByHfId);
|
||||
if (built) out.push(built);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function buildElement(
|
||||
el: Element,
|
||||
@@ -143,11 +178,7 @@ function buildElement(
|
||||
start !== null && endAttr !== null ? Math.max(0, parseFloat(endAttr) - start) : null;
|
||||
const trackIndex = trackAttr !== null ? parseInt(trackAttr, 10) : null;
|
||||
|
||||
const children: HyperFramesElement[] = [];
|
||||
for (const child of Array.from(el.children)) {
|
||||
const built = buildElement(child, childPrefix, animationIdsByHfId);
|
||||
if (built) children.push(built);
|
||||
}
|
||||
const children = buildChildren(el, childPrefix, animationIdsByHfId);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -215,15 +246,8 @@ function extractDuration(doc: Document): number | null {
|
||||
*/
|
||||
export function buildRoots(document: Document): HyperFramesElement[] {
|
||||
const body = document.body;
|
||||
const roots: HyperFramesElement[] = [];
|
||||
const animationIdsByHfId = buildAnimationIdMap(document);
|
||||
if (body) {
|
||||
for (const child of Array.from(body.children)) {
|
||||
const built = buildElement(child, "", animationIdsByHfId);
|
||||
if (built) roots.push(built);
|
||||
}
|
||||
}
|
||||
return roots;
|
||||
if (!body) return [];
|
||||
return buildChildren(body, "", buildAnimationIdMap(document));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user