# Dream-on-SX: OCaml's Dream web framework on the SX CEK `[activated — target user confirmed; gated only on ocaml-on-sx]` Carved out of `plans/ocaml-on-sx.md`. The OCaml-on-SX plan was scoped down to **substrate validation + HM + reference oracle** (Phases 1–5 + minimal stdlib slice). Dream is the practical alternative-stack story — the opposite framing — and is now the **chosen framework layer for the rose-ash host**: the decision is to move off Quart and adopt Dream (not Quart) as the ergonomic HTTP front door over the native SX server. `plans/host-on-sx.md` Phase 4 is the concrete consumer that pulls Dream. **Target user — CONFIRMED.** The earlier "needs a concrete target user" condition is met: rose-ash itself is the user — the subsystems (feed/acl/mod/commerce/identity/…) need an ergonomic HTTP front door, and the project owner has chosen Dream over Quart for it. This plan is no longer cold; it is *gated*, not deferred. **Do not start without (the one remaining gate):** 1. OCaml-on-SX Phases 1–5 + Phase 6 minimal stdlib green. (As of writing ocaml-on-sx is at 480/480 and advancing — verify its scoreboard covers Phases 1–5 + the stdlib slice before starting.) Until that gate is green, the native server (host-on-sx Phases 1–3) carries the host; do not block host migration on Dream. ## Why this might be worth doing (when the time comes) Dream is the cleanest middleware-shaped HTTP framework in any language: - `handler = request -> response promise` - `middleware = handler -> handler` - `m1 @@ m2 @@ handler` — left-fold composition It maps onto SX with almost no impedance — `@@` is function composition, `request → response promise` is `(perform (:http-respond ...))`, middleware chain is plain SX function composition. So the integration cost is low *if* the OCaml-on-SX foundation is in place. The user-facing story: rose-ash users who'd never touch s-expressions might write Dream/OCaml apps that integrate with the same federation, auth, and storage primitives. Demo: a Dream app serving sx.rose-ash.com — the framework that describes the runtime it runs on. ## Dream semantic mappings | Dream construct | SX mapping | |----------------|-----------| | `handler = request -> response promise` | `(fn (req) (perform (:http-respond ...)))` | | `middleware = handler -> handler` | `(fn (next) (fn (req) ...))` | | `Dream.router [routes]` | `(ocaml-dream-router routes)` — dispatch on method+path | | `Dream.get "/path" h` | route record `{:method "GET" :path "/path" :handler h}` | | `Dream.scope "/p" [ms] [rs]` | prefix mount with middleware chain | | `Dream.param req "name"` | path param extracted during routing | | `m1 @@ m2 @@ handler` | `(m1 (m2 handler))` — left-fold composition | | `Dream.session_field req "k"` | `(perform (:session-get req "k"))` | | `Dream.set_session_field req "k" v` | `(perform (:session-set req "k" v))` | | `Dream.flash req` | `(perform (:flash-get req))` | | `Dream.form req` | `(perform (:form-parse req))` — returns Ok/Error ADT | | `Dream.websocket handler` | `(perform (:websocket handler))` | | `Dream.run handler` | starts SX HTTP server with handler as root | ## Roadmap The five types: `request`, `response`, `handler = request -> response`, `middleware = handler -> handler`, `route`. Everything else is a function over these. - [x] **Core types** in `lib/dream/types.sx`: request/response records, route record. - [x] **Router** in `lib/dream/router.sx`: - `dream-get path handler`, `dream-post path handler`, etc. for all HTTP methods. - `dream-scope prefix middlewares routes` — prefix mount with middleware chain. - `dream-router routes` — dispatch tree, returns handler; no match → 404. - Path param extraction: `:name` segments, `**` wildcard. - `dream-param req name` — retrieve matched path param. - [x] **Middleware** in `lib/dream/middleware.sx`: - `dream-pipeline middlewares handler` — compose middleware left-to-right. - `dream-no-middleware` — identity. - Logger: `(dream-logger next req)` — logs method, path, status, timing. - Content-type sniffer. - [x] **Sessions** in `lib/dream/session.sx`: - Cookie-backed session middleware. - `dream-session-field req key`, `dream-set-session-field req key val`. - `dream-invalidate-session req`. - [x] **Flash messages** in `lib/dream/flash.sx`: - `dream-flash-middleware` — single-request cookie store. - `dream-add-flash-message req category msg`. - `dream-flash-messages req` — returns list of `(category, msg)`. - [x] **Forms + CSRF** in `lib/dream/form.sx`: - [x] `dream-form req` — returns `(Ok fields)` or `(Err :csrf-token-invalid)`. - [x] `dream-multipart req` — multipart form data (in-memory, not yet streaming). - [x] CSRF middleware: stateless signed tokens, session-scoped. - [x] `dream-csrf-tag req` — returns hidden input fragment for SX templates. - [x] **WebSockets** in `lib/dream/websocket.sx`: - `dream-websocket handler` — upgrades request; handler `(fn (ws) ...)`. - `dream-send ws msg`, `dream-receive ws`, `dream-close ws`. - [x] **Static files:** `dream-static root-path` — serves files, ETags, range requests. - [x] **`dream-run`**: wires root handler into SX's `perform (:http-listen ...)`. - [x] **Demos** in `lib/dream/demos/`: - [x] `hello.ml` → `lib/dream/demos/hello.sx`: "Hello, World!" route. - [x] `counter.ml` → `lib/dream/demos/counter.sx`: in-memory counter with sessions. - [x] `chat.ml` → `lib/dream/demos/chat.sx`: multi-room WebSocket chat. - [x] `todo.ml` → `lib/dream/demos/todo.sx`: CRUD list with forms + CSRF. - [x] Tests in `lib/dream/tests/`: routing dispatch, middleware composition, session round-trip, CSRF accept/reject, flash read-after-write — **258 tests across 10 suites** (well past the 60+ target). Runner: `lib/dream/conformance.sh`. **Roadmap complete (2026-06-07): all boxes ticked, 258/258 green.** Loop continues with extensions + hardening below. - **2026-06-07 — Ext: router HTTP correctness** (router suite 27→36, 267 total). Dispatch now tracks which routes' *paths* matched: path matched + method didn't → `405 Method Not Allowed` with an `Allow` header listing the path's methods (was a blanket 404); genuinely-absent paths stay 404. `HEAD` falls back to the matching `GET` handler with the body blanked but headers kept. `dr/route-params` (path-only match) + `dr/method-accepts?` (ANY / HEAD→GET) + `dream-method-not-allowed`. NOTE: in this worktree every `sx-tree` *edit* tool (`sx_replace_node`, `sx_replace_by_pattern`, `sx_insert_near`) raises a yojson `Expected string, got null` error — only `sx_write_file` works, so edits rewrite the whole file. - **2026-06-07 — Ext: error handling + status phrases** (`lib/dream/error.sx`, 15 tests, 282 total). `dream-status-text` / `dream-status-line` reason-phrase map (string keys); `dream-status-page` renders a status page. `dream-catch` is a `guard`-based middleware that turns a raised error into a 500 (`dream-catch-with on-error` for a custom page receiving `(req e)`); normal responses pass through untouched, composes around a router. (`guard` catches explicit `(error …)` raises; `e` stringifies to the message.) - **2026-06-07 — Ext: CORS** (`lib/dream/cors.sx`, 12 tests, 294 total). `dream-cors` decorates responses with `Access-Control-Allow-Origin` (+ credentials), and short-circuits preflight `OPTIONS` with a 204 carrying Allow-Methods/Headers/Max-Age. `dream-cors-origin` for a specific origin, `dream-cors-with opts` for full control (origin/methods/headers/credentials/max-age). Composes around a router. - **2026-06-07 — Ext: JSON** (`lib/dream/json.sx`, 35 tests, 329 total). Host JSON primitives live in the ocaml-on-sx runtime (not the base env), so Dream ships its own pure-SX `dream-json-encode` (scalars/list/dict, string escaping) + `dream-json-parse` (recursive-descent over chars, objects/arrays/strings/numbers/true/false/null, whitespace-tolerant). `dream-json-value` (encode → application/json response) and `dream-json-body` (parse request body). GOTCHA: `number?` is unreliable in this env — used `(= (type-of v) "number")`; `parse-float` handles decimals. Multi-key dict encode order follows `keys` (non-deterministic) so tests assert via parse round-trip. - **2026-06-07 — Ext: signed session cookies** (`lib/dream/session.sx`, session suite 30→41, 340 total). The default store uses guessable sids (`s1`, `s2`), so `dream-sessions-signed backend secret` signs the cookie value (`sid.signature`) and rejects any cookie whose signature doesn't verify — a forged plaintext `s1` or a wrong-secret cookie yields a fresh session instead of a hijack. `dream-cookie-sign` / `dream-cookie-unsign` (keyed hash; same not-cryptographic caveat — inject a host HMAC in production). Plain `dream-sessions` unchanged for the no-secret case. - **2026-06-07 — Ext: query/header convenience** (`lib/dream/types.sx`, types suite 41→59, 358 total). `dream-queries`, `dream-query-param-or` / `dream-header-or` / `dream-param-or` (defaults), `dream-has-query?` / `-header?` / `-param?`, `dream-content-type-of`, `dream-method-is?`, `dream-accepts?` / `dream-wants-json?` (Accept-header content negotiation). - **2026-06-07 — Ext: api.sx facade + README** (`lib/dream/api.sx`, 9 tests, 367 total). `dream-version`, `dream-defaults` (pure stack: error-catch + content-type; logger is opt-in since it performs IO), `dream-make-app routes`, `dream-make-app-with`, `dream-serve`/`dream-serve-port`. `lib/dream/README.md` documents the full public surface, quickstart, the dependency-injection testing story, and caveats. **All planned extensions complete — 367/367 across 14 suites.** - **2026-06-07 — Ext: auth** (`lib/dream/auth.sx`, 23 tests, 390 total). Pure-SX base64 codec (`dream-base64-encode`/`-decode`, arithmetic via `quotient`/`mod` — no bitwise), verified against RFC vectors (Man/Ma/M padding). `dream-basic-auth realm check` → 401 + `WWW-Authenticate: Basic realm=…`, attaches `:dream-user` on success; `dream-basic-credentials` / `dream-authorization` accessors. `dream-require-bearer check` → attaches `:dream-principal` or 401; `dream-bearer-token` accessor. - **2026-06-07 — Ext: HTML escaping** (`lib/dream/html.sx`, 11 tests, 401 total). `dream-escape` (&/>/"/' entities, ampersand first to avoid double-escape), `dream-attr`, `dream-escape-join`. Fixed a real **XSS hole** in the todo demo, which interpolated user text into `