Post-release review of media-use ↔ figma coupling (spec §13.1):
- figma asset imports now regenerate .media/index.md, the agent-readable
inventory media-use maintains — format locked byte-identical via a
cross-runner parity test against media-use's own index-gen.mjs
- figma asset --description/--entity land in the manifest record, the
index table, and <img alt>; component rasterize auto-describes with
the node name. Named brand marks become visible to media-use's
resolve --entity lookups.
- spec §13.1 records the review verdict (loose coupling correct) and
the follow-up queue (shared media-ledger module, global cache for
figma assets, media-use version-keyed idempotency)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(skills): clear Snyk findings and harden supply-chain surface
Address the security-audit findings on the published skills with no change to
any skill's behaviour.
- media-use: resolve.test.mjs runs resolve.mjs via execFileSync with an argv
array instead of execSync(`node … "${tmp}" …`), removing the command-injection
(CWE-78) sink that drove the Snyk Fail.
- music-to-video: replace dynamic `element.innerHTML = <var>` with a setSvg()
helper (DOMParser image/svg+xml + importNode, text fallback) in the
intro-kinetic-cascade and logo-split-lockup-pulse frame templates, clearing the
DOM-XSS (CWE-79) Snyk Fail. Renders identical SVG.
- pr-to-video: fetch-people-avatars.mjs refuses any avatar URL that is not https
on a GitHub avatar host (SSRF guard) and only writes under the project dir
(path-traversal guard); best-effort, always-exit-0 behaviour is unchanged.
- embedded-captions: pin `uvx --from whisperx==3.8.6` (overridable via
$WHISPERX_VERSION) so transcription no longer resolves "latest" at runtime.
- gsap: add Subresource Integrity (integrity + crossorigin) to the 8 render-time
CDN GSAP <script> tags across embedded-captions, music-to-video,
faceless-explainer, pr-to-video and product-launch-video.
- hyperframes-animation / hyperframes-creative: document package-loader's
defense-in-depth and note that the installLine strings are display-only.
Verified: media-use resolve (12/12), probe injection (1/1) and manifest (19/19)
tests pass; avatar host-allowlist checks pass; all changed JS passes node --check
and oxfmt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): clarify product-launch-video vs website-to-video routing
Sharpen the router's product-vs-site decision in hyperframes/SKILL.md: the
split is now "is the site selling a product?" — yes (SaaS / app / product /
company site) → /product-launch-video (a promo; the default for any commercial
URL, even if the site is only named); no, or the user just wants the site shown
as-is (portfolio / blog / docs / personal / event) → /website-to-video (a tour).
Updates the workflow table, the disambiguation bullet, and both workflows'
Input/Output blurbs to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(skills): satisfy oxfmt in the two music-to-video templates
The CI Format job runs `oxfmt --check .`, which also formats embedded <script> in .html. Reflow the setSvg() blocks added for the DOM-XSS fix to oxfmt's wrapping — no logic change. Regenerate the music-to-video manifest hash to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(skills): sanitize SVG in music-to-video templates (real CWE-79 fix)
Addresses @Magi's review: the previous setSvg() only swapped the sink
(innerHTML → DOMParser + importNode) but did NOT sanitize, so active SVG
content still executed on insertion into the live document. Verified in
headless Chrome that the old shape fired both an svg `onload` handler and an
inline `<script>`.
setSvg() now runs a default-deny cleanSvg() over the parsed tree before it ever
enters the document: only an allow-list of inert drawing elements
(svg/g/path/line/rect/circle/… ) and presentation attributes
(d/fill/stroke/viewBox/…) survives. Every other element (`<script>`, `<image>`,
`<use>`, `<foreignObject>`, `<a>`, `<animate>`, …), every `on*` handler, and
href/xlink:href/style are stripped — on the root node too. Non-SVG or malformed
input still falls back to textContent.
Trusted content (the bundled icon library + the default spark/cloud marks)
renders byte-identically; only hostile markup in vars.icon / leftMark / rightMark
is neutralized.
Browser-verified (headless Chrome, both templates' helper):
old setSvg → fired ["script","onload"]
new setSvg → fired [] · trusted icon still renders · 0 danger nodes · 0 on* attrs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap execSync(<shell-string>) → execFileSync(file, [argv]) in probe.mjs, heygen-search.mjs, and eval.mjs so hostile filenames / queries / manifest metadata can't inject shell. Adds probe.test.mjs regression guard and a CI Test (skills) job so it actually runs. Closes the media-use High/Critical scanner alert.
The `media-use` SKILL.md `description:` was an unquoted YAML scalar containing
a mid-value `: ` (`...the full cascade: project cache...`). YAML 1.2 reads
that as a nested mapping and the parse fails with "Nested mappings are not
allowed in compact mappings". `skills add` aborts the entire install when any
one skill fails to parse, so this single file blocked installing all 19
skills for everyone following the README's `npx skills add heygen-com/hyperframes`.
- Replace the offending `: ` with ` — ` (keeps the plain-scalar style used by
the other 18 skills; the description already uses `—` as a separator).
- Add a frontmatter guard to scripts/lint-skills.ts that flags unquoted
top-level scalars containing `: ` — the exact ambiguity — so this can't
regress. No new dependency.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(media-use): core infrastructure — manifest, cache, adopt, probe
Foundation for media-use — the media resolution layer for HyperFrames.
- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)
* fix(media-use): oxfmt formatting + cap freeze download size
Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(media-use): resolve engine + all providers + brand from frame.md
- resolve.mjs: cheapest-first cascade
- BGM/SFX via heygen --headers, Image/Icon via heygen asset search
- Brand tokens from frame.md / design.md (local, no API)
- SKILL.md: full agent docs + hyperframes.dev/design redirect
- Router skill + workflow skill references
* fix(media-use): oxfmt formatting for resolve + providers
Format brand/heygen-search/providers/sfx providers + resolve.mjs
(CI oxfmt --check gate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(media-use): align providers with the real heygen CLI surface (v0.1.6)
Verified live against the official Go `heygen` CLI v0.1.6 with a valid key:
- Caller attribution: pass `--headers 'X-HeyGen-Client-Source: media-use'`
(the allowlisted flag the CLI added for media-use in v0.1.6). The old
`--x-source media-use` was never a real flag and broke every call.
- Command is `asset search` (the `list` leaf was dropped in v0.1.6), not
`asset search list`.
- `--min-score` is sent server-side: honored by `audio sounds list`, but the
`asset search` backend rejects it and returns no score field, so only the
audio providers pass it (image/icon don't).
- Drop hardcoded `ext` so resolve.mjs derives it from the URL: catalog icons
are .png (not .svg), some BGM is .wav (not .mp3).
Also: surface CLI/auth failures on stderr instead of swallowing them as
'no results', carry icon width/height through, and document the heygen CLI
install + >= v0.1.6 requirement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(studio): redesign Asset tab + fix beat analysis auto-trigger
Asset tab: categorized sections, filter chips, text search, audio
spectrum visualizer, "in use" badge, manifest metadata, panel tokens.
Beat fix: only run analysis when a beats file exists on disk.
* fix(studio): oxfmt formatting for AssetsTab
CI oxfmt --check gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(media-use): resolve tests + eval harness
12 resolve engine tests + eval against 7 real registry blocks.
* fix(media-use): oxfmt formatting for eval + resolve tests
CI oxfmt --check gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(media-use): core infrastructure — manifest, cache, adopt, probe
Foundation for media-use — the media resolution layer for HyperFrames.
- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)
* fix(media-use): oxfmt formatting + cap freeze download size
Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(media-use): resolve engine + all providers + brand from frame.md
- resolve.mjs: cheapest-first cascade
- BGM/SFX via heygen --headers, Image/Icon via heygen asset search
- Brand tokens from frame.md / design.md (local, no API)
- SKILL.md: full agent docs + hyperframes.dev/design redirect
- Router skill + workflow skill references
* fix(media-use): oxfmt formatting for resolve + providers
Format brand/heygen-search/providers/sfx providers + resolve.mjs
(CI oxfmt --check gate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(media-use): align providers with the real heygen CLI surface (v0.1.6)
Verified live against the official Go `heygen` CLI v0.1.6 with a valid key:
- Caller attribution: pass `--headers 'X-HeyGen-Client-Source: media-use'`
(the allowlisted flag the CLI added for media-use in v0.1.6). The old
`--x-source media-use` was never a real flag and broke every call.
- Command is `asset search` (the `list` leaf was dropped in v0.1.6), not
`asset search list`.
- `--min-score` is sent server-side: honored by `audio sounds list`, but the
`asset search` backend rejects it and returns no score field, so only the
audio providers pass it (image/icon don't).
- Drop hardcoded `ext` so resolve.mjs derives it from the URL: catalog icons
are .png (not .svg), some BGM is .wav (not .mp3).
Also: surface CLI/auth failures on stderr instead of swallowing them as
'no results', carry icon width/height through, and document the heygen CLI
install + >= v0.1.6 requirement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(media-use): core infrastructure — manifest, cache, adopt, probe
Foundation for media-use — the media resolution layer for HyperFrames.
- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)
* fix(media-use): oxfmt formatting + cap freeze download size
Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>