OASIS_LINUX/docs/devs/architecture/02-arquitectura-software.md
SITO ddd2787b73 Oasis 0.9.5 con Karvan para escritorio
Rama de desarrollo experimental sobre el Oasis de epsylon, sin afiliacion con el
proyecto original. Base: 0.9.5.

Trae el modulo Karvan —salas efimeras en memoria con autodestruccion por TTL, chat
que funciona sin JavaScript y llamadas de audio y video sobre WebRTC— con el enlace
en el menu lateral por el mismo patron que el resto de modulos: renderKarvanLink
calcado de renderPollsLink, entrada en modules_view y en la lista de /modules.

Los estilos del modulo van en su propia hoja, styles/karvan.css, siguiendo el patron
de highlight.css: asi se ve igual con cualquier tema, sin depender del tema movil.

Sin servidores ICE de terceros: no hay STUN de fabrica, porque un STUN aprende la IP
publica y el momento de cada llamada. Solo candidatos host salvo que se configure un
TURN propio en oasis-config.json, con credenciales efimeras por el mecanismo REST de
coturn.

El codigo propio vive separado —views/fork/, backend/fork_routes.js,
models/karvan_model.js y translations/fork/— de modo que sobre los ficheros de
upstream solo hay enganches de una linea y cada version nueva se integra sin
arrastrar nada.

Respecto al arbol de Android: fuera el wrapper y main.js, que es su arranque; el
tema vuelve a Dark-SNH y se restauran los modulos que en movil se recortan por peso.

Verificado arrancando sin OASIS_MOBILE: nueve rutas OK, menu lateral de upstream con
27 grupos y el enlace de Karvan, cero rastro de la interfaz movil (ni hexagonos, ni
topbar, ni barra inferior), y una sala creada y servida.
2026-08-19 00:35:08 +02:00

14 KiB

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.

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 servidorsrc/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 middlewarebackend.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):

.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):

.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:

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:

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):

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)));

Leerpull-stream + collect:

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:<msgId>, 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: <idAnterior> 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:

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 <li><a>. 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"<link href="/assets/themes/${theme}.css">.
  • 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 <nombre>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.