openvibers/openvibe.design README.md
README.md
| 1 | # OpenVibe.Design |
| 2 | |
| 3 | > Make it yours, find the best AI designer. |
| 4 | |
| 5 | **Live site:** https://openvibe.design · **API docs:** https://openvibe.services · **For AI:** https://openvibe.design/llms.txt |
| 6 | **Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test` |
| 7 | |
| 8 | **Status:** v2, the theme studio and the Design Arena (v0.1.0, see [STATUS.json](STATUS.json)). |
| 9 | **Domain:** `openvibe.design` · **Port:** 5090 · **Service id:** `design` · **Env prefix:** `DESIGN` |
| 10 | **License:** AGPL-3.0-only (the same as every OpenVibe service). |
| 11 | |
| 12 | OpenVibe.Design is the Design room: the theme studio where a person browses OpenVibe's themes, tries them on |
| 13 | previews, edits one with contrast checks, uses it, submits it to the gallery, and rolls back to any earlier |
| 14 | setting, and the Design Arena where one prompt goes to three AI models, two outputs are judged blind at phone and |
| 15 | desktop sizes, and an Elo leaderboard per category ranks the designers with the cost and time of every generation. |
| 16 | Theme selection stays in OpenVibe.Network; this service signs the person in and proxies their calls to the |
| 17 | Network's theme API. |
| 18 | |
| 19 | ## What it is |
| 20 | |
| 21 | - **Identity:** service id `design`, name OpenVibe.Design, port 5090, host 127.0.0.1. It stores no accounts and |
| 22 | no tokens: the person's Network session lives in httpOnly cookies and their theme lives in OpenVibe.Network. The |
| 23 | Arena rounds, entries, judgements and ratings live in OpenVibe.Bench. The legacy `arena_*` tables remain for historical account events and migration. |
| 24 | - **Sign-in:** an OAuth 2 client of OpenVibe.Network (`design`), authorization code with PKCE S256, a state check |
| 25 | and a server-side code exchange. |
| 26 | - **The theme API proxy:** every theme read and write goes to OpenVibe.Network's `/api/themes`, with the person's |
| 27 | access token from the `design_at` cookie as a Bearer. The proxy never logs, renders or echoes the token. |
| 28 | |
| 29 | ## The studio |
| 30 | |
| 31 | - **Gallery:** every theme in the catalogue, filtered by dark or light; picking one paints the previews. |
| 32 | - **Previews:** the theme is tried on three sites at once: a Live channel, a Chat room and a shop's grid. They read |
| 33 | only theme tokens, so they look exactly as the sites will. *See it as* repaints them as people with protanopia, |
| 34 | deuteranopia or tritanopia see them (SVG colour matrices). |
| 35 | - **Editor:** six colours (background, surface, text, muted text, accent, lines); [public/js/studio.js](public/js/studio.js) |
| 36 | derives the full token set from them, the same tokens Network's validator accepts. |
| 37 | - **Checks:** WCAG contrast for text on the background and on cards, muted text, button text and links. Every |
| 38 | required check must pass before *Use this theme* is enabled; *Lines against cards* is advisory. |
| 39 | - **Use, share, roll back:** *Use* is `PUT /api/me/theme` (source `studio`), *Share it in the gallery* submits the |
| 40 | theme for review, and *History* lists the person's earlier looks with a one-tap *Restore*. |
| 41 | |
| 42 | ## Design Studio v1 |
| 43 | |
| 44 | `/create` takes one prompt and shows three model variants on `/d/:id`. The variants use the Arena's OpenVibe.AI route pins and HTML sanitizer. The canvas supports phone, tablet and desktop widths, and changing the token theme repaints all frames. A signed-in design belongs to its Network subject. A guest receives a secret link, kept among the last three drafts in a private cookie. Guests can create two designs per day per IP; signed-in people can create ten. Generation progress is polled, and completed frames appear individually. |
| 45 | |
| 46 | First-party services can request a missing glyph with `POST /api/v1/glyphs/drafts` and a service token for audience `openvibe.design` carrying `design.glyph.draft`. Send `{ "name": "daily-2026-11-09", "sentence": "What this icon should mean" }`. The response names pending draft IDs; repeated requests return the same IDs without drawing again. Reference icons for a draft come from OpenVibe.Exchange (`POST /v1/search/icons`, the Exchange platform API served by OpenVibe.AI) with Design's own Network client-credentials token (`exchange.search.icons`), so no key is needed; `EXCHANGE_SEARCH_KEY` is the owner's override. Without either, or when Exchange fails, Design reads Iconify, applies the same licence filter and sends only names and concepts to the model. Candidates use the cheapest AI preset and wait in the existing `/icons` staff review queue. Only approved drafts appear in `GET /api/v1/glyphs/drafts?status=approved`; a requesting site keeps a bundled glyph until then. |
| 47 | |
| 48 | Pick one frame with **Use this design**: it becomes the working frame, its HTML becomes version 1 and the pick is recorded as one Arena vote through OpenVibe.Bench (a signed-in person votes once; a guest's pick is saved without a vote). Edit it by chat (`POST /api/v1/designs/:id/edit`) or by clicking an element: the iframe reports `body > …:nth-of-type(n)`, and the floating panel changes its text, theme-token colour, padding or alignment. The server validates the path, tag and values, escapes the text, rebuilds only the inline style and saves a new version; the overlay script is injected at render time and is never stored with the model HTML. Versions are append-only in `design_versions`; undo/redo move the pointer (`design_picks.current_seq`), and an edit after undo appends a new version instead of deleting the old ones. Chat edits cost AI money, so each design has a small AI-edit budget (`DESIGN_CHAT_EDITS_PER_DESIGN`, default 5); a failed attempt spends budget but never replaces the last good version. |
| 49 | |
| 50 | The API is under `/api/v1/designs`; `/developers` lists the routes. `migrations/0002_design_studio.sql` stores each design and its JSON variants in one row, and `0003_design_working_versions.sql` adds the pick, its Bench vote fields, the AI-edit counter and the version table, using the service's existing PostgreSQL or development PGlite setup. Production needs the existing `DATABASE_URL` and `DATABASE_DIRECT_URL` roles and working OpenVibe.AI and OpenVibe.Bench service credentials (the Bench scope now includes `bench.entry.create` so Design can submit the generated variants to the pick-vote round). Checks, HTML/zip export, preview publishing and MCP remain part C. |
| 51 | |
| 52 | ## The Design Arena |
| 53 | |
| 54 | - **A round:** a signed-in person writes one prompt in one of six categories. Design creates a public Bench task of |
| 55 | kind `design.page`, or `design.component` for logo, then creates a round. Bench queues and generates three entries |
| 56 | through OpenVibe.AI, including their HTML, provider, model, cost, time and tokens. |
| 57 | - **Budgets:** Bench enforces its per-person and site daily round limits. |
| 58 | - **Safety:** the model's page is text until it is rendered, and then only inside `<iframe sandbox srcdoc>` with no |
| 59 | `allow-scripts` and no `allow-same-origin`. A CSP meta in the srcdoc blocks every network fetch |
| 60 | (`default-src 'none'; style-src 'unsafe-inline'; img-src data:`). Scripts, `on*` handlers and `javascript:` URLs |
| 61 | are stripped before storage, and an output is capped at 200 KB. |
| 62 | - **Voting:** `/arena/vote` serves two successful outputs in random order with the model names hidden. One vote per |
| 63 | person per round; the round's creator cannot vote. A vote reveals both models and updates the category's Elo |
| 64 | ratings (K = 32, a tie is 0.5, `both_bad` is a game for neither). |
| 65 | - **Leaderboard:** Bench Elo boards for both kinds, including confidence intervals and provisional status, and |
| 66 | Bench Cost boards with entries, usable entries, total cost and cost per solved entry. |
| 67 | - **Open dataset:** `GET /api/v1/arena/dataset.jsonl` redirects to Bench's `design.page` JSONL dataset. |
| 68 | - **Bench access:** Design uses a client-credentials token (audience `openvibe.bench`) to create tasks and read |
| 69 | boards. It sends the creator's Network token to create each round so Bench records ownership and enforces the |
| 70 | personal budget and creator vote rule. Blind pairs and votes also use the person's token. |
| 71 | - **Legacy import:** `scripts/arena-to-bench.js` previews a migration by default. `--apply` copies old rounds and |
| 72 | outputs and records votes as anonymous `automatic_grader` judgements. Bench's Elo board does not count these |
| 73 | historical judgements. The importer needs `bench.entry.create` and is subject to Bench's round limits. |
| 74 | |
| 75 | ## Pages |
| 76 | |
| 77 | | Route | What it is | |
| 78 | |---|---| |
| 79 | | `/` | the theme catalogue's front door | |
| 80 | | `/studio` | the theme studio; includes `/js/studio.js` and `/css/design.css` | |
| 81 | | `/patterns` | production patterns with live demos and generated SwiftUI and Compose specs | |
| 82 | | `/patterns/:slug` | one Kit pattern, its rule, dated bug, tokens and native specs | |
| 83 | | `/patterns/mark/studies` | five OpenVibe mark studies copied from the research artifact | |
| 84 | | `/t/:slug` | one public theme (Network 404 → the 404 page) | |
| 85 | | `/arena` | the Arena leaderboard per category and the way to start a round | |
| 86 | | `/arena/new` | the start-a-round form (works without JavaScript) | |
| 87 | | `/arena/vote` | the next blind pair, and the reveal after a vote | |
| 88 | | `/arena/r/:id` | one round: every output at 390 px and 1280 px, with cost and time | |
| 89 | | `/developers` | the API, for people and agents | |
| 90 | | anything else | the 404 page | |
| 91 | |
| 92 | All of them are server-rendered through the OpenVibe Frame (`openvibe-shared/shell`) and readable without |
| 93 | JavaScript. A page rendered for a signed-in person is `private, no-store`; a public page is |
| 94 | `public, max-age=120` with `Vary: Cookie`. |
| 95 | |
| 96 | ## Token files |
| 97 | |
| 98 | `GET /tokens/<theme>.<ext>` gives one theme as the file a tool reads. `<theme>` is a built-in theme or a |
| 99 | community theme from the catalogue (`default` is Vibe); `<ext>` is the format: |
| 100 | |
| 101 | | Route | What it is | |
| 102 | |---|---| |
| 103 | | `/tokens/<theme>.tokens.json` | the W3C Design Tokens document: `color`, `motion.duration`, `motion.easing`, `brand` | |
| 104 | | `/tokens/<theme>.css` | `:root { … }` with every variable | |
| 105 | | `/tokens/<theme>.tailwind.js` | a Tailwind preset (`theme.extend.colors`, `transitionDuration`, `transitionTimingFunction`) | |
| 106 | | `/tokens/<theme>.style-dictionary.json` | the Style Dictionary source | |
| 107 | | `/tokens/<theme>.swift` | a Swift enum `OVTokens` (SwiftUI) | |
| 108 | | `/tokens/<theme>.kt` | a Kotlin object `OVTokens` (Jetpack Compose) | |
| 109 | | `/tokens/<theme>.xml` | an Android `colors.xml` | |
| 110 | |
| 111 | Every file carries the theme's colours, the kit's motion (`--ov-dur-1…4` and the three curves, from |
| 112 | `openvibe-shared/ov-fx`) and the mark's colours. A built-in theme does not change, so its file is |
| 113 | `public, max-age=31536000, immutable`; a community theme may be edited, so its file is cached for ten |
| 114 | minutes. Every file is `Access-Control-Allow-Origin: *`. A theme or a format that is not there is a 404. |
| 115 | |
| 116 | ## API |
| 117 | |
| 118 | ### MCP |
| 119 | |
| 120 | `POST /mcp` is a public, stateless Streamable HTTP MCP endpoint (protocol `2025-06-18`). |
| 121 | It offers `openvibe_design_list_themes`, `openvibe_design_get_theme_tokens`, |
| 122 | `openvibe_design_list_components`, `openvibe_design_get_component`, and |
| 123 | `openvibe_design_validate_kit_document`. The tools read the same Network catalogue, |
| 124 | token compiler and component contract as the JSON API. Discover it through |
| 125 | `/.well-known/agent.json` or `/llms.txt`; no sign-in is needed. |
| 126 | |
| 127 | The theme surface is proxied to `config.networkUrl` (`OV_NETWORK_INTERNAL_URL`, default `http://127.0.0.1:4000`) |
| 128 | under `/api/themes`; the Arena's own data is under `/api/v1/arena`. Errors are RFC 9457 |
| 129 | `application/problem+json`. Arena reads are public; writes need the session and a same-origin signal |
| 130 | (`Sec-Fetch-Site: same-origin`, else an `Origin` equal to the site). A missing signal is refused on a cookie write; |
| 131 | a Bearer write is not cookie-authenticated and is unaffected. |
| 132 | |
| 133 | | Route | Auth | What it does | |
| 134 | |---|---|---| |
| 135 | | `GET /api/themes` | none | the public catalogue; cached in memory 5 minutes | |
| 136 | | `GET /api/themes/:idOrSlug` | none | one public theme | |
| 137 | | `GET /api/me/theme` | cookie | Network `GET /api/themes/me/active` → `{ theme_id, theme, custom_variables }`; 401 when signed out | |
| 138 | | `PUT /api/me/theme` | cookie + same-origin | Network `PUT /api/themes/me` with `{ theme_id, custom_variables, source: "studio" }` | |
| 139 | | `GET /api/me/theme/history` | cookie | Network `GET /api/themes/me/history` | |
| 140 | | `POST /api/me/theme/history/:id/restore` | cookie + same-origin | Network's restore; `id` must be a positive integer, else 400 | |
| 141 | | `POST /api/themes/submit` | cookie + same-origin | Network `POST /api/themes` with `{ name, slug, description, mode, variables, tags }` | |
| 142 | | `POST /api/v1/designs` | none/cookie + same-origin | create three variants `{ prompt, kind, themeId? }`; a guest gets a secret edit key | |
| 143 | | `GET /api/v1/designs/:id` | owner key or cookie | frames, the working frame, the pick and the versions; guest key in `X-Design-Key` or `?key=` | |
| 144 | | `POST /api/v1/designs/:id/pick` | owner key or cookie + same-origin | `{ variantIndex }`; idempotent, one Arena vote | |
| 145 | | `POST /api/v1/designs/:id/edit` | owner key or cookie + same-origin | chat edit `{ instruction }`; one AI run from the picked variant's model, small per-design budget | |
| 146 | | `POST /api/v1/designs/:id/click` | owner key or cookie + same-origin | `{ path, text?, color?, spacing?, align? }`, validated, saves a version | |
| 147 | | `GET /api/v1/designs/:id/versions` | owner key or cookie | the version list with model/provider provenance | |
| 148 | | `POST /api/v1/designs/:id/undo`, `/redo` | owner key or cookie + same-origin | move the working-frame pointer; history stays | |
| 149 | | `POST /api/v1/arena/rounds` | cookie + same-origin | queue a round `{ category, prompt }` (subject to the two daily budgets) | |
| 150 | | `GET /api/v1/arena/rounds/:id` | none | one round; entries are blind until Bench reveals them to this viewer | |
| 151 | | `GET /api/v1/arena/vote[?category=logo]` | cookie | the next blind pair for this person (models hidden) | |
| 152 | | `POST /api/v1/arena/vote` | cookie + same-origin | `{ round_id, left_output, right_output, winner }`; reveals both models and updates Elo | |
| 153 | | `GET /api/v1/arena/leaderboard?category=…&type=elo\|cost` | none | Bench Elo or Cost board for the category's kind | |
| 154 | | `GET /api/v1/arena/dataset.jsonl` | none | 302 to Bench's `design.page` JSONL dataset | |
| 155 | |
| 156 | - A Network **4xx** answer passes through with its status and body; a Network failure (no answer, timeout, 5xx) |
| 157 | is a `502 network.unavailable` problem. |
| 158 | - A cookie-authenticated write (`PUT`, `POST`) without `Sec-Fetch-Site: same-origin` or this site's `Origin` is refused with `403 design.cross_site`; a Bearer write is unaffected. |
| 159 | - Everything under `/api/me/*` carries `Cache-Control: private, no-store`. |
| 160 | |
| 161 | Plumbing: `GET /api/health`, `GET /api/ready` (the database is required; Network, JWKS and the OAuth client are |
| 162 | optional checks), `GET /release.json`, `POST /release-metrics`, `/shared/*` (the pinned Frame), `/css/*` and |
| 163 | `/js/*` (content-hashed `?v=` → immutable), `GET /robots.txt`, `GET /sitemap.xml` (the public themes), |
| 164 | `GET /llms.txt` and `GET /metrics` (loopback only). |
| 165 | |
| 166 | ## Sign-in |
| 167 | |
| 168 | `server/auth/sso.js` is OpenVibe.Help's module with this site's names: |
| 169 | |
| 170 | - `GET /auth/login?next=/studio[&silent=1]` mints a state and a PKCE S256 verifier/challenge, stores them (and the |
| 171 | sanitized `next`) in the httpOnly `design_oauth` flow cookie scoped to `/auth`, and redirects to |
| 172 | `${OV_NETWORK_URL}/oauth/authorize` with `client_id=design` and `redirect_uri=${BASE_URL}/auth/callback`. |
| 173 | - `GET /auth/callback` checks the state in constant time, exchanges the code at the Network's internal |
| 174 | `/oauth/token` with the client secret and the code verifier, and sets `design_at` (24 h) and `design_rt` (30 d): |
| 175 | httpOnly, SameSite=Lax, Secure in production, Path=/. |
| 176 | - `GET /auth/logout` clears them and revokes the refresh token (best effort); `GET /auth/me` answers `{ user }` or |
| 177 | `{ user: null }` for the shared navbar. A FedCM assertion or an app/service token never becomes a session. |
| 178 | - `sanitizeNext` accepts same-site relative paths only. An absolute URL on this site's own origin counts as its |
| 179 | path; another origin, `//evil`, a control character or a backslash goes to `/`. |
| 180 | |
| 181 | The Network signs the access token (RS256); `server/auth/keys.js` fetches and refreshes its JWKS from |
| 182 | `${OV_NETWORK_INTERNAL_URL}/api/.well-known/jwks` through `openvibe-sdk/auth`. |
| 183 | |
| 184 | ## Configuration |
| 185 | |
| 186 | See [.env.example](.env.example). Required in production: `BASE_URL` and `OV_OAUTH_CLIENT_SECRET` (the `design` |
| 187 | OAuth client on the Network). Optional: `OV_NETWORK_URL` (default `https://openvibe.network`), |
| 188 | `OV_NETWORK_INTERNAL_URL` (default `http://127.0.0.1:4000`), `OV_NETWORK_ISSUER`, `OV_OAUTH_REDIRECT_URI`, |
| 189 | `DESIGN_THEME_CACHE_MS`, `DESIGN_UPSTREAM_TIMEOUT_MS`, `DESIGN_RATE_LIMIT_PER_MIN`, `DESIGN_CHAT_EDITS_PER_DESIGN` |
| 190 | (default 5) and `COOKIE_SECURE`. The Bench scope must include `bench.entry.create` in addition to the Arena |
| 191 | capabilities, so Design can fill the pick-vote round with the variants it already generated. |
| 192 | |
| 193 | ## How to run |
| 194 | |
| 195 | ```bash |
| 196 | npm install |
| 197 | npm run dev # http://localhost:5090 |
| 198 | OV_ALLOW_SUITE=1 npm test |
| 199 | ``` |
| 200 | |
| 201 | `npm test` runs every `test/*.test.js` in its own process with in-process OpenVibe.Network and OpenVibe.Bench |
| 202 | stand-ins (`test/helpers/network.js`, `test/helpers/bench.js`). Nothing calls the real internet. `OV_ALLOW_SUITE=1` is the suite opt-in the shared |
| 203 | test runner expects. |
| 204 | |
| 205 | ## Deploy (for the lead) |
| 206 | |
| 207 | - **Deploy:** `sudo ovhost deploy design` on the host (checkout at `/opt/openvibe.design`, unit |
| 208 | `openvibe-design.service` on 127.0.0.1:5090, env `/etc/openvibe/design.env`). |
| 209 | - **nginx:** [deploy/nginx/openvibe.design.conf](deploy/nginx/openvibe.design.conf), installed with |
| 210 | `ov-vhost-install`. It allows GET/POST/PUT on `/api` and GET on `/auth`. |
| 211 | - **Readiness:** `/api/ready` reports `degraded` (HTTP 200) when OpenVibe.Network does not answer; the process |
| 212 | still serves the pages and the crawl artifacts. |
| 213 | |
| 214 | ## Security (threat notes) |
| 215 | |
| 216 | Reporting a vulnerability: [SECURITY.md](SECURITY.md). |
| 217 | |
| 218 | - Session tokens are httpOnly cookies; no script can read them and nothing renders or logs them. The only place |
| 219 | they go is the `Authorization` header of a request to the configured Network origin. |
| 220 | - A theme write must come from this site's own origin (the `Origin` header), so another site cannot make a |
| 221 | signed-in visitor change their theme. |
| 222 | - `next` after sign-in is this site's own path only; control characters and backslashes are refused because |
| 223 | browsers drop or rewrite them. |
| 224 | - Every dynamic value in the placeholder views goes through the shared `esc`; the views are plain and show model |
| 225 | text escaped. |
| 226 | - Secrets live only in the environment file; only variable names appear in code and docs, and no secret or token |
| 227 | is logged. |
| 228 | |
| 229 | --- |
| 230 | |
| 231 | Part of the [OpenVibe network](https://openvibe.network). Built in the open by |
| 232 | [OpenVibers](https://github.com/OpenVibers). |
| 233 | |
| 234 | <!-- versions:start --> |
| 235 | - openvibe-contracts: v0.419.0 |
| 236 | - openvibe-sdk: v0.166.0 |
| 237 | - openvibe-shared: v4.241.0 |
| 238 | - openvibe-publishing: v1.7.0 |
| 239 | <!-- versions:end --> |
| 240 |