openvibers/openvibe.design README.md

README.md

main 18.3 KB · 240 lines · markdown

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
12OpenVibe.Design is the Design room: the theme studio where a person browses OpenVibe's themes, tries them on
13previews, edits one with contrast checks, uses it, submits it to the gallery, and rolls back to any earlier
14setting, and the Design Arena where one prompt goes to three AI models, two outputs are judged blind at phone and
15desktop sizes, and an Elo leaderboard per category ranks the designers with the cost and time of every generation.
16Theme selection stays in OpenVibe.Network; this service signs the person in and proxies their calls to the
17Network'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
46First-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
48Pick 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
50The 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
92All of them are server-rendered through the OpenVibe Frame (`openvibe-shared/shell`) and readable without
93JavaScript. 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
99community 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
111Every 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
114minutes. 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`).
121It 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,
124token 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
127The theme surface is proxied to `config.networkUrl` (`OV_NETWORK_INTERNAL_URL`, default `http://127.0.0.1:4000`)
128under `/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;
131a 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
161Plumbing: `GET /api/health`, `GET /api/ready` (the database is required; Network, JWKS and the OAuth client are
162optional 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
181The 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
186See [.env.example](.env.example). Required in production: `BASE_URL` and `OV_OAUTH_CLIENT_SECRET` (the `design`
187OAuth 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
191capabilities, so Design can fill the pick-vote round with the variants it already generated.
192
193## How to run
194
195```bash
196npm install
197npm run dev # http://localhost:5090
198OV_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
202stand-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
203test 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
216Reporting 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
231Part 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