Commit Graph

2 Commits

Author SHA1 Message Date
Leon Aburime d4af1c5cca refactor(daemon): fold plugin-asset-cache/plugin-preview-bakes into plugins/ (#5195)
Move the two top-level plugin-* leaf modules into the plugins/ folder so
plugin-owned modules live under one directory. Both files are pure leaf
modules (node/undici imports only), so the moves are byte-identical renames;
only the four import specifiers in server.ts and the two colocated tests are
repointed.

No public export surface change; no behavior change.

Co-authored-by: LA <la@LAs-MacBook-Pro.lan>
2026-07-08 09:56:44 +00:00
elihahah666 a93869ee09 feat(plugins): 25 motionsites example plugins + daemon asset cache (#3873)
* feat(plugins): add 25 motionsites example plugins + daemon asset cache

Add 25 first-party example scenario plugins under
plugins/_official/examples/ (packaged from motionsites.ai templates via the
motionsites-to-open-design-plugin skill): each ships open-design.json,
SKILL.md, and a self-contained rendered example.html fidelity seed. All 25
pass PluginManifestSchema.

Their cross-border CDN images/videos (cloudinary, higgs.ai, motionsites,
cloudfront) are mirrored to Cloudflare R2 (plugin-assets.open-design.ai) and
high-fidelity compressed (732MB -> 302MB), so example.html / query / SKILL.md
reference the fast public origin instead of slow cross-border hosts.

Daemon asset cache (plugin-asset-cache.ts + /api/asset-cache): a same-origin
disk-cached proxy for any external preview media that remains. The preview
URL rewriter now routes external media (src/poster attrs, CSS url(), and JS
string constants) through it, so cross-border assets satisfy the sandbox CSP
(img-src 'self') and load from local cache. SSRF-guarded: http(s) only, no
credentials, and every resolved address must be public.

* fix(asset-cache): tie SSRF validation to the actual outbound connection

The previous guard resolved DNS in assertSafePublicUrl and then let fetch()
resolve again independently, so a DNS-rebinding host could pass the validation
lookup with a public address and steer the real connection to a private one
(e.g. 169.254.169.254). Move the authoritative check to a connection-time
undici Agent lookup (createValidatingLookup): the address that is validated is
the exact address the socket connects to, closing the TOCTOU gap.
assertSafePublicUrl is now a cheap up-front reject (scheme / credentials /
localhost / literal private IP) only. Adds createValidatingLookup unit tests
(public pass, rebinding-to-private reject, all:true any-private reject).

* fix(asset-cache): reject IPv4-mapped IPv6 literals in the private-IP guard

Node's URL parser normalizes a bracketed mapped literal like
`http://[::ffff:127.0.0.1]/` to the hex form `::ffff:7f00:1`, which the old
dotted-decimal regex missed — so a literal mapped host slipped past
assertSafePublicUrl (and literal IPs skip the DNS lookup hook), letting
`/api/asset-cache` target loopback/private IPv4 via its mapped representation.

Canonicalize IPv6 now: expandIpv6() folds `::` compression and any embedded
dotted IPv4 into eight 16-bit groups, and isPrivateAddress() detects the full
`::ffff:0:0/96` mapped range (hex or dotted), feeding the embedded IPv4 back
through the v4 private-range checks. Group-based classification also replaces
the prefix string-matching for ::, ::1, fe80::/10, fc00::/7, ff00::/8.
Regression coverage added for ::ffff:7f00:1 / ::ffff:127.0.0.1 and
http://[::ffff:127.0.0.1]/x.png.

* fix(asset-cache): full link-local range + stream-cap the upstream body

Two SSRF/DoS hardening fixes on the cache path:

- Link-local was matched as the single fe80:: prefix, leaking the rest of
  fe80::/10 (fe90::, febf::, …) through as "public". Classify with a mask
  ((groups[0] & 0xffc0) === 0xfe80); ULA/multicast use masks too now.

- maxBytes was not a hard ceiling for responses without a trustworthy
  Content-Length: arrayBuffer() buffered the whole body before the size check,
  a memory-exhaustion path for a caller-supplied proxy. Stream the body and
  abort the moment the accumulated size exceeds maxBytes, before concatenating.

Tests: fe80::1 / fe90::1 / febf::1 rejected; a no-Content-Length 800-byte
stream against a 16-byte cap rejects with 413 after <10 pulls (proves it stops
reading instead of buffering the full body).

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-06-08 07:31:41 +00:00