feat(embeds): brand-asset preview — full-palette white-label theming + stateless AI theme suggestion#91
Open
cm2489 wants to merge 7 commits into
Open
feat(embeds): brand-asset preview — full-palette white-label theming + stateless AI theme suggestion#91cm2489 wants to merge 7 commits into
cm2489 wants to merge 7 commits into
Conversation
…api/brand, and configurator Pure, dependency-free, deliberately not server-only: the configurator's live contrast warning must agree exactly with the server's discard decision, so both call this one implementation. adjustInkForContrast is guaranteed to converge for the 4.5:1 AA threshold (better-of-black/white floor is sqrt(21)). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, forced light/dark/auto mode Spec §3.4's planned expansion, delivered as one validated :root style tag (EmbedThemeStyle) instead of per-widget inline vars: fixes the unthemed body band below short content (permanent on action-panel refusal states) and collapses three duplicated themeStyle blocks + the ActionPanelMessage special case. embed.css re-keys onto internal tokens (ONE dark block; no component rule touches prefers-color-scheme anymore — forced mode can't drift), color-mix() derivations behind @supports with static fallbacks. Contrast is rejected at this boundary (pair-or-nothing, AA 4.5:1), per the embed-theme fail-closed doctrine. Visual deltas flagged for review: primary buttons invert ink/surface in default dark mode; avatar initials drop their dark-mode accent variant; border tint now derives from ink. embed.js forwards data-surface/-ink/-mode and mirrors forced mode onto iframe colorScheme; 5,026 bytes (budget 5KB, test-asserted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mode select, the two new font stacks, and a custom surface/ink pair gated client-side by the exact lib/contrast math the server enforces — a failing pair is warned about AND omitted from both the preview URL and the snippet, so the configurator can never emit values the server would discard. Fixes the stale S16 header comment claiming rep-lookup has no theming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l extraction, AI mapping, LRU cache lib/brand-fetch: node:https with a guarded DNS lookup (validation pinned to the exact addresses the socket connects to — rebinding-proof, stdlib-only), manual redirects with per-hop re-guard, byte caps with truncation-as- success, log-nothing doctrine cloned from /api/district. URLs truncate to origin at parse time; the path never exists past normalizeBrandUrl. lib/brand-extract: pure regex harvesting (candidates for a language model, not ground truth — no parser dep), bounded character-class scans only. lib/brandprompt: BRAND_MODEL = claude-sonnet-5 (owner decision, plan 2026-07-16), compact candidates JSON only — raw HTML never reaches the API; output re-validated through the widget-boundary validators, contrast repaired (the one surface that adjusts instead of rejecting). lib/brand-cache: per-instance in-memory LRU — never Upstash, so 'the address is never stored' stays literally true. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second Anthropic-calling endpoint. district doctrine strengthened: POST
+ log-nothing catches + origin truncation at parse + in-memory-only cache.
Spend surfaced: per-IP 5/10min + GLOBAL 250/day breaker ('brand-global'
constant key) — worst-case day ≈ $2 on claude-sonnet-5; noteBrandPreview
counts actual spend only, via after(). CI proves the SSRF guard REJECTS
(private/metadata/localhost/userinfo/port targets -> 400 before any
socket); the happy path is deliberately not e2e — no guard-bypass test seam
will ever be added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… mock-site strip The sales moment: an org enters its site URL, /api/brand suggests a theme, and every control autofills (plain setState — manual edits keep working, re-running overwrites). Adjusted-colors honesty note when the server repaired contrast; four bilingual error strings keyed to the route taxonomy; privacy line states the never-stored contract. The mock strip is the minimal honest version: their surface color + their logo loaded client-side from their own https host (never proxied), no fake chrome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Queue item 2 (2026-07-16 priority list), built from the approved plan (
we-need-planning-mode, decisions locked at plan review: BRAND_MODEL = claude-sonnet-5, full-palette scope, same PR).What this delivers
Part A — the theme surface widens to spec §3.4's full set. New validated knobs on all three widgets:
surface+inkcolor pair (accepted only as a pair meeting WCAG AA 4.5:1 — else discarded, fail-closed), fonts 2→4 system-only stacks (humanist,geometric— never a webfont; that would leak visitor IPs to a font host), and forcedmode=light|dark|auto. Everything else derives in CSS (color-mix()behind@supports, static fallbacks for pre-Safari-16.2). Delivery moved from per-widget inline vars to one validated<style>tag at:root(EmbedThemeStyle+buildThemeCss— contents are exclusively re-verified hex + closed-map literals +color-scheme; unit-tested that hostile input can never surface). That fixes the unthemed body band below short content (permanent on action-panel refusal states) and deletes the three duplicatedthemeStyleblocks; widgets no longer take athemeprop at all.Part B —
POST /api/brand+ "Match your site" on /embeds. An org enters its site URL → SSRF-guarded stateless fetch of the homepage (+ ≤2 same-origin stylesheets) → regex signal extraction → oneclaude-sonnet-5call maps candidates onto the closed knobs → server re-validates through the same widget-boundary validators (contrast repaired here, with an honesty note — the widgets still reject) → configurator autofills, live preview re-keys, minimal mock "on your site" strip (their logo, client-side, never proxied).$ decision (surfaced per operating rules)
Second Anthropic-calling endpoint.
BRAND_MODEL = 'claude-sonnet-5'(owner decision at plan review; one-line constant swap). ~$0.008/preview uncached at intro pricing. Caps: 5/10min per IP + global 250/day breaker (brand-globalconstant key) + per-instance 15-min LRU → worst-case day ≈ $2; realistic traffic pennies.noteBrandPreview()counts actual spend only (<env>:usage:brand:<day>).Privacy & security design
normalizeBrandUrl(the path doesn't exist past that call), log-nothing catches throughout (district doctrine), cache is in-memory only — never Upstash.node:httpswith a guarded DNSlookup— validation is pinned to the exact addresses the socket connects to. Full v4/v6 deny table (incl. mapped-v4, NAT64, metadata range), IP-literal/localhost/single-label/userinfo/port rejection pre-DNS, manual redirects ≤3 with per-hop re-guard, byte caps withAccept-Encoding: identity, honest UA. No guard-bypass test seam exists — CI proves rejection (400s for private/metadata/localhost targets); the happy path is deliberately manual-only.502 generation_failed. Raw HTML never reaches the API (compact candidates JSON only, ~500–900 tokens).Visual deltas to eyeball (sanctioned, flagged)
prefers-color-schemein component rules).color-mix(was static blue-gray rgba) — near-invisible at 15–25% alpha.embed.jsis 5,026 bytes (< 5KB budget, test-asserted;docsLoaderBodyclaim stays true).Tests
61 new/extended: contrast math, validator/resolver/CSS-builder injection matrix, SSRF guard tables (incl.
::ffff:169.254.169.254, NAT64, mixed-answer rebinding shape), extraction fixtures (dark/CJK/tag-soup/truncated), prompt/parse/finalize, LRU, route taxonomy + limiter e2e, theme-surface e2e (incl. action-panel refusal theming, forced modes vs emulated OS preference), loader forwarding, configurator autofill/errors/a11y (EN+ES). Full suite locally: 1,563 passed / 1 flaky-passed-on-retry / 14 skipped; all seven CI gates + typecheck + lint clean.Manual verification checklist (localhost,
ANTHROPIC_API_KEYset)/embeds→ Match your site with 2–3 real org URLs incl. a dark site: autofill, preview re-key, adjusted note; error copy on a bot-blocked site./embed/rep-lookup?mode=darkon light OS (whole iframe incl. band below content); custom dark pair?surface=%230f1a2b&ink=%23f5f7fa— inputs/toggles/buttons/focus/chip readable;font=humanist|geometricvisibly differ./embed/action-panel?token=garbage&mode=dark&surface=…&ink=…— refusal card fully themed./embed/bill-card?slug=hr-5582-119no params, light + dark OS — pixel-familiar except deltas 1–3 above./es/embedscopy; paste a full-knob snippet into a scratch page on another port.Out of scope (explicit): tenant webfonts, logo-in-widget (Pro provisioning per spec §2.3),
TenantRecordtheme fields, Upstash brand cache, CSS@importfollowing.🤖 Generated with Claude Code