[English](stdlib-web.md) | [Português](stdlib-web.pt_BR.md) # stdlib web — Kof's Native Web Stack **Last updated:** September 4, 2026 **Version:** 0.2.6-beta (`kof.http` JVM+JS + retry/circuit; WebSocket/SSE JVM + hardening) **Status:** implemented (Phase 1 of the Spring independence plan) — `kof serve` + `kof.http` JVM+JS + `app.ws`/`app.sse` JVM + limits/counters --- ## 1. Philosophy > A Kof web application does not need Spring. HTTP, routes, JSON, request > context and middleware are part of the Kof ecosystem. No external dependency: the HTTP server is generated inside the JVM runtime of the compiled program itself (`dev.kof.runtime.KofRuntime`). No servlet container, no Spring MVC, no annotations. ## 2. Complete example ```kof record User(String name, Int age) main() { var app = web.app() // Middleware: returns null to continue; String to respond directly app.use { if (header("x-auth") == "secret") { return null } return "{\"error\": \"unauthorized\"}" } app.get("/hello") { return "Hello from Kof" } // Path parameter + query string app.get("/users/:id") { return "user " + param("id") + " q=" + query("name") } app.get("/agent") { return "agent=" + header("user-agent") } app.get("/me") { return method() + " " + path() } // Request body app.post("/echo") { return "got:" + body() } // End-to-end typed JSON app.post("/user") { var user = json.decode(body()) return json.encode(user) } app.listen(8080) } ``` ```bash kof serve app.kf # compiles and runs (the app calls app.listen) kof run app.kf # same — the program starts its own server ``` ## 3. API ### `web.app()` Creates an application. The returned value (`kof.web.App`) is a handle; at runtime it is an internal registry identifier. ### Routes | Call | HTTP method | |---------|-------------| | `app.get(path) { ... }` | GET | | `app.post(path) { ... }` | POST | | `app.put(path) { ... }` | PUT | | `app.delete(path) { ... }` | DELETE | | `app.patch(path) { ... }` | PATCH | | `app.options(path) { ... }` | OPTIONS | The body `{ ... }` is a trailing lambda — the route handler. A handler can also be passed explicitly: `app.get("/x", handler)`. - `path` supports segments with a parameter: `/users/:id` (`:` prefix). - The handler returns `String` (response body, 200) or `null` (404). - The response detects JSON automatically when the body starts with `{` or `[` (`Content-Type: application/json`). ### Middleware `app.use { ... }` registers a middleware executed before routing. Return `null` → continues; return `String` → immediate response (200). ### Server ### Segurança (`app.security()`) — D-SEC C18 (14/09) | Chamada | Descrição | |---------|-----------| | `app.security()` | Middleware composto com defaults seguros (headers de hardening) | | `app.security(opts)` | Idem, com overrides via `Map` | Aplica a **ordem fixa** rate-limit → CORS → headers → cookies/session → csrf → auth → RBAC → rota (D-SEC). Substitui a cadeia manual de `app.use`. Sem argumentos, liga os **headers de hardening** (sempre seguros) e o **CSRF** para métodos que mudam estado: - `Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; base-uri 'self'` - `X-Content-Type-Options: nosniff` - `X-Frame-Options: DENY` - `Referrer-Policy: no-referrer` - `Strict-Transport-Security` — só sob TLS (`listenSecure`) Opts documentados (chaves do `Map`; qualquer outra é ignorada): | Chave | Tipo | Default | Efeito | |-------|------|---------|--------| | `headers` | `Bool` | `true` | Liga/desliga os headers acima | | `cors` / `corsOrigin` | `String` | off | Origem permitida, CSV ou `*`. Origem não listada → 403; preflight `OPTIONS` → 204 | | `rateLimit` | `String` ou `Number` | off | `"limite/janelaSegundos"` (ex.: `"100/60"`) ou só o limite. Por IP remoto; excedeu → 429 + `Retry-After` | | `csrf` | `Bool` | `true` | Double-submit cookie: emite `csrf` (SameSite=Lax) em métodos seguros; exige `X-CSRF-Token` casando com o cookie em POST/PUT/PATCH/DELETE, senão 403. `csrf:false` desliga | | `sessionHeader` | `String` | off | Nome do header de sessão. Fora dos `publicPaths`, **toda** request (GET incluído) exige sessão válida; ausente/inválida → 401 | | `publicPaths` / `permitAll` | `String` CSV | — | Allow-list de matchers públicos (ex.: `"/register,/login"`); todo o resto exige autenticação | | `auth` | `Bool` | `false` | Exige `Authorization: Bearer` JWT válido (secret via `auth.secret`); ausente/inválido → 401 + `WWW-Authenticate` | | `roles` | `String` CSV ou `List` | — | Exige todas as roles (claims `roles`); falta → 403 (implica auth) | **Auth-if-present:** mesmo sem `auth: true`, uma request que **traz** `Authorization` com token inválido nunca passa (401) — evita "token ruim vira anônimo". ```kof main() { auth.secret(secrets.get("JWT_SECRET", "dev")) var app = web.app() var o = mapOf() o.put("cors", "https://app.example") o.put("rateLimit", "100/60") o.put("auth", true) o.put("roles", "admin") app.security(o) app.get("/admin") { return "ok" } app.listen(8080) } ``` **Security by default:** `listen`/`listenSecure` com `KOF_ENV=production` sem `app.security()` avisa em `stderr` (nunca falha silenciosamente). **JVM-only** — Native/JS reportam `WEB006` (gap honesto, mesmo precedente `WEB002`/`WEB005`). ### Servidor | Call | Description | |---------|-----------| | `app.listen(port)` | Starts the server (blocking) on `0.0.0.0` | | `app.listenSecure(port)` | Same, with TLS (JVM; self-signed `keytool` + `SSLServerSocket`) | | `app.port()` | Port actually bound (useful with `listen(0)`) | | `app.close()` | Shuts down the server (graceful shutdown) | `app.listen(0)` binds an ephemeral port; `app.port()` reveals the real port. `app.listenSecure` is available on the JVM (Native/JS `WEB002`). | `app.listen(port)` | Inicia o servidor (bloqueante) em `0.0.0.0` | | `app.listenSecure(port)` | Idem, com TLS self-signed de dev (JVM; `keytool` + `SSLServerSocket`) | | `app.listenSecure(port, certPem, keyPem)` | TLS com **certificado próprio** (PKCS#8 PEM) — produção (JVM) | | `app.port()` | Porta efetivamente vinculada (útil com `listen(0)`) | | `app.close()` | Encerra o servidor (graceful shutdown) | `app.listen(0)` vincula uma porta efêmera; `app.port()` revela a porta real. `app.listenSecure` está disponível no JVM (Native/JS `WEB002`). A variante de 3 args usa o par cert/chave do usuário (`-----BEGIN CERTIFICATE-----` / `-----BEGIN PRIVATE KEY-----`, chave PKCS#8 RSA/EC/DSA); o self-signed de 1 arg continua como conveniência de dev, não de produção (D-SEC). ### Static files (`app.serveDir`) (31/08) | Call | Description | |---------|-----------| | `app.serveDir(prefix, dir)` | Serves the files from `dir` under `prefix` (fallback after dynamic routes) | The handler returns the **binary file** from disk with `Content-Type` by extension (HTML/CSS/JS, images, audio, **video**, fonts, PDF...), `Cache-Control` and protection against path traversal (`..`). It is the alternative to pasting base64/HTML/CSS into a `String` literal in the source — the app handles the FILE. **Range requests**: `serveDir` answers `Range: bytes=...` with `206 Partial Content` + `Content-Range` + `Accept-Ranges: bytes` (and `416` for an invalid range). This is what allows `