# 2. Arquitectura de software del backend Oasis usa un patrón **MVC casero** (sin framework MVC): ``` src/backend/backend.js → el ROUTER monolítico (Koa, ~7250 líneas, ~472 rutas) src/models/*_model.js → la lógica de DATOS (lee/escribe SSB) src/views/*_view.js → el RENDER de HTML (con hyperaxe) ``` Flujo de una funcionalidad: ``` navegador → backend.js (ruta) → xModel.metodo() (SSB) → xView(data) (HTML) → ctx.body ``` --- ## 2.1 Stack de software (dependencias clave) Todas las dependencias están en `src/server/package.json`. Importante: casi todos los `require` usan **ruta relativa explícita** al node_modules del servidor, p.ej. `require("../server/node_modules/@koa/router")`. **Capa web / HTTP** - **`koa`** — servidor HTTP base. Se instancian varias apps Koa en `src/client/middleware.js` (principal + sub-apps para estáticos). - **`@koa/router`** — enrutado. `backend.js:442` instancia `const router = new koaRouter()`. Toda la API es **una única cadena fluida** `router.param(...).get(...).post(...)` (líneas ~1270-7124). - **`koa-static` / `koa-mount`** — sirven assets estáticos (`/assets`, `/maptiles`, `/game-assets`, `/js`...) montados en `middleware.js`. - **`koa-body`** — parseo de `multipart/form-data` y POST. Se aplica por ruta: `.post('/votes/create', koaBody(), ...)`. - `express` y `cors` figuran como dependencias pero **el flujo principal es Koa**; Express no se usa en el router. **Motor de plantillas (vistas)** - **`hyperaxe`** — hyperscript → HTML. **No hay archivos `.html`**; el HTML se construye programáticamente desde JS: `div(...)`, `nav(...)`, `form(...)`... Cada elemento expone `.outerHTML`. - **`ssb-markdown` + `ssb-msgs` + `ssb-ref`** — render de Markdown SSB con resolución de menciones/enlaces internos (`src/views/markdown.js`). - **`highlight.js`, `qrcode`, `moment`, `pretty-ms`** — sintaxis, QR, fechas. - **`dompurify` + `jsdom` + `is-svg`** — saneado anti-XSS (`src/backend/sanitizeHtml.js`). **Streams SSB** - **`pull-stream`** — el paradigma de streams de SSB. Es el mecanismo central de lectura: `pull(source, pull.filter(...), pull.collect(cb))`. - Auxiliares: `pull-paramap` (map asíncrono), `pull-sort`, `pull-abortable` (cancelar streams `live`), `pull-cat`, `pull-many`, `pull-pushable`. **Cliente / servidor SSB** - **`ssb-client`** — cliente RPC muxrpc para conectar al sbot (en `gui.js`). - **`ssb-config`, `ssb-keys`, `ssb-ref`** — config, claves ed25519, validación de identificadores (`@feed`, `%msg`, `&blob`). - **`secret-stack` + `ssb-caps` + `ssb-db`** y los plugins SSB → ver [01-red-ssb.md](01-red-ssb.md). **Otros**: `module-alias`, `lodash`, `minimist`, `open`, `archiver`, `file-type`, `pdfjs-dist`, `openpgp`, `axios`, `node-llama-cpp` + `@xenova/transformers` (módulo IA local). --- ## 2.2 Flujo de una petición **a) Construcción del servidor** — `src/client/middleware.js`: la función exportada `({host, port, middleware, allowHost})` crea la app Koa, monta estáticos, fija cabeceras de seguridad (CSP, `X-Frame-Options`...), valida el host, aplica los middlewares recibidos y finalmente `app.listen({host, port})` (línea 180). En `backend.js:7239`: `const app = http({ host, port, middleware, allowHost: config.allowHost })`. **b) Cadena de middleware** — `backend.js:7126-7238`: incluye (1) bloqueo de no-GET en modo público, (2) `setLanguage(...)` por cookie, (3) gate de indexación (muestra `indexingView` si la base no está sincronizada), (4) refresco de estado compartido cada 60 s, y por último `routes` (las rutas del router). **c) Ruta GET — ejemplo agenda** (`backend.js:1977-1982`): ```js .get('/agenda', async (ctx) => { const filter = qf(ctx); let data = await agendaModel.listAgenda(filter); // MODELO: lee SSB if (Array.isArray(data)) data = await applyListFilters(data, ctx); ctx.body = await agendaView(data, filter); // VISTA: genera HTML }) ``` `agendaModel.listAgenda` abre el cliente (`cooler.open()`), lee el log (`createLogStream`), agrega ítems y devuelve datos. `agendaView` los recibe y construye HTML con hyperaxe, envuelto en `template(...)`. El HTML se asigna a `ctx.body` y Koa lo sirve. **d) Ruta POST — ejemplo votos** (`backend.js:5572`): ```js .post('/votes/create', koaBody(), async ctx => { const b = ctx.request.body; const parsedOptions = b.options ? b.options.split(',')... : defaultOptions; await votesModel.createVote(stripDangerousTags(b.question), b.deadline, parsedOptions, ...); ctx.redirect(safeReturnTo(ctx, '/votes?filter=mine', ['/votes'])); }) ``` El POST **sanea** la entrada (`stripDangerousTags`), llama al modelo que **publica un mensaje SSB**, y **redirige** (patrón Post/Redirect/Get). El GET posterior vuelve a leer del modelo y renderiza. --- ## 2.3 Conexión al servidor SSB: el "cooler" El **cooler** es la abstracción que gestiona la conexión al sbot. Se define en `src/client/gui.js` (importado en `backend.js:441` como `const ssb = require("../client/gui")`) y se instancia en `backend.js:540`: ```js const cooler = ssb({ offline: config.offline, port: config.port, host: config.host, isPublic: config.public }); ``` - Si existe un sbot **en el mismo proceso** (`internalSSB`), `cooler.open()` lo devuelve directamente sin RPC. - Si no, conecta por muxrpc vía socket Unix `noauth` con backoff exponencial. **Patrón de uso en todo el código**: `const ssb = await cooler.open();` y luego se usan los métodos muxrpc: | Método | Para qué | |---|---| | `ssb.createUserStream({id, reverse, limit})` | feed de un usuario | | `ssb.createLogStream({reverse, limit})` | log global (lo más usado) | | `ssb.query.read({query:[{$filter:{...}}]})` | consultas ssb-query | | `ssb.messagesByType({type, private})` | mensajes por tipo | | `ssb.backlinks.read({query})` | referencias/backlinks | | `ssb.get(id, cb)` | un mensaje por id | | `ssb.publish(content, cb)` | **publicar un mensaje** | | `ssb.blobs.want/get/add` | blobs | | `ssb.friends.isFollowing/isBlocking/graph` | grafo social | | `ssb.conn.peers / ssb.status() / ssb.progress()` | estado de red | --- ## 2.4 Patrón de un MODELO Todos los modelos siguen la **factory pattern**: `module.exports = ({ cooler, isPublic, ...deps }) => ({ ...métodos })`. Reciben el `cooler` por inyección desde `backend.js` (líneas 553-602). Muchos cachean el cliente: ```js let ssb; const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; }; ``` **Publicar (escribir)** — `ssb.publish(content, cb)` con un campo `type`. Ejemplo `votes_model.js` (`createVote`): ```js const content = { type: 'votes', question, options, deadline, createdBy: userId, status: 'OPEN', votes: {...}, totalVotes: 0, createdAt: new Date().toISOString() }; return new Promise((res, rej) => ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg))); ``` **Leer** — `pull-stream` + `collect`: ```js pull( ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, results) => err ? reject(err) : resolve(results)) ); ``` Luego se procesa en memoria: se construye un índice, se filtra por `type`, se ordena. **Borrar / editar — modelo de tombstone** (el log es *append-only*: nada se borra físicamente): - **Borrado lógico**: se publica `{ type:'tombstone', target:, deletedAt, author }`. - **Validación** (`src/models/tombstone_validator.js`, `buildValidatedTombstoneSet`): solo acepta el tombstone si **el autor del tombstone coincide con el autor del mensaje objetivo** — evita borrados por terceros. - **Edición**: se publica una versión nueva con `replaces: ` y se lapida la anterior, formando una cadena de versiones; al leer se sigue la cadena hasta la "punta". - Las comprobaciones de autoría (`if (c.createdBy !== userId) throw`) se hacen en el modelo. **Tipos de mensaje SSB** (campo `content.type`): los nativos SSB `post`, `vote` (likes), `contact` (follow/block), `about` (perfil), y los **propios de Oasis**: `votes`, `tombstone`, `task`, `event`, `calendar`, `transfer`, `tribe`, `market`, `report`, `job`, `project`, `gameScore`, etc. > **`main_models.js`** es el modelo núcleo: exporta `about`, `blob`, `friend`, > `meta`, `post`, `vote`, `spreads`, `lifetime`. Su función `transform()` > enriquece cada mensaje con metadatos (autor, avatar, votos, timestamps) antes > de pasarlo a las vistas. --- ## 2.5 Patrón de una VISTA Las vistas exportan funciones que reciben datos del modelo y devuelven HTML. Patrón típico: ```js const { div, h2, p, section, button, form } = require("../server/node_modules/hyperaxe"); const { template, i18n, userLink } = require('./main_views'); exports.agendaView = async (data, filter) => template(i18n.agendaTitle, section( h2(i18n.agendaTitle), /* ... */ )); ``` **Layout común — `main_views.js`**: - `template(titlePrefix, ...elements)` construye el documento completo: `html → head (title, CSS, favicon) → body (header, sidebar de navegación, main-content)`. Se exporta como `exports.template`. - **Navegación**: helper `navLink({href, emoji, text, current})` genera cada `
  • `. `navGroup({id, emoji, title})` agrupa enlaces en secciones colapsables del menú lateral. Cada módulo tiene su `renderXxxLink()` que solo emite el enlace si el módulo está activo en config. - **i18n**: `i18n` es un objeto compartido importado por todas las vistas. `setLanguage(lang)` lo muta in-place (11 idiomas: `en, es, fr, eu, de, it, pt, zh, ar, hi, ru` en `src/client/assets/translations/`). El middleware llama `setLanguage(...)` en cada petición. Las cadenas se referencian como `i18n.agendaTitle`, con fallback `i18n.x || 'Default'`. **Nunca texto literal en las vistas.** - **Temas**: `const theme = currentConfig.themes.current || "Dark-SNH"` → ``. - **Helpers reutilizables** exportados por `main_views`: `userLink`, `renderStateChip`, `renderVisibilityChip`, `renderSpreadButton`, `errorView`, `formatCarbon`, `markdown(input, mentions)`. --- ## 2.6 Configuración **`src/configs/config-manager.js`** — gestor central de `oasis-config.json`: - Si el archivo no existe, escribe un `defaultConfig`. - `getConfig()` lee y parsea el JSON **en cada llamada** (sin caché → cambios en caliente). - `saveConfig(newConfig)` reescribe el archivo. Qué controla **`oasis-config.json`**: - `themes.current` — tema CSS activo. - `ux.current` — modo de interfaz (`"blocks"` o `"ainav"`). - **`modules`** — ~50 flags `Mod: "on"|"off"` que activan/desactivan cada módulo. En backend se comprueba con `checkMod(ctx, mod)` (líneas 170-177): si está `off`, redirige a `/modules`. - `wallet`/`walletPub`, `ai.prompt`, `ssbLogStream.limit` (límite de mensajes leídos del log), `homePage`, `language`, `wish`, `pmVisibility`, `lanBroadcasting`. **`src/configs/shared-state.js`** — estado en memoria (no persistente): contador de inbox, huella de carbono, nº de peers online, valor eco, etc. Lo actualiza el middleware periódico y lo consume `template`. --- ## 2.7 Tabla de módulos (modelo ↔ vista) | Modelo | Vista | Notas | |---|---|---| | `main_models.js` | `main_views.js` | Núcleo: layout, i18n, temas, about/blob/friend | | `activity_model` | `activity_view` | Feed de actividad | | `agenda_model` | `agenda_view` | Agrega tasks/events/transfers/market/... | | `audios_model` | `audio_view` | | | `banking_model` | `banking_views` | ECOin, epochs | | `blockchain_model` | `blockchain_view` | | | `bookmarking_model` | `bookmark_view` | | | `calendars_model` | `calendars_view` | Cifrado | | `chats_model` | `chats_view` | Cifrado | | `cipher_model` | `cipher_view` | | | `courts_model` | `courts_view` | | | `cv_model` | `cv_view` | | | `documents_model` | `document_view` | | | `events_model` | `event_view` | | | `favorites_model` | `favorites_view` | Agrega favoritos | | `feed_model` | `feed_view` | | | `forum_model` | `forum_view` | Cifrado | | **`games_model`** | **`games_view`** | Hall of fame + shell de juegos | | `images_model` | `image_view` | | | `inhabitants_model` | `inhabitants_view` | Perfiles | | `jobs_model` | `jobs_view` | | | `larp_model` | `larp_view` | | | `legacy_model` | `legacy_view` | | | `logs_model` | `logs_view` | | | `maps_model` | `maps_view` | Cifrado | | `market_model` | `market_view` | | | `melody_model` | `melody_view` | | | `opinions_model` | `opinions_view` | | | `pads_model` | `pads_view` | Cifrado | | `parliament_model` | `parliament_view` | | | `pixelia_model` | `pixelia_view` | | | `pm_model` | `pm_view` | Mensajería privada | | `projects_model` | `projects_view` | | | `reports_model` | `report_view` | | | `search_model` | `search_view` | | | `shops_model` | `shops_view` | | | `stats_model` | `stats_view` | | | `tags_model` | `tags_view` | | | `tasks_model` | `task_view` | | | `torrents_model` | `torrents_view` | | | `transfers_model` | `transfer_view` | | | `trending_model` | `trending_view` | | | `tribes_model` + `tribes_content_model` | `tribes_view` | Cifrado | | `videos_model` | `video_view` | | | `votes_model` | `vote_view` | | | `wallet_model` | `wallet_view` | | | (sin modelo) | `AI_view` | Lógica en `src/AI/*` | | `crypto.js`/`tribe_crypto.js` | — | Cifrado simétrico por dominio | --- ## 2.8 Mapa de dependencias entre archivos **`backend.js` → modelos** (líneas 541-602): importa `main_models` y **todos** los `*_model.js` como factories invocadas con `{ cooler, isPublic, ...deps }`. Hay **inyección de dependencias entre modelos**: p.ej. `agenda_model` recibe `{calendarsModel, eventsModel, tasksModel, marketModel, jobsModel, projectsModel}`; `favorites_model` recibe casi todos los modelos de contenido. **`backend.js` → vistas** (líneas ~1162-1216 + `require` inline): importa las funciones de render de cada `*_view.js`. Algunas se cargan diferidas dentro de los handlers (`require('../views/...')`). **Modelos → importan**: `pull-stream`, `moment`, `./tombstone_validator`, `../configs/config-manager`, helpers, y reciben `cooler` por parámetro (no importan `gui` directamente). `tombstone_validator.js` no tiene dependencias (función pura). **Vistas → importan**: hyperaxe, `{template, i18n, userLink, ...}` de `main_views`, `moment`, y a veces `../server/SSB_server` para obtener `config.keys.id` (el feedId propio). **Estado compartido**: `config-manager.js` (lee/escribe `oasis-config.json`) y `shared-state.js` (memoria) son importados por backend, modelos y vistas; son los puntos de coordinación transversal. `nameCache.js` (un Map en memoria) lo alimenta `main_models` y lo consume `main_views`. Siguiente: **[03-modulos-y-juegos.md](03-modulos-y-juegos.md)**.