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.
This commit is contained in:
SITO 2026-08-19 00:35:08 +02:00
commit ddd2787b73
286 changed files with 157145 additions and 0 deletions

17
docs/AI/info.md Normal file
View file

@ -0,0 +1,17 @@
# Oasis AI General Info
Collective Artificial Intelligence (CAI) model called "42" is based into: llama-2-7b-chat.Q4_K_M.
https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF
This is a static model trained in English on an offline dataset with a size of: 3,8 GiB (4.081.004.224 bytes).
---
The main idea behind this implementation of an GGUF/LLM in the OASIS network is to enable distributed learning generated through the collective action of many individuals, with the goal of redistributing the necessary processing load, as well as the ecological footprint and corporate bias.
Our AI will be trained with content from the OASIS network and its purpose is to take action and obtain answers to individual, but also global, problems.
Future versions of the tuned model will be released as we improve model safety with community feedback.
---

View file

@ -0,0 +1,69 @@
# Connect your Mastodon account
The **Fediverse** module lets you use your Mastodon account from inside Oasis: read your timeline, publish posts (text, images or video), reply, boost and favourite — all in one place.
This short guide shows you how to connect your account.
---
## 1) Create an access token in Mastodon
1. Log in to your Mastodon server (for example `mastodon.social`).
2. Go to **Preferences → Development → New application**.
3. Fill in:
- **Application name**: `Oasis`
- **Application website**: optional, leave blank if you want.
- **Redirect URI**: leave the value that's already there.
4. Under **Scopes**, tick these (and nothing else):
```
read:accounts
read:statuses
write:statuses
write:media
write:favourites
```
5. Click **Submit**. Open the new **Oasis** application and copy **"Your access token"**.
> Keep this token private — it lets you post to your account. You can delete it any time from the same Development page.
---
## 2) Connect it in Oasis
1. In Oasis, open **Settings**.
2. Find the **Fediverse** section (the **Mastodon** box).
3. Enter:
- **Address**: your server, e.g. `mastodon.social`.
- **Access token**: the token you just copied.
4. Click **Connect it**.
That's it. Oasis checks the token and opens your timeline. From now on you'll find **Fediverse** in the main menu.
To stop using it, go back to **Settings → Fediverse** and click **Disconnect**.
---
## 3) Using it
Open **Fediverse → Timelines**:
- **Read** your timeline, newest first. Use **Load more** to see older posts.
- **Post**: write in the box, optionally **Attach media** (images or video), **Preview**, then **Publish**.
- **Reply**: open a post's thread and answer from there.
- **Boost** and **Favourite** any post with its buttons.
---
## Show "via Oasis" on your posts (optional)
If you'd like your posts to show that they were sent from Oasis, go to **Preferences → Privacy** in Mastodon and enable **"Display from which app you sent a post"**. Since your application is named `Oasis`, your posts will then show *"via Oasis"*.
---
## If something doesn't work
- **Invalid or expired token** — create the token again and make sure the scopes above are ticked.
- **Invalid instance URL** — use your server's domain, e.g. `mastodon.social`.
- **Couldn't publish / upload** — your token is missing `write:statuses` or `write:media`; re-create it with the right scopes.

17
docs/MANIFESTO Normal file
View file

@ -0,0 +1,17 @@
"""
Im tired of watching injustice prevail while the echoes of despair roam the halls of a world that is burning out. Indifference is the cloak under which those who delight in the suffering of others hide. Reality, with all its weight, crushes us—but within us, there is a flame that refuses to go out, a hope that persists despite the shadows.
Yes, we are tired. Tired of the systems that diminish us, of the lies that divide us, of the structures that tell us we cannot change. But havent we witnessed the transformative power of human connection? Havent we seen what happens when ideas flow without barriers, when a group of souls comes together in a shared purpose, when digital empathy turns into concrete action?
Today, in the midst of this chaos, within the social network that connects the most willing minds and hearts, we have the power to give life to new worlds. Worlds where justice is non-negotiable, where collaboration replaces destructive competition, where knowledge is not a privilege but a universal right.
Projects like OASIS are not just virtual spaces; they are seeds we plant today to harvest tomorrow: a place where voices rise together to build the future we dream of. A space where each piece of code, each line of thought, each project is born with the intention to heal, to learn, to change. We dont have all the answers, but we are certain that the act of building begins here, in this community of dreamers and doers—creators who understand that the future is forged by hands that reach out to collaborate, not to destroy.
So even if today the shadows are long and impunity still reigns, tomorrow the sun of hope will rise—and it will rise with the strength of an army of ideas and projects born in this Oasis of infinite possibilities. May our voices never go silent, may our hands never stop working, may our eyes never stop gazing toward the horizon—where new worlds await to be created by us. Because in the end, if we dont dream the future, who will?
The revolution is in our hands, and its strength lies in the connection between all of us. Together, I still believe—we can create something truly worthwhile ...
.- by psy (03c8.net)
"""

221
docs/PUB/deploy.md Normal file
View file

@ -0,0 +1,221 @@
# Oasis PUB Deployment Guide
This guide walks you through deploying an **Oasis PUB** on a VPS using the Oasis launcher (`sh oasis.sh server`). A PUB needs a static, publicly-reachable IP address and an open TCP port (default `8008`).
---
## 1) Prepare the server
Install the basics:
```
sudo apt-get update
sudo apt-get install -y git curl build-essential
```
Install Node.js (Oasis is tested on Node 22; older LTS versions also work for server-only mode):
```
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 22
nvm use 22
nvm alias default 22
```
## 2) Clone Oasis
```
cd ~
git clone https://code.03c8.net/krakenslab/oasis oasis
cd oasis
```
## 3) Install dependencies
The `install.sh` script installs Node deps and applies the bundled patches. **You can skip the AI model download** — a PUB does not need it.
```
bash install.sh
```
If the AI model download fails or you skipped it, that's fine. The PUB will run without it.
## 4) Configure the PUB
Two example config files live in `docs/PUB/`:
- [`server-config.json.example`](./server-config.json.example) — the Oasis Sbot config (cap, friends graph, gossip, incoming/outgoing ports). Goes into `src/configs/server-config.json`.
- [`oasis-config.json.example`](./oasis-config.json.example) — the GUI/module config tuned for a PUB (AI/wallet/market/jobs/shops off; LAN broadcasting off). Goes into `src/configs/oasis-config.json` if you ever run the GUI on the same host.
Copy them:
```sh
cp docs/PUB/server-config.json.example src/configs/server-config.json
cp docs/PUB/oasis-config.json.example src/configs/oasis-config.json # only if you run the GUI here
```
The reference `server-config.json`:
```json
{
"logging": { "level": "notice" },
"caps": { "shs": "H5EC+V5BU9s0lWxCkt4z8a095Sj8a6TgiLKPYi1JD7s=" },
"pub": true,
"local": false,
"friends": { "dunbar": 300, "hops": 3 },
"gossip": {
"connections": 50,
"local": false,
"friends": true,
"seed": true,
"global": true
},
"replicationScheduler": {
"autostart": true,
"partialReplication": null
},
"autofollow": {
"enabled": true,
"feeds": [
"@0qSCyK3xyL71X4qKkmf84Cb2riP6OeUqxCvbP2Z6HWs=.ed25519"
]
},
"connections": {
"seeds": [],
"incoming": {
"net": [
{
"scope": ["device", "local", "public"],
"transform": "shs",
"port": 8008,
"external": "pub.example.com"
}
],
"unix": [
{
"scope": ["device", "local", "private"],
"transform": "noauth"
}
]
},
"outgoing": {
"net": [{ "transform": "shs" }],
"tunnel": [],
"onion": [],
"ws": []
}
}
}
```
**Three values to know:**
- **`connections.incoming.net[].external`** — the externally-reachable hostname of this PUB (e.g. `pub.example.com`). This is what gets embedded into invite codes generated by `sh oasis.sh invite` and into pub announce messages for the `public` scope. If it's missing, multiserver falls back to the first non-loopback IPv4 address it finds, which means invites will hand out the raw VPS IP instead of your domain. Set it on the public listener before you generate any invites. Note: this is a per-listener field — the top-level `host` does **not** affect invite codes.
- **`caps.shs`** — the network-level secret handshake cap. The Oasis network uses `H5EC+V5BU9s0lWxCkt4z8a095Sj8a6TgiLKPYi1JD7s=` (set in the example above). All PUBs and clients on this network must hold the **same** value — nodes with a different cap silently can't connect. If you're bootstrapping a separate isolated network instead, generate your own with:
```sh
node scripts/generate_shs.js
```
- **`autofollow.feeds`** — the upstream PUB(s) this node will automatically follow on first boot. The example seeds from `solarnethub.com`'s PUB (`@0qSCyK3xyL71X4qKkmf84Cb2riP6OeUqxCvbP2Z6HWs=.ed25519`), the default seed of the Oasis network. Once your PUB connects to it, gossip propagates the rest of the network's pub list. Replace this id only if you're bootstrapping from a different network root.
Everything else is the standard PUB shape: `pub: true`, no LAN discovery, dunbar 300, three friend hops, replication scheduler running on autostart, port `8008` open for SHS on every scope (device/local/public), and a `noauth` unix socket for the CLI.
## 5) Launch the PUB (server-only)
In server-only mode Oasis runs **only the Oasis Sbot**, not the web GUI or AI service. `aiMod` is forced off automatically when launched this way.
The repo ships a ready-to-use systemd unit at `docs/PUB/oasis-pub.service`. Copy it, edit the `YOUR_USER` placeholder, then enable it:
```
sudo cp ~/oasis/docs/PUB/oasis-pub.service /etc/systemd/system/oasis-pub.service
sudo nano /etc/systemd/system/oasis-pub.service # replace YOUR_USER
sudo systemctl daemon-reload
sudo systemctl enable --now oasis-pub
sudo journalctl -u oasis-pub -f
```
Data is written to `~/.ssb/`.
## 6) PUB admin: `sh oasis.sh <command>`
All admin actions go through the launcher itself, which talks to the running Oasis Sbot via the local unix socket (`~/.ssb/socket`). No external CLI install is needed — just keep the systemd unit `oasis-pub` active.
```
sh oasis.sh whoami # print this PUB id
sh oasis.sh invite [N] # create invite code (N = uses, default 1)
sh oasis.sh name "<text>" # set PUB display name
sh oasis.sh announce <host> [port] # publish pub address (default port 8008)
sh oasis.sh follow <feedId> # follow another PUB / feed
sh oasis.sh status # peer / replication overview
sh oasis.sh gossip # known gossip peers
```
## 7) Get your PUB ID
```
sh oasis.sh whoami
```
Example response:
```
{ "id": "@mGrevRCSX4E5dLgmflWBc50Qkn/1RXUAtDaGHOJ8xB4=.ed25519" }
```
## 8) Set the PUB profile name
```
sh oasis.sh name "My PUB Name"
```
## 9) Create invite codes
```
sh oasis.sh invite # uses=1 (default)
sh oasis.sh invite 500 # uses=500 (open PUB)
```
The output is a single-use (or N-use) invite code you can hand out. Clients redeem it in their `/invites` page.
## 10) Announce the PUB
So peers can discover the PUB by hostname:
```
sh oasis.sh announce mypub.example.com
sh oasis.sh announce mypub.example.com 8008
```
## 11) Follow another PUB (federation)
Federate so both replicate each other:
```
sh oasis.sh follow "@mGrevRCSX4E5dLgmflWBc50Qkn/1RXUAtDaGHOJ8xB4=.ed25519"
```
## 12) Health checks
```
sh oasis.sh status # peer / replication overview
sh oasis.sh gossip # known gossip peers with state
ls -la ~/.ssb/ # confirm flume/, blobs/, gossip.json, conn.json exist
sudo journalctl -u oasis-pub -f # tail service logs
```
## 13) Disabling the AI module (only relevant if also running the GUI)
`sh oasis.sh server` does **not** load the GUI or AI service, so `oasis-config.json` is ignored in server-only mode. Only `server-config.json` matters.
If you also run the GUI on the same VPS (`sh oasis.sh` without `server`), set `aiMod` to `off` in `src/configs/oasis-config.json` to skip the AI model:
```
sed -i 's/"aiMod": *"on"/"aiMod": "off"/' src/configs/oasis-config.json
```
## 14) Joining the Oasis network
The default seed PUB at `solarnethub.com` is included in `autofollow.feeds` above. As soon as your PUB connects to it (or to any peer that knows about it), gossip propagates the rest of the network's pub list.

View file

@ -0,0 +1,78 @@
{
"themes": {
"current": "Dark-SNH"
},
"ux": {
"current": "blocks"
},
"modules": {
"popularMod": "on",
"topicsMod": "on",
"summariesMod": "on",
"latestMod": "on",
"threadsMod": "on",
"multiverseMod": "on",
"invitesMod": "on",
"walletMod": "off",
"legacyMod": "off",
"cipherMod": "off",
"bookmarksMod": "on",
"videosMod": "on",
"docsMod": "on",
"audiosMod": "on",
"tagsMod": "on",
"imagesMod": "on",
"trendingMod": "on",
"eventsMod": "on",
"tasksMod": "on",
"marketMod": "off",
"votesMod": "on",
"tribesMod": "on",
"reportsMod": "on",
"opinionsMod": "on",
"padsMod": "on",
"calendarsMod": "on",
"transfersMod": "off",
"feedMod": "on",
"pixeliaMod": "on",
"melodyMod": "on",
"agendaMod": "on",
"aiMod": "off",
"aiNavMod": "off",
"forumMod": "on",
"gamesMod": "on",
"jobsMod": "off",
"shopsMod": "off",
"projectsMod": "off",
"bankingMod": "off",
"parliamentMod": "on",
"courtsMod": "on",
"favoritesMod": "on",
"logsMod": "on",
"mapsMod": "on",
"chatsMod": "on",
"torrentsMod": "on",
"graphosMod": "on",
"larpMod": "on"
},
"wallet": {
"url": "http://localhost:7474",
"user": "",
"pass": "",
"fee": "5"
},
"walletPub": {
"pubId": ""
},
"ai": {
"prompt": "Provide an informative and precise response."
},
"ssbLogStream": {
"limit": 2000
},
"homePage": "activity",
"language": "en",
"wish": "whole",
"pmVisibility": "whole",
"lanBroadcasting": false
}

View file

@ -0,0 +1,26 @@
[Unit]
Description=Oasis PUB
Documentation=https://wiki.solarnethub.com/socialnet/snh-pub
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/home/YOUR_USER/oasis
ExecStart=/bin/sh /home/YOUR_USER/oasis/oasis.sh server
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=oasis-pub
NoNewPrivileges=yes
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/home/YOUR_USER/.ssb /home/YOUR_USER/oasis
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,65 @@
{
"logging": {
"level": "notice"
},
"caps": {
"shs": "H5EC+V5BU9s0lWxCkt4z8a095Sj8a6TgiLKPYi1JD7s="
},
"pub": true,
"local": false,
"friends": {
"dunbar": 300,
"hops": 3
},
"gossip": {
"connections": 50,
"local": false,
"friends": true,
"seed": true,
"global": true
},
"replicationScheduler": {
"autostart": true,
"partialReplication": null
},
"autofollow": {
"enabled": true,
"feeds": [
"@0qSCyK3xyL71X4qKkmf84Cb2riP6OeUqxCvbP2Z6HWs=.ed25519"
]
},
"connections": {
"seeds": [
],
"incoming": {
"net": [
{
"scope": ["device", "local", "public"],
"transform": "shs",
"port": 8008,
"external": "pub.example.com"
}
],
"unix": [
{
"scope": [
"device",
"local",
"private"
],
"transform": "noauth"
}
]
},
"outgoing": {
"net": [
{
"transform": "shs"
}
],
"tunnel": [],
"onion": [],
"ws": []
}
}
}

View file

@ -0,0 +1,62 @@
# Guía de desarrollo de Oasis
Esta carpeta documenta cómo está construido Oasis por dentro, para que puedas
entenderlo y extenderlo (por ejemplo, creando un **módulo de juegos** nuevo).
Está pensada para leerse en orden, pero cada archivo es autónomo.
## Índice
1. **[01-red-ssb.md](01-red-ssb.md)** — Cómo funciona Oasis **a nivel de red**:
Secure Scuttlebutt (SSB), secret-stack, multiserver, muxrpc, EBT, blobs,
federación P2P, puertos y cifrado.
2. **[02-arquitectura-software.md](02-arquitectura-software.md)** — La
**arquitectura de software** del backend: el patrón MVC casero
(models / views / backend.js), el stack de dependencias, el "cooler", el
flujo de una petición y cómo se interrelacionan los archivos.
3. **[03-modulos-y-juegos.md](03-modulos-y-juegos.md)** — Cómo está construido
el **módulo de juegos** actual y la anatomía de un módulo en general.
4. **[04-tutorial-modulo-desde-cero.md](04-tutorial-modulo-desde-cero.md)** —
**Tutorial paso a paso** para crear un módulo nuevo desde cero (caso práctico:
un módulo de juegos propio), con todos los puntos de integración.
5. **[05-mapa-funciones.txt](05-mapa-funciones.txt)** — **Gran mapa ASCII** de
cómo se relacionan las funciones lógicas entre archivos, la persistencia
(SSB) y la media (blobs).
## Conceptos en 30 segundos
Oasis es un **cliente web de Secure Scuttlebutt (SSB)**. SSB es un protocolo
social **P2P, distribuido y federado**: no hay servidor central; cada nodo tiene
una copia local de un *log append-only* firmado criptográficamente, y los nodos
se replican entre sí por *gossip*.
Oasis se compone de **dos procesos**:
```
┌─────────────────────────┐ ┌──────────────────────────┐
│ SBOT (nodo de red SSB) │ <─────> │ FRONTEND WEB (Koa/HTML) │
│ src/server/SSB_server │ unix │ src/backend/backend.js │
│ habla con otros peers │ socket │ habla con tu navegador │
└─────────────────────────┘ └──────────────────────────┘
puerto 8008 (SSB) puerto 3000 (HTTP)
```
- El **sbot** (`src/server/SSB_server.js`) es el nodo SSB: se conecta a otros
peers, replica feeds, sirve y descarga blobs. → [01-red-ssb.md](01-red-ssb.md)
- El **frontend** (`src/backend/backend.js`) es un servidor HTTP en Koa que
renderiza HTML puro (sin JS de navegador) y se conecta al sbot como **cliente
muxrpc** por un socket Unix local. → [02-arquitectura-software.md](02-arquitectura-software.md)
El patrón para **cada funcionalidad** ("módulo") es siempre el mismo:
```
navegador → backend.js (ruta) → *_model.js (lee/escribe SSB) → *_view.js (HTML) → navegador
```
> Las rutas, líneas y nombres citados en estos documentos corresponden a la
> versión 0.8.1 del código. Si editas el código, vuelve a comprobar los números
> de línea con `grep`.

View file

@ -0,0 +1,243 @@
# 1. Oasis a nivel de red (Secure Scuttlebutt)
Oasis es un cliente/servidor de **Secure Scuttlebutt (SSB)**. Toda la "red" de
Oasis se construye sobre la pila clásica del ecosistema SSB:
`secret-stack` + `ssb-db` + un montón de plugins.
Hay una separación clara en **dos procesos**:
- **Backend SSB (sbot)**`src/server/SSB_server.js`: el nodo de red propiamente
dicho. Es quien habla con otros peers.
- **Frontend web (GUI Koa)**`src/backend/backend.js`: un servidor HTTP que
actúa como **cliente muxrpc** del sbot, conectándose por un socket Unix local.
```
RED SSB (otros peers Oasis, misma SHS cap)
net:IP:8008~shs:<pubkey> · ssb-lan (UDP) · ssb-onion (Tor)
┌─────────────────────────────────────────────┐
│ SBOT (src/server/SSB_server.js) │
│ peer <-> MULTISERVER <-> SECRET-STACK (SHS) │
<-> MUXRPC <-> PLUGINS │
└─────────────────────────┬───────────────────┘
│ unix socket ~noauth (muxrpc local, sin SHS)
┌─────────────────────────────────────────────┐
│ FRONTEND (src/backend/backend.js, Koa) │
│ cooler = src/client/gui.js → ssb-client │
│ app.listen({host, port:3000}) │
└─────────────────────────┬───────────────────┘
Navegador http://localhost:3000
```
---
## 1.1 Arranque de red
### Scripts de arranque
- **`install.sh`**: instala Node 22 y hace `npm install` en `src/server`
(línea 23). Opcionalmente descarga modelos de IA. No toca la red.
- **`oasis.sh`** es el lanzador. Modos relevantes:
- `server` / `pub` (líneas 61-69): ejecuta `node SSB_server.js start` — **solo
el sbot headless**, sin interfaz web.
- `gui` o por defecto (líneas 73-81): ejecuta `node backend.js` — el frontend.
- comandos pub `whoami|invite|name|announce|follow|status|gossip`
(líneas 70-72) → delegan en `scripts/oasis-pub.js`.
- **`src/server/package.json`**, script `start`:
`npm run start:ssb && sleep 10 && npm run start:backend`.
Arranca el sbot en segundo plano, **espera 10 s** (para que el socket Unix
exista) y luego arranca el frontend.
### Qué hace `SSB_server.js` paso a paso
(`src/server/SSB_server.js`)
1. **Líneas 7-11**: carga `secret-stack`, `ssb-caps` (la capability SHS),
`ssb-db`, la config (`./ssb_config`) y utilidades de metadatos.
2. **Líneas 13-52** (IIFE): *monkey-patch* de `console.error`/`console.warn`
para silenciar ruido de red — rechazos de handshake por SHS cap incorrecta,
excepciones EBT, etc. Un rechazo se convierte en un log limpio:
`[ts] REJECTED <ip:port> (wrong SHS cap)`.
3. **Líneas 56-81**: construye el servidor con
`SecretStack({ caps }).use(plugin).use(plugin)...` encadenando todos los
plugins (ver tabla abajo).
4. **Líneas 83-86**: si **no** es pub, carga `ssb-lan` (descubrimiento LAN por
UDP) y el router LAN local `./lanRouter`.
5. **Líneas 88-98**: lógica de `autofollow`; carga `ssb-autofollow` si hay feeds.
6. **Líneas 123-196** (`argv[0] === 'start'`): instancia el servidor, escribe
`manifest.json` (el catálogo de métodos muxrpc remotos), monta una barra de
progreso de replicación, imprime metadatos y **escucha eventos del hub de
conexiones** (`server.conn.hub().listen()`) para loguear `CONNECTED` /
`DISCONNECTED`.
7. **Líneas 198-202**: exporta `{ config, server, open() }` — esto permite a la
GUI reusar el servidor **in-process** si corre en el mismo proceso.
### Plugins secret-stack cargados (líneas 56-98)
| Plugin | Función de red |
|---|---|
| `ssb-db` | base de datos *append-only* de feeds |
| `ssb-master` | identidad maestra / auth local |
| `ssb-gossip` | gossip clásico (legacy) de peers |
| `ssb-ebt` | **Epidemic Broadcast Trees** — replicación gossip eficiente |
| `ssb-friends` | grafo social: hops, follows/blocks |
| `ssb-blobs` | transferencia de ficheros binarios |
| `ssb-meme` | blobs de imágenes/memes |
| `ssb-conn` | gestor de conexiones moderno (hub, db, staging) |
| `ssb-box` | cifrado de mensajes privados (box1) |
| `ssb-search`, `ssb-private`, `ssb-friend-pub` | búsqueda, PM, pub de amigos |
| `ssb-invite` / `ssb-invite-client` | invites (servidor si es pub, cliente si no) |
| `ssb-replication-scheduler`, `ssb-partial-replication` | planificación de replicación |
| `ssb-onion` | transporte Tor |
| `ssb-unix-socket` | **socket Unix local** para clientes (la GUI) |
| `ssb-no-auth` | conexiones locales sin handshake SHS |
| `ssb-about`, `ssb-backlinks`, `ssb-links`, `ssb-tangle`, `ssb-query` | índices de vistas |
| `ssb-lan` + `lanRouter` | descubrimiento LAN (solo si no es pub) |
> Nota: la replicación NO usa `ssb-replicate` directamente; la orquestan
> `ssb-ebt` + `ssb-replication-scheduler` + `ssb-friends`.
---
## 1.2 El protocolo SSB en términos del proyecto
- **secret-stack + handshake SHS (capability).** `SecretStack({ caps })` crea el
nodo. La `caps.shs` está **fijada en `src/configs/server-config.json`**
(líneas 5-7): `"H5EC+V5BU9s0lWxCkt4z8a095Sj8a6TgiLKPYi1JD7s="`. Es la **clave
de capacidad de red** (Secret Handshake): dos peers solo completan el
handshake si comparten *exactamente* esta cap. Actúa como "contraseña de red":
**Oasis forma su propia red SSB privada**, distinta de la red SSB pública.
- **multiserver (transportes).** Capa de transporte abstracta. La config
`connections` (`server-config.json`, líneas 29-60) define:
- **incoming.net**: `transform: "shs"`, `port: 8008` → TCP cifrado con SHS.
- **incoming.unix**: `transform: "noauth"` → socket Unix local sin handshake,
para que el frontend se conecte como cliente privilegiado.
- **outgoing.net**: `transform: "shs"`.
- Una dirección multiserver típica: `net:IP:8008~shs:<pubkey-base64>`.
- **muxrpc (RPC sobre el stream).** Una vez establecido el stream SHS, `muxrpc`
multiplexa llamadas RPC asíncronas y *pull-streams* sobre la misma conexión.
El `manifest.json` describe qué métodos remotos existen (`sync`/`async`/
`source`). Es como el frontend invoca `ssb.publish(...)`, `ssb.blobs.get(...)`,
etc.
- **EBT (`epidemic-broadcast-trees` + `ssb-ebt`).** El algoritmo de replicación
gossip. Cada peer anuncia el `seq` (reloj vectorial) de los feeds que tiene;
el otro envía solo los mensajes que faltan, formando árboles de difusión
epidémica que evitan duplicados.
- **Identidad de los peers.** Cada peer es un **feed id ed25519**:
`@<base64>.ed25519`. En las direcciones multiserver, el sufijo
`~shs:<pubkey>` identifica criptográficamente al peer durante el handshake.
---
## 1.3 Federación / P2P
- **Descubrimiento LAN** (`src/server/lanRouter.js`): plugin local custom.
`startRouter` (líneas 55-64) arranca `ssb.lan.start()` (broadcast UDP) y
consume `ssb.lan.discoveredPeers()`. Cada peer descubierto pasa por
`handleDiscovery``stagePeer` (líneas 15-45): lo *stagea* en
`ssb.conn.stage(...)` (vía moderna) con fallback a `ssb.gossip.add(...)`
(legacy); si `eagerReplicate`, fuerza `ebt.request`. Respeta el flag
`lanBroadcasting` de `oasis-config.json`.
- **Pubs / invites.** En modo pub se carga `ssb-invite` (servidor); en cliente,
`ssb-invite-client`. Los comandos `oasis.sh invite|announce|follow`
(→ `scripts/oasis-pub.js`) permiten a un pub publicar su dirección y generar
códigos de invitación. Un invite contiene `host:port:pubkey~seed`; al
canjearlo, el peer se conecta y el pub le sigue de vuelta (`ssb-friend-pub`).
- **Replicación por hops.** `ssb-friends` define el grafo social.
`server-config.json` fija `friends.hops: 2` → el nodo replica los feeds de la
gente que sigues y de la gente que ellos siguen (amigos de amigos). El
`ssb-replication-scheduler` decide qué feeds pedir por EBT según ese grafo.
- **"Distributed not decentralized".** No hay servidor central ni DHT global.
Cada nodo Oasis tiene su **propia copia local** de la base de datos
(logs *append-only* firmados con ed25519) y solo replica los feeds dentro de
su radio social (hops) más los descubiertos por LAN. La red se federa por
gossip entre peers que comparten la misma SHS cap. Es **distribuida** (datos
replicados en muchos nodos autónomos, funciona offline) pero **no
descentralizada** como una blockchain (no hay consenso global ni vista única;
cada quien ve solo su subgrafo). El flag `wish` de `oasis-config.json`
(`whole` / `mutuals` / `only-lan`) controla el alcance de replicación deseado.
---
## 1.4 Puertos y endpoints
| Qué | Dónde | Puerto / dirección |
|---|---|---|
| muxrpc SSB (sbot) — otros peers se conectan aquí | `server-config.json` incoming.net | **8008/TCP** transform `shs` |
| Socket Unix local — para el frontend | incoming.unix | `unix:<ssb.path>/socket~noauth:<pubkey>` |
| Frontend web (Koa) — tu navegador | `oasis_client.js` / `middleware.js:180` | **3000** host `localhost` (configurable con `--host`/`--port`) |
**Cómo se conecta el frontend al sbot** (el "cooler", `src/client/gui.js`):
1. Intenta `require('../server/SSB_server')` y reusar el `server` **in-process**
si existe (sin socket, uso directo del objeto).
2. Si no, construye `remote = unix:<path>/socket~noauth:<pubkey>` y se conecta
con **`ssb-client`** (muxrpc por el socket Unix `noauth`).
3. `attemptConnectionWithBackoff`: reintentos con backoff exponencial (de ahí el
`sleep 10` del arranque).
4. Expone `cooler.open()` / `cooler.close()`. **Todos los modelos** del backend
reciben este `cooler` y hacen `await cooler.open()` para obtener el cliente
muxrpc.
> Para exponer la GUI en un VPS se usa `--host=0.0.0.0`.
---
## 1.5 Blobs (ficheros / imágenes)
- Plugin `ssb-blobs` (+ `ssb-meme`). Tamaño máximo en `ssb_config.js`: 50 MB.
- Un blob se referencia por hash: `&<sha256-base64>.sha256`. **Los mensajes del
log solo llevan el hash**, no el binario.
- Flujo en `src/backend/blobHandler.js`:
- **Subida**: `ssbClient.blobs.add(...)` recibe el contenido por pull-stream y
devuelve el ref `&...sha256`.
- **Descarga por red**: `blobs.has(id)` comprueba si ya está local; si no,
`blobs.want(id)` **lo pide a los peers conectados** (gossip de "want/has"
sobre muxrpc), y `blobs.get(id)` lo lee como stream.
Los blobs viajan sobre la misma conexión SHS/muxrpc, fuera del log append-only.
---
## 1.6 Caps / cifrado
- **`ssb-caps`** define las capabilities de protocolo (shs, sign, invite). La
**shs cap real** está fijada en `server-config.json` → red privada de Oasis.
- **Handshake SHS**: autenticación mutua por ed25519 + confidencialidad del
stream de transporte.
- **`ssb-box`**: cifrado de **mensajes privados box1** (hasta ~7 destinatarios,
cifrado por clave pública de cada feed). `ssb-private` expone publish/read.
- **Grupos privados ("tribes").** No se usa `box2`/`ssb-tribes`. En su lugar
Oasis implementa cifrado **propio a nivel de aplicación** en
`src/models/crypto.js`, instanciado por dominio en `backend.js`
(`tribeCrypto`, `chatCrypto`, `padCrypto`, `mapCrypto`, `calendarCrypto`,
`eventCrypto`, `forumCrypto`). Los grupos se cifran con esas claves derivadas
y se almacenan/replican como mensajes normales del feed.
---
## 1.7 Archivos clave de la capa de red
| Archivo | Rol |
|---|---|
| `src/server/SSB_server.js` | Construye el sbot y carga los plugins (56-98) |
| `src/server/ssb_config.js` | Fusiona `server-config.json`; fija `blobs.max=50MB` |
| `src/server/ssb_metadata.js` | Deriva e imprime puerto SSB, hops, LAN, feed ID |
| `src/server/lanRouter.js` | Descubrimiento/staging de peers LAN |
| `src/configs/server-config.json` | `caps.shs`, `pub`, `friends.hops`, `connections` (puertos) |
| `src/configs/oasis-config.json` | `lanBroadcasting`, `wish`, `pmVisibility`, módulos |
| `src/client/gui.js` | El "cooler": cliente muxrpc por socket Unix noauth |
| `src/backend/blobHandler.js` | add/has/want/get de blobs |
| `scripts/oasis-pub.js` | Comandos pub (invite/announce/follow) |
Siguiente: **[02-arquitectura-software.md](02-arquitectura-software.md)**.

View file

@ -0,0 +1,350 @@
# 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:<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:
```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
`<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](03-modulos-y-juegos.md)**.

View file

@ -0,0 +1,216 @@
# 3. El módulo de juegos y la anatomía de un módulo
> **Hallazgo clave que cambia las expectativas:** el módulo de juegos de Oasis
> **NO es un sistema multijugador asíncrono por SSB**. Es mucho más simple:
>
> - Cada juego es un **único `index.html` autocontenido** (HTML + CSS + JS
> inline), 100% client-side, servido como archivo estático y embebido en un
> `<iframe>`. No hay JS externos, ni sprites, ni carpetas de assets: cada
> carpeta de juego tiene **solo 2 archivos**: `index.html` + `thumbnail.svg`.
> - La **única integración con SSB** es publicar un mensaje tipo `gameScore`
> (puntuación individual) para un **Hall of Fame** (ranking global). No hay
> retos, ni partidas, ni movimientos sincronizados. El "multijugador" se
> reduce a comparar las puntuaciones de distintos autores leyendo el log SSB.
---
## 3.1 El modelo: `src/models/games_model.js` (67 líneas)
Minimalista. Solo gestiona puntuaciones. Patrón factory estándar:
`module.exports = ({ cooler }) => {...}`.
- **Líneas 5-9**: lista blanca `VALID_GAMES` (Set de ids permitidos). ⚠️ Incluye
14 juegos pero **NO** `rockpaperscissors` ni `audiopendulum` (que sí están en
la vista): esos dos **no pueden enviar score** (el modelo los rechaza).
- **Líneas 18-25** `readAll()`: vuelca todo el log con `createLogStream`.
- **Líneas 28-38** `submitScore(game, score)` — único método de escritura:
```js
async submitScore(game, score) {
if (!VALID_GAMES.has(game)) throw new Error('invalid game');
const n = Number(score);
if (!Number.isFinite(n) || n < 0 || n > 9999999) throw new Error('invalid score');
const ssbClient = await openSsb();
return new Promise((resolve, reject) => {
ssbClient.publish({ type: 'gameScore', game, score: Math.round(n) },
(err, msg) => err ? reject(err) : resolve(msg));
});
}
```
El mensaje SSB resultante: `{ type: 'gameScore', game: '<id>', score: <int> }`.
- **Líneas 40-65** `getHallOfFame()`: recorre el log, filtra
`content.type === 'gameScore'`, y por cada par `game:author` guarda **solo la
mejor puntuación**. Devuelve `hall[game] = [{author, score, game, ts}, ...]`
ordenado descendente y truncado a top-10 por juego.
---
## 3.2 La vista: `src/views/games_view.js` (151 líneas)
- **Líneas 5-22** `getGames()`: catálogo hardcodeado de **16 juegos**. Cada
entrada es `{ id, title: () => i18n.<key>, desc: () => i18n.<key> }`. Es el
registro maestro de la vista.
- **Línea 58** `VALID_GAME_IDS`: un **tercer** Set, usado para validar la ruta
del shell. Incluye los 16.
- **Líneas 26-56** `renderHallOfFame(hall)`: tabla por juego con thumbnail
(`/game-assets/${id}/thumbnail.svg`), título, descripción y filas (posición,
`userLink(author)`, score, fecha).
- **Líneas 60-93** `gameShellView(name)`: así se **embebe cada juego**. Valida
`name` contra `VALID_GAME_IDS` y renderiza un iframe:
```js
iframe({
src: `/game-assets/${name}/index.html`,
class: `game-iframe game-iframe-${name}`,
scrolling: 'no',
allowfullscreen: true
})
```
- **Líneas 95-151** `gamesView(filter, hall)`: el **lobby**. Barra de filtros
(`all` / `scoring`). En modo `all` lista cada juego con thumbnail, título,
récord top y botón PLAY → `/games/${id}`. En modo `scoring` invoca
`renderHallOfFame`.
> ⚠️ Hay **TRES listas** que deben mantenerse sincronizadas: `getGames()` y
> `VALID_GAME_IDS` (vista) + `VALID_GAMES` (modelo). La inconsistencia de
> `rockpaperscissors`/`audiopendulum` lo demuestra.
---
## 3.3 Estructura de un juego: `src/games/<nombre>/`
17 carpetas: `8ball, arkanoid, artillery, asteroids, audiopendulum, cocoland,
cocoman, ecoinflow, flipflop, labyrinth, neoninfiltrator, pingpong,
rockpaperscissors, spaceinvaders, tetris, tiktaktoe`.
**TODOS los juegos tienen exactamente 2 archivos**:
- `index.html` (6-10 KB): documento completo y autosuficiente, con `<style>` y
`<script>` inline. Sin dependencias externas, sin CDN.
- `thumbnail.svg` (~1-2 KB): miniatura SVG vectorial (viewBox ~`0 0 400 220`).
No existe distinción "arcade" vs "social SSB": todos son arcade HTML5/canvas o
DOM puro, client-side, en iframe. La única diferencia es si **envían score** o
no.
**Mecanismo de envío de score** (idéntico en todos los juegos puntuables;
ejemplo `tiktaktoe/index.html`):
```html
<div id="scoreSubmit" style="display:none">
<form method="POST" action="/games/submit-score" target="_top">
<input type="hidden" name="game" value="tiktaktoe">
<input type="hidden" id="scoreInput" name="score" value="0">
<button type="submit">Submit Score to Hall of Fame</button>
</form>
</div>
```
Claves del patrón:
- `target="_top"` → el POST **rompe el iframe** y navega la ventana superior
(necesario porque el juego corre sandboxeado dentro de `/games/<name>`).
- El JS del juego rellena `#scoreInput` al terminar
(`document.getElementById('scoreInput').value = score;`) y muestra el `div`.
- Cada juego tiene una topbar con `<a href="/games" target="_top">← Back</a>`.
El resto del `index.html` es lógica de juego propia en JS vanilla (tiktaktoe usa
minimax con poda alfa-beta; tetris usa canvas).
`gameScore` además es un tipo SSB de primera clase **indexado en búsqueda**
(`src/views/search_view.js` lo mapea a `/games/<game>` y lo renderiza con su
thumbnail).
---
## 3.4 Cómo se sirve y enruta
**Servido estático** — `src/client/middleware.js` (líneas 109-111):
```js
const gamesStatic = new Koa();
gamesStatic.use(koaStatic(join(__dirname, "..", "games")));
app.use(mount("/game-assets", gamesStatic));
```
Sub-app Koa con `koa-static` sobre `src/games/`, montada en `/game-assets`. Por
eso `/game-assets/tetris/index.html` sirve `src/games/tetris/index.html`.
**Independiente del router principal** → una carpeta nueva queda servida
automáticamente.
**Rutas dinámicas** — `src/backend/backend.js`:
- **Línea 598**: `const gamesModel = require('../models/games_model')({ cooler });`
- **Línea 1168**: `const { gamesView } = require("../views/games_view");`
- **Líneas 1358-1373**:
```js
.get('/games', async (ctx) => {
if (!checkMod(ctx, 'gamesMod')) { ctx.redirect('/modules'); return; }
const filter = ctx.query.filter === 'scoring' ? 'scoring' : 'all';
const hall = await gamesModel.getHallOfFame();
ctx.body = gamesView(filter, hall);
})
.get('/games/:name', async (ctx) => {
if (!checkMod(ctx, 'gamesMod')) { ctx.redirect('/modules'); return; }
const { gameShellView } = require('../views/games_view');
ctx.body = gameShellView(ctx.params.name);
})
.post('/games/submit-score', koaBody(), async (ctx) => {
if (!checkMod(ctx, 'gamesMod')) { ctx.redirect('/modules'); return; }
const { game, score } = ctx.request.body;
try { await gamesModel.submitScore(game, score); } catch (_) {}
ctx.redirect('/games?filter=scoring');
})
```
Flujo completo:
```
/games (lobby)
└─ PLAY → /games/:name (shell con iframe)
└─ iframe carga /game-assets/:name/index.html
└─ al terminar: POST /games/submit-score (target="_top")
└─ redirect /games?filter=scoring (Hall of Fame)
```
El guard `checkMod(ctx, 'gamesMod')` (líneas 170-177) revisa la config del
servidor + una cookie por-usuario; default `on`. Si está off → `/modules`.
---
## 3.5 Cómo se registra el módulo (puntos de integración)
| Lugar | Archivo | Qué hay |
|---|---|---|
| **Menú** | `src/views/main_views.js:898` | `renderGamesLink()` (emite el `navLink` si `gamesMod==="on"`); se invoca en línea 1280 dentro del `navGroup` "creative" |
| **Config** | `src/configs/oasis-config.json` | `"gamesMod": "on"` dentro de `modules` |
| **Página /modules** | `src/views/modules_view.js:23` | `{ name: 'games', label, description }` |
| **Listas de backend** | `src/backend/backend.js` | `'games'` en línea 1346 (GET /modules), 7054 (POST /save-modules), 7038 (`ALL_MODULES`) y presets (7040-7042) |
| **i18n** | `src/client/assets/translations/oasis_*.js` (×11) | `gamesTitle`, `gamesDescription`, `modulesGamesLabel/Description`, y por juego `games<Nombre>Title` + `games<Nombre>Desc` |
> El nombre del módulo está **hardcodeado y duplicado en muchos sitios**; no hay
> un registro central único. Por eso, al crear un módulo, cada punto es
> obligatorio. → [04-tutorial-modulo-desde-cero.md](04-tutorial-modulo-desde-cero.md)
---
## 3.6 Receta rápida: añadir un juego NUEVO al sistema existente
Para añadir un juego `foobar`:
1. **`src/games/foobar/index.html`** — HTML autocontenido (CSS+JS inline).
Incluir la topbar `← Back` y, si puntúa, el form oculto a
`/games/submit-score` con `target="_top"` y `name="game" value="foobar"`.
2. **`src/games/foobar/thumbnail.svg`** — miniatura.
3. **`src/views/games_view.js`**:
- Añadir a `getGames()`:
`{ id: 'foobar', title: () => i18n.gamesFoobarTitle, desc: () => i18n.gamesFoobarDesc }`.
- Añadir `'foobar'` a `VALID_GAME_IDS`.
4. **`src/models/games_model.js`** — añadir `'foobar'` a `VALID_GAMES`
(**obligatorio si puntúa**, o `submitScore` lo rechaza).
5. **i18n** — añadir `gamesFoobarTitle` y `gamesFoobarDesc` en TODOS los
`oasis_*.js`.
No hay que tocar el backend ni el static serving (la carpeta nueva ya se sirve
por `/game-assets/`).

View file

@ -0,0 +1,361 @@
# 4. Tutorial: crear un módulo desde cero
Vamos a crear un módulo nuevo, **paso a paso**, tomando `games` como plantilla.
Como caso práctico crearemos un módulo `arena` (un módulo de juegos propio: un
lobby de juegos con su propio ranking). Sustituye `arena` por el nombre de tu
módulo.
> **Idea clave**: en Oasis no existe un "registro central" de módulos. Crear un
> módulo = crear 2 archivos nuevos (model + view) y **tocar ~7 sitios** de
> integración. Esta guía los enumera todos. Si te saltas uno, el módulo
> funcionará a medias (p.ej. la ruta existe pero no aparece en el menú).
## Mapa de lo que vamos a tocar
```
NUEVO src/models/arena_model.js ← lógica de datos (SSB)
NUEVO src/views/arena_view.js ← render HTML (hyperaxe)
EDITA src/backend/backend.js ← instanciar modelo + rutas + 4 listas
EDITA src/views/main_views.js ← enlace en el menú lateral
EDITA src/configs/oasis-config.json ← flag arenaMod: "on"
EDITA src/views/modules_view.js ← entrada en la página /modules
EDITA src/client/assets/translations/oasis_*.js (×11) ← i18n
(opc) src/client/middleware.js ← static mount si sirves archivos (juegos)
```
---
## Paso 1 — El modelo: `src/models/arena_model.js`
El modelo encapsula **toda la lógica de datos**: publicar y leer mensajes SSB.
Sigue el patrón factory `({ cooler }) => ({...})`.
```js
const pull = require('../server/node_modules/pull-stream');
const { getConfig } = require('../configs/config-manager.js');
const logLimit = getConfig().ssbLogStream?.limit || 5000;
const VALID_GAMES = new Set(['snake', 'breakout']); // tus juegos puntuables
module.exports = ({ cooler }) => {
let ssb;
const openSsb = async () => {
if (!ssb) ssb = await cooler.open();
return ssb;
};
function readAll(ssbClient) {
return new Promise((resolve, reject) => {
pull(
ssbClient.createLogStream({ limit: logLimit }),
pull.collect((err, res) => (err ? reject(err) : resolve(res)))
);
});
}
return {
// ESCRITURA: publica un mensaje SSB tipado
async submitScore(game, score) {
if (!VALID_GAMES.has(game)) throw new Error('invalid game');
const n = Number(score);
if (!Number.isFinite(n) || n < 0 || n > 9999999) throw new Error('invalid score');
const ssbClient = await openSsb();
return new Promise((resolve, reject) => {
ssbClient.publish({ type: 'arenaScore', game, score: Math.round(n) },
(err, msg) => (err ? reject(err) : resolve(msg)));
});
},
// LECTURA: lee el log, filtra por type, agrega
async getRanking() {
const ssbClient = await openSsb();
const messages = await readAll(ssbClient);
const best = {};
for (const m of messages) {
const c = m.value && m.value.content;
if (!c || c.type !== 'arenaScore') continue;
if (!VALID_GAMES.has(c.game)) continue;
const key = `${c.game}:${m.value.author}`;
const score = Number(c.score);
if (!Number.isFinite(score)) continue;
if (!best[key] || score > best[key].score) {
best[key] = { author: m.value.author, score, game: c.game, ts: m.value.timestamp || 0 };
}
}
const ranking = {};
for (const g of VALID_GAMES) ranking[g] = [];
for (const e of Object.values(best)) if (ranking[e.game]) ranking[e.game].push(e);
for (const g of VALID_GAMES) ranking[g] = ranking[g].sort((a, b) => b.score - a.score).slice(0, 10);
return ranking;
}
};
};
```
Reglas del patrón de modelo:
- **Conexión SSB lazy** con `openSsb()` cacheado.
- **Escritura** siempre con `ssb.publish({ type: '<algo>', ... })`. El `type` es
el discriminador de tus mensajes en el log global compartido — elige un nombre
único (`arenaScore`, no `score`).
- **Lectura** con `createLogStream` + `pull.collect`, luego filtras por `type` en
memoria.
- **Borrado/edición** (si lo necesitas): publica
`{ type:'tombstone', target, author }` y usa
`src/models/tombstone_validator.js` al leer. Para editar, publica una versión
nueva con `replaces: <idAnterior>`.
---
## Paso 2 — La vista: `src/views/arena_view.js`
La vista recibe los datos del modelo y devuelve HTML con **hyperaxe**. Siempre se
envuelve en `template(...)` y se usan claves `i18n` (nunca texto literal).
```js
const { div, h2, p, section, form, input, button, a, img, table, tr, td, th, iframe }
= require("../server/node_modules/hyperaxe");
const { template, i18n, userLink } = require('./main_views');
const moment = require("../server/node_modules/moment");
const getGames = () => [
{ id: 'snake', title: () => i18n.arenaSnakeTitle, desc: () => i18n.arenaSnakeDesc },
{ id: 'breakout', title: () => i18n.arenaBreakoutTitle, desc: () => i18n.arenaBreakoutDesc },
];
const VALID_GAME_IDS = new Set(['snake', 'breakout']);
// Lobby
exports.arenaView = (filter = 'all', ranking = null) => {
const games = getGames();
const filterBar = div({ class: 'filter-group' },
form({ method: 'GET', action: '/arena' },
input({ type: 'hidden', name: 'filter', value: 'all' }),
button({ type: 'submit', class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.arenaFilterAll)),
form({ method: 'GET', action: '/arena' },
input({ type: 'hidden', name: 'filter', value: 'scoring' }),
button({ type: 'submit', class: filter === 'scoring' ? 'filter-btn active' : 'filter-btn' }, i18n.arenaFilterScoring))
);
const content = filter === 'scoring' && ranking
? div({ class: 'arena-scoring' },
getGames().filter(g => ranking[g.id] && ranking[g.id].length).map(g =>
div({ class: 'arena-section' },
h2(g.title()),
table({ class: 'ranking-table' },
tr(th('#'), th(i18n.arenaPlayer), th(i18n.arenaScore)),
...ranking[g.id].map((e, i) =>
tr(td(String(i + 1)), td(userLink(e.author)), td(String(e.score))))))))
: div({ class: 'arena-list' },
games.map(g =>
div({ class: 'game-row' },
img({ src: `/arena-assets/${g.id}/thumbnail.svg`, loading: 'lazy' }),
div(h2(g.title()), p(g.desc())),
a({ href: `/arena/${g.id}`, class: 'filter-btn' }, i18n.arenaPlayButton))));
return template(i18n.arenaTitle, section(h2(i18n.arenaTitle), filterBar), section(content));
};
// Shell: embebe el juego en un iframe
exports.arenaShellView = (name) => {
if (!VALID_GAME_IDS.has(name)) return template(i18n.arenaTitle, section(p(i18n.notFound || 'Not found')));
const game = getGames().find(g => g.id === name);
return template(game ? game.title() : name,
section({ class: 'game-shell-section' },
iframe({ src: `/arena-assets/${name}/index.html`, class: `game-iframe game-iframe-${name}`,
scrolling: 'no', allowfullscreen: true })));
};
```
Reglas del patrón de vista:
- `template(titulo, ...secciones)` siempre como envoltorio (te da head, menú
lateral, tema, etc.).
- Helpers de `main_views`: `userLink(feedId)`, `markdown(texto, mentions)`,
chips, `errorView`...
- Si tu módulo no es de juegos, simplemente no uses iframe/static: renderiza tus
datos directamente (listas, formularios POST a tus rutas).
---
## Paso 3 — Backend: `src/backend/backend.js`
**3a. Instanciar el modelo** (junto a la línea ~598, donde están los demás):
```js
const arenaModel = require('../models/arena_model')({ cooler });
```
**3b. Importar la vista** (junto a la línea ~1168):
```js
const { arenaView } = require("../views/arena_view");
```
**3c. Añadir las rutas** (dentro de la cadena `router....`, junto a las de
games en ~1358). Cada ruta empieza con el guard `checkMod`:
```js
.get('/arena', async (ctx) => {
if (!checkMod(ctx, 'arenaMod')) { ctx.redirect('/modules'); return; }
const filter = ctx.query.filter === 'scoring' ? 'scoring' : 'all';
const ranking = await arenaModel.getRanking();
ctx.body = arenaView(filter, ranking);
})
.get('/arena/:name', async (ctx) => {
if (!checkMod(ctx, 'arenaMod')) { ctx.redirect('/modules'); return; }
const { arenaShellView } = require('../views/arena_view');
ctx.body = arenaShellView(ctx.params.name);
})
.post('/arena/submit-score', koaBody(), async (ctx) => {
if (!checkMod(ctx, 'arenaMod')) { ctx.redirect('/modules'); return; }
const { game, score } = ctx.request.body;
try { await arenaModel.submitScore(game, score); } catch (_) {}
ctx.redirect('/arena?filter=scoring');
})
```
**3d. Añadir `'arena'` a las cuatro listas de módulos** (si no lo haces, el
módulo no aparecerá en la página `/modules` ni se podrá activar/desactivar):
- Línea **1346** — array `modules` del GET `/modules`.
- Línea **7054** — array `modules` del POST `/save-modules`.
- Línea **7038**`ALL_MODULES`.
- Líneas **7040-7042** — los presets (`minimal`/`social`/`economy`/`full`) donde
quieras que aparezca por defecto.
---
## Paso 4 — Menú lateral: `src/views/main_views.js`
**4a. Crear `renderArenaLink()`** (copia el patrón de `renderGamesLink`,
línea ~898):
```js
const renderArenaLink = () => {
const arenaMod = getConfig().modules.arenaMod === "on";
return arenaMod
? [navLink({ href: "/arena", emoji: "🎮", text: i18n.arenaTitle, class: "arena-link enabled" })]
: "";
};
```
**4b. Invocarlo dentro de un `navGroup`** (junto a `renderGamesLink()` en la
línea ~1280, grupo "creative", o el grupo que prefieras):
```js
renderGamesLink(),
renderArenaLink(), // ← añadir
```
---
## Paso 5 — Config: `src/configs/oasis-config.json`
Añadir el flag dentro de `modules` (junto a `"gamesMod": "on"`):
```json
"arenaMod": "on",
```
`config-manager.js` también tiene un `defaultConfig`; si quieres que el módulo
exista en instalaciones nuevas, añade el flag ahí también.
---
## Paso 6 — Página `/modules`: `src/views/modules_view.js`
Añadir una entrada al array `modules` (línea ~23). El toggle on/off lo renderiza
el código genérico (`${name}Mod` / `${name}Form`):
```js
{ name: 'arena', label: i18n.modulesArenaLabel, description: i18n.modulesArenaDescription },
```
---
## Paso 7 — i18n: `src/client/assets/translations/oasis_*.js`
En **todos** los archivos de idioma (×11) añade, como mínimo:
```js
arenaTitle: 'Arena',
arenaDescription: 'Play and compete in your network.',
arenaFilterAll: 'All',
arenaFilterScoring: 'Ranking',
arenaPlayButton: 'PLAY',
arenaPlayer: 'Player',
arenaScore: 'Score',
modulesArenaLabel: 'Arena',
modulesArenaDescription: 'A games arena with rankings.',
// y por cada juego:
arenaSnakeTitle: 'Snake', arenaSnakeDesc: 'Classic snake.',
arenaBreakoutTitle: 'Breakout', arenaBreakoutDesc: 'Break the bricks.',
```
Empieza por `oasis_en.js` (es el fallback). Si una clave falta en un idioma,
usa el patrón `i18n.x || 'Default'` en la vista para evitar `undefined`.
---
## Paso 8 (opcional) — Servir archivos estáticos (juegos)
Solo si tu módulo sirve archivos (como hace games con sus `index.html`). En
`src/client/middleware.js`, replica el bloque de games (líneas 109-111):
```js
const arenaStatic = new Koa();
arenaStatic.use(koaStatic(join(__dirname, "..", "arena-games")));
app.use(mount("/arena-assets", arenaStatic));
```
Y crea tus juegos en `src/arena-games/<id>/index.html` + `thumbnail.svg`. Cada
`index.html` autocontenido; si puntúa, el form oculto apunta a
`/arena/submit-score` con `target="_top"`:
```html
<form method="POST" action="/arena/submit-score" target="_top">
<input type="hidden" name="game" value="snake">
<input type="hidden" id="scoreInput" name="score" value="0">
<button type="submit">Submit Score</button>
</form>
```
---
## Checklist final
```
[ ] src/models/arena_model.js (factory {cooler}, publish/read SSB)
[ ] src/views/arena_view.js (hyperaxe + template + i18n)
[ ] backend.js: instancia modelo (~598)
[ ] backend.js: import vista (~1168)
[ ] backend.js: rutas GET/POST con checkMod(ctx,'arenaMod')
[ ] backend.js: 'arena' en las 4 listas (1346, 7054, 7038, presets)
[ ] main_views.js: renderArenaLink() + llamada en navGroup
[ ] oasis-config.json: "arenaMod": "on"
[ ] modules_view.js: entrada en array modules
[ ] oasis_*.js (×11): claves i18n
[ ] (opc) middleware.js: static mount + carpeta de juegos
```
## Cómo probarlo
```bash
# Arranca sbot + frontend
cd src/server && npm run start
# o por separado (sbot ya levantado):
node ../backend/backend.js
```
Abre `http://localhost:3000/arena`. Si redirige a `/modules`, activa el módulo
ahí (o revisa `arenaMod` en `oasis-config.json`). Comprueba:
1. El enlace aparece en el menú lateral.
2. `/arena` muestra el lobby.
3. `/arena/snake` embebe el juego.
4. Al enviar score, redirige a `/arena?filter=scoring` y aparece en el ranking.
> Errores típicos: `submitScore` lanza `invalid game` → falta el id en
> `VALID_GAMES` (modelo). El juego no carga en el iframe → ruta de
> `/arena-assets/...` mal o falta el static mount. La vista peta con `undefined`
> → falta una clave i18n; añade el fallback `|| '...'`.
Siguiente: **[05-mapa-funciones.txt](05-mapa-funciones.txt)** — el mapa ASCII de
relaciones entre funciones, persistencia y media.

View file

@ -0,0 +1,262 @@
================================================================================
OASIS · MAPA DE FUNCIONES Y FLUJO
Cómo se relacionan las funciones lógicas entre archivos,
la persistencia (SSB) y la media (blobs)
versión 0.8.1
================================================================================
LEYENDA
[proc] proceso ( ) función / método
--> llamada / flujo === límite de proceso ~~~ red / disco
*.js archivo #NNN número de línea aprox.
================================================================================
0. VISIÓN GLOBAL: DOS PROCESOS
================================================================================
NAVEGADOR OTROS PEERS SSB
(HTML puro, (misma SHS cap)
sin JS) |
| HTTP :3000 | net:IP:8008~shs
v v ~~~~~~~~~~~~~
============================[proc gui]== ==============[proc sbot]======
| src/backend/backend.js (Koa router) | | src/server/SSB_server.js |
| | | SecretStack({caps}).use(..)|
| cooler = src/client/gui.js | | ssb-db (log append-only) |
| | | <-> | ssb-ebt / ssb-friends |
| | cooler.open() |unix | ssb-blobs / ssb-conn |
| v |sock | ssb-box / ssb-private |
| ssb (cliente muxrpc) ---------------|~noauth-> muxrpc API (publish,get, |
| | | createLogStream,...) |
========================================= ===============================
modelos + vistas replicación P2P por gossip
================================================================================
1. CADENA DE UNA PETICIÓN (el patrón de TODO módulo)
================================================================================
navegador
| GET /modulo (o POST /modulo/accion)
v
middleware.js (#43..#209) ── app.listen({host,port})
| setLanguage(cookie) · gate indexación · refresco shared-state(60s)
v
backend.js router.get('/modulo') #1270..#7124 (~472 rutas)
| checkMod(ctx,'moduloMod') #170 ── off? --> redirect /modules
|
+--> xModel.metodo(filtro) [ LECTURA / ESCRITURA SSB ] -----+
| (src/models/xxx_model.js) |
| v
| <------------------ datos JS ------------------------ cooler.open()
| (gui.js)
+--> xView(datos, filtro) [ RENDER HTML ] |
| (src/views/xxx_view.js) v
| template(i18n.titulo, section(...)) ssb muxrpc
v
ctx.body = "<html>...</html>" --> Koa --> navegador
POST: koaBody() -> stripDangerousTags() -> xModel.crear() -> ctx.redirect()
(patrón Post/Redirect/Get; el GET siguiente re-renderiza)
================================================================================
2. PERSISTENCIA: cómo el MODELO habla con SSB (pull-stream + publish)
================================================================================
src/models/xxx_model.js = module.exports = ({cooler, ...deps}) => ({...})
openSsb() ............ if(!ssb) ssb = await cooler.open(); // lazy, cacheado
|
v
ESCRIBIR (persistir):
ssb.publish({ type:'<tipo>', ...campos }, cb)
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+--> ssb-db: añade mensaje FIRMADO (ed25519) al log append-only
| del feed propio @miclave.ed25519
+--> ssb-ebt replica el mensaje a los peers (hops<=2) ~~~~>
LEER (consultar):
pull(
ssb.createLogStream({limit}) <-- lee el log local (todos los feeds
pull.filter(m => m.value.content.type === '<tipo>') replicados)
pull.collect((err,res)=>...)
)
(variantes: ssb.messagesByType, ssb.query.read, ssb.backlinks.read,
ssb.createUserStream{id})
BORRAR (lógico; el log NO se borra):
ssb.publish({ type:'tombstone', target:<msgId>, author })
|
+--> tombstone_validator.js :: buildValidatedTombstoneSet(msgs)
acepta SOLO si author(tombstone) === author(target) [autoría]
EDITAR:
ssb.publish({ ...nuevo, replaces:<idAnterior> }) + tombstone del viejo
|
+--> al leer, se sigue la cadena replaces -> hasta la "punta"
----------------------------------------------------------------------------
TIPOS DE MENSAJE (content.type) = el discriminador en el log compartido
----------------------------------------------------------------------------
nativos SSB : post · vote(like) · contact(follow/block) · about(perfil)
Oasis : votes · tombstone · task · event · calendar · transfer ·
tribe · market · report · job · project · gameScore · ...
================================================================================
3. MEDIA: cómo viajan los ficheros (BLOBS, fuera del log)
================================================================================
El log solo guarda el HASH. El binario viaja aparte por la red SSB.
SUBIR:
navegador --(multipart)--> backend.js --> blobHandler.js
ssb.blobs.add(pull-stream) --> &<sha256>.sha256 (ref = hash)
(luego se publica un mensaje con ese ref dentro de content)
MOSTRAR / DESCARGAR:
vista genera <img src="/blob/&xxx.sha256"> ó markdown.js resuelve menciones
|
v
backend.js ruta /blob/:id --> blobHandler.js
ssb.blobs.has(id)? --- no ---> ssb.blobs.want(id) ~~~~~~~~~~~~~~~~~>
| (gossip "want/has" a los peers)
+-- si ---> ssb.blobs.get(id) -> pull-stream -> ctx.body (bytes)
límite: 50 MB (ssb_config.js #17-19)
================================================================================
4. RENDER: cómo la VISTA construye HTML (hyperaxe, sin .html)
================================================================================
src/views/xxx_view.js
require("../server/node_modules/hyperaxe") -> div(), form(), section()...
require('./main_views') -> { template, i18n, userLink, markdown, chips }
exports.xView(datos) =
template( i18n.xTitle, // <- LAYOUT global
section( h2(i18n.xTitle), ...lista... ) )
|
v
main_views.js :: template(titulo, ...elems) #1017..#1321
html
+- head: <title> · CSS · /assets/themes/<tema>.css (oasis-config.themes)
+- body
+- header (logo, inbox badge <- shared-state, publish, search)
+- sidebar-left = navGroup(...) { renderXxxLink() por módulo }
| navLink({href,emoji,text}) #327
| renderGamesLink() #898 -> si gamesMod==='on' (config-manager)
+- main-content = tus secciones
i18n: objeto compartido mutado por setLanguage(lang) (11 idiomas)
backend middleware lo fija por cookie en cada request
markdown.js: ssb-markdown -> resuelve @feed,%msg,&blob,#hashtag a rutas /...
================================================================================
5. GRAFO DE DEPENDENCIAS ENTRE ARCHIVOS
================================================================================
src/backend/backend.js (orquestador central)
|--require--> ../client/gui.js .................. cooler (conexión SSB)
|--require--> ../client/middleware.js ........... http server Koa
|--require--> ../configs/config-manager.js ...... getConfig/saveConfig
|--require--> ../configs/shared-state.js ........ estado en memoria
|--require--> ./sanitizeHtml.js / ./blobHandler.js
|--require--> ../server/SSB_server.js ........... config + (sbot in-process)
|--require--> ../models/*_model.js (factories: ({cooler,deps}))
| |--inyecta--> otros modelos (agenda<-tasks,events,market..)
| |--inyecta--> *Crypto (crypto.js por dominio)
|--require--> ../views/*_view.js (funciones de render)
src/models/xxx_model.js
|--require--> pull-stream · moment
|--require--> ./tombstone_validator.js (función pura, sin deps)
|--require--> ../configs/config-manager.js
|--recibe----> cooler (NO importa gui directamente)
src/views/xxx_view.js
|--require--> hyperaxe
|--require--> ./main_views.js { template, i18n, userLink, helpers }
|--require--> moment · (a veces) ../server/SSB_server (feedId propio)
src/views/main_views.js (el "framework" de vistas)
|--require--> hyperaxe · lodash · highlight.js · qrcode · moment
|--require--> ./markdown.js · ../backend/renderUrl.js · ./sanitizeHtml?
|--require--> ../backend/nameCache.js (Map nombres <- main_models warmup)
|--require--> ../client/gui.js · ../configs/{config-manager,shared-state}
|--require--> ../client/assets/translations/i18n.js (11 idiomas)
src/server/SSB_server.js (sbot)
|--require--> secret-stack · ssb-caps · ssb-db · plugins ssb-*
|--require--> ./ssb_config.js (<- server-config.json) · ./ssb_metadata.js
|--require--> ./lanRouter.js (descubrimiento LAN)
================================================================================
6. EL MÓDULO GAMES, DE PRINCIPIO A FIN (ejemplo concreto)
================================================================================
MENU main_views.js #898 renderGamesLink() --(gamesMod==on)--> /games
|
LOBBY backend.js #1358 GET /games |
checkMod #170 -> gamesModel.getHallOfFame() #40 ----------+
| pull(createLogStream) |
| filter type==='gameScore' |
| best[game:author]=max(score) |
gamesView(filter,hall) #95 -> HTML |
|
PLAY GET /games/:name #1364 -> gameShellView(name) #60 |
<iframe src="/game-assets/<name>/index.html"> |
| |
STATIC middleware.js #109 mount('/game-assets', src/games/) ~~disk~+
|
JUEGO src/games/<name>/index.html (HTML+CSS+JS inline, canvas)
al terminar: form POST -> /games/submit-score target=_top
|
SCORE POST /games/submit-score #1369
gamesModel.submitScore(game,score) #28
ssb.publish({type:'gameScore',game,score}) ~~> SSB log
redirect /games?filter=scoring -> Hall of Fame
LISTAS A SINCRONIZAR (sin registro central):
games_view.js getGames() #5 + VALID_GAME_IDS #58 (16 juegos)
games_model.js VALID_GAMES #5 (14 -> falta rps,audio)
backend.js listas módulos #1346 #7038 #7054 #7040 ('games')
oasis-config.json modules.gamesMod
modules_view.js array modules #23
oasis_*.js gamesTitle, games<Nombre>Title/Desc (x11 idiomas)
================================================================================
7. CONFIG Y ESTADO (puntos de coordinación transversal)
================================================================================
oasis-config.json <--getConfig()/saveConfig()--> config-manager.js
| modules.{xMod:on/off} themes.current ux.current language
| wish pmVisibility lanBroadcasting ssbLogStream.limit wallet
+--leen--> backend.js (checkMod) · main_views (render links/tema) · modelos
server-config.json <-- ssb_config.js --> SSB_server.js
| caps.shs (red privada) friends.hops=2 connections (puerto 8008, unix)
shared-state.js (memoria, NO persistente)
| inboxCount carbon peersOnline ecoValue lastRefresh
+--escribe--> middleware refresco 60s +--lee--> main_views (badges)
nameCache.js (Map en memoria)
+--alimenta--> main_models._startNameWarmup (escucha 'about' en live)
+--consume---> main_models.about · main_views (mostrar nombres)
================================================================================
RESUMEN EN UNA FRASE
================================================================================
backend.js enruta -> el MODELO persiste/lee mensajes tipados en el log SSB
(replicado P2P por hops) y los blobs por want/has -> la VISTA (hyperaxe)
los pinta dentro de template() con i18n y tema -> y todo módulo se activa con
un flag xMod en oasis-config.json comprobado por checkMod().
================================================================================

View file

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at <solarnethub@riseup.net>. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

125
docs/devs/contract.md Normal file
View file

@ -0,0 +1,125 @@
# Collective Code Construction Contract - Oasis Implementation
The Collective Code Construction Contract (C4) is an evolution of the github.com [Fork + Pull Model](https://help.github.com/articles/about-pull-requests/), aimed at providing an optimal collaboration model for free software projects.
This is the Oasis-specific implementation, based on [revision 2 of C4](https://github.com/zeromq/rfc/blob/63024673f19ad136652ff7b3bfb3a6547811e006/42/README.md).
## Summary
Thank you for contributing to Oasis! Here we try capture how we collaborate, and why we do it this way.
This entire document is open for changes, if there is anything that is confusing or can be improved, please start a discussion with us!
## License
Copyright (c) 2009-2016 Pieter Hintjens.
This Specification is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This Specification is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses>.
## Abstract
C4 provides a standard process for contributing, evaluating and discussing improvements on software projects. It defines specific technical requirements for projects like a style guide, unit tests, `git` and similar platforms. It also establishes different personas for projects, with clear and distinct duties. C4 specifies a process for documenting and discussing issues including seeking consensus and clear descriptions, use of "pull requests" and systematic reviews.
## Language
The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](http://tools.ietf.org/html/rfc2119).
## 1. Goals
C4 is meant to provide a reusable optimal collaboration model for open source software projects. It has these specific goals:
1. To maximize the scale and diversity of the community around a project, by reducing the friction for new Contributors and creating a scaled participation model with strong positive feedbacks;
1. To relieve dependencies on key individuals by separating different skill sets so that there is a larger pool of competence in any required domain;
1. To allow the project to develop faster and more accurately, by increasing the diversity of the decision making process;
1. To support the natural life cycle of project versions from experimental through to stable, by allowing safe experimentation, rapid failure, and isolation of stable code;
1. To reduce the internal complexity of project repositories, thus making it easier for Contributors to participate and reducing the scope for error;
1. To enforce collective ownership of the project, which increases economic incentive to Contributors and reduces the risk of hijack by hostile entities.
## 2. Design
### 2.1. Preliminaries
1. The project MUST use the git distributed revision control system.
1. The project MUST be hosted on github.com or equivalent, herein called the "Platform".
1. The project MUST use the Platform issue tracker.
1. The project SHOULD have clearly documented guidelines for code style.
1. A "Contributor" is a person who wishes to provide a patch, being a set of commits that solve some clearly identified problem.
1. A "Maintainer" is a person who merges patches to the project. Maintainers are not developers; their job is to enforce process.
1. Contributors MUST NOT have commit access to the repository unless they are also Maintainers.
1. Maintainers MUST have commit access to the repository.
1. Everyone, without distinction or discrimination, MUST have an equal right to become a Contributor under the terms of this contract.
### 2.2. Licensing and Ownership
1. The project MUST use a share-alike license such as the MPLv2, or a GPLv3 variant thereof (GPL, LGPL, AGPL).
1. All contributions to the project source code ("patches") MUST use the same license as the project.
1. All patches are owned by their authors. There MUST NOT be any copyright assignment process.
1. Each Contributor MUST be responsible for identifying themselves in the project Contributor list.
### 2.3. Patch Requirements
1. Maintainers and Contributors MUST have a Platform account and SHOULD use their real names or a well-known alias.
1. A patch SHOULD be a minimal and accurate answer to exactly one identified and agreed problem.
1. A patch MUST adhere to the code style guidelines of the project if these are defined.
1. A patch MUST adhere to the "Evolution of Public Contracts" guidelines defined below.
1. A patch MUST NOT include non-trivial code from other projects unless the Contributor is the original author of that code.
1. A patch MUST compile cleanly and pass project self-tests on at least the principal target platform.
1. A patch commit message SHOULD consist of a single short (less than 50 characters) line stating the problem ("Problem: ...") being solved, followed by a blank line and then the proposed solution ("Solution: ...").
1. A "Correct Patch" is one that satisfies the above requirements.
### 2.4. Development Process
1. Change on the project MUST be governed by the pattern of accurately identifying problems and applying minimal, accurate solutions to these problems.
1. To request changes, a user SHOULD log an issue on the project Platform issue tracker.
1. The user or Contributor SHOULD write the issue by describing the problem they face or observe.
1. The user or Contributor SHOULD seek consensus on the accuracy of their observation, and the value of solving the problem.
1. Users MUST NOT log feature requests, ideas, suggestions, or any solutions to problems that are not explicitly documented and provable.
1. Thus, the release history of the project MUST be a list of meaningful issues logged and solved.
1. To work on an issue, a Contributor MUST fork the project repository and then work on their forked repository.
1. To submit a patch, a Contributor MUST create a Platform pull request back to the project.
1. A Contributor MUST NOT commit changes directly to the project.
1. If the Platform implements pull requests as issues, a Contributor MAY directly send a pull request without logging a separate issue.
1. To discuss a patch, people MAY comment on the Platform pull request, on the commit, or elsewhere.
1. To accept or reject a patch, a Maintainer MUST use the Platform interface.
1. Maintainers SHOULD NOT merge their own patches except in exceptional cases, such as non-responsiveness from other Maintainers for an extended period (more than 1-2 days).
1. Maintainers MUST NOT make value judgments on correct patches.
1. Maintainers MUST merge correct patches from other Contributors rapidly.
1. Maintainers MAY merge incorrect patches from other Contributors with the goals of (a) ending fruitless discussions, (b) capturing toxic patches in the historical record, (c) engaging with the Contributor on improving their patch quality.
1. The user who created an issue SHOULD close the issue after checking the patch is successful.
1. Any Contributor who has value judgments on a patch SHOULD express these via their own patches.
1. Maintainers SHOULD close user issues that are left open without action for an uncomfortable period of time.
### 2.5. Branches and Releases
1. The project MUST have one branch ("master") that always holds the latest in-progress version and SHOULD always build.
1. The project MUST NOT use topic branches for any reason. Personal forks MAY use topic branches.
1. To make a stable release a Maintainer must tag the repository. Stable releases MUST always be released from the repository master.
### 2.6. Evolution of Public Contracts
1. All Public Contracts (APIs or protocols) MUST be documented.
1. All Public Contracts SHOULD have space for extensibility and experimentation.
1. A patch that modifies a stable Public Contract SHOULD not break existing applications unless there is overriding consensus on the value of doing this.
1. A patch that introduces new features SHOULD do so using new names (a new contract).
1. New contracts SHOULD be marked as "draft" until they are stable and used by real users.
1. Old contracts SHOULD be deprecated in a systematic fashion by marking them as "deprecated" and replacing them with new contracts as needed.
1. When sufficient time has passed, old deprecated contracts SHOULD be removed.
1. Old names MUST NOT be reused by new contracts.
### 2.7. Project Administration
1. The project founders MUST act as Administrators to manage the set of project Maintainers.
1. The Administrators MUST ensure their own succession over time by promoting the most effective Maintainers.
1. A new Contributor who makes correct patches, who clearly understands the project goals, and the process SHOULD be invited to become a Maintainer.
1. Administrators SHOULD remove Maintainers who are inactive for an extended period of time, or who repeatedly fail to apply this process accurately.
1. Administrators SHOULD block or ban "bad actors" who cause stress and pain to others in the project. This should be done after public discussion, with a chance for all parties to speak. A bad actor is someone who repeatedly ignores the rules and culture of the project, who is needlessly argumentative or hostile, or who is offensive, and who is unable to self-correct their behavior when asked to do so by others.
## Further Reading
- [Argyris' Models 1 and 2](http://en.wikipedia.org/wiki/Chris_Argyris) - the goals of C4 are consistent with Argyris' Model 2.
- [Toyota Kata](http://en.wikipedia.org/wiki/Toyota_Kata) - covering the Improvement Kata (fixing problems one at a time) and the Coaching Kata (helping others to learn the Improvement Kata).

68
docs/devs/contributing.md Normal file
View file

@ -0,0 +1,68 @@
# Contributing
If you want to dive into the details, please see the [contract](./contract.md)
that defines the contributor role in this project. If you're comfortable with
a top-level summary, you can start here first.
Our workflow is basically [GitHub Flow][github-flow] with specific roles:
- **Contributor:** Write patches that reduce the number of problems.
- **Maintainers:** Merge patches that reduce the number of problems.
If you have an issue, it's best to open an issue to describe the problem and
discuss solutions, but don't worry if you've already skipped that step.
Assuming you already have a [developer install](./install.md) you should be
able to start editing source code. There are a few useful commands you should
know about:
- **`npm install`**: Ensure that software dependencies are installed.
- **`npm test`**: Ensure that all automated tests pass.
- **`npm run fix`**: If an automated test failed, this may fix it.
Please run `npm test` before writing a commit, because if there are errors then
maintainers won't be able to merge your patch. Please ask for help if `npm test`
is giving you any trouble.
**Note:** `npm run fix` is run automatically as a pre-commit hook. You always
have the option to disable pre-commit hooks with `git commit --no-verify`.
## Frequently Failed Tests
### Unknown word
<!-- spell-checker:disable -->
```
/src/index.js:10:42 - Unknown word (Scuttlebtut)
```
<!-- spell-checker:enable -->
If this word is a typo, please fix the typo. If this error is a mistake, and
you're sure that this is a word, please add the word to `.cspell.json`.
### Code style issues found
```
Checking formatting...
src/index.js
README.md
Code style issues found in the above file(s). Forgot to run Prettier?
```
You can use `npm run fix` to resolve inconsistent code style. Please remember to
add those changes with `git add` or similar before you commit.
## Tips
### TypeScript opportunities
If you're looking for places where TypeScript would enjoy more detail, you can
run the TypeScript linter with `--noImplicitAny`:
```sh
npx tsc --allowJs --resolveJsonModule --lib es2018,dom --checkJs --noEmit --skipLibCheck --noImplicitAny src/index.js
```
[github-flow]: https://guides.github.com/introduction/flow/

86
docs/devs/install.md Normal file
View file

@ -0,0 +1,86 @@
# Developer Install
To deploy the development environment:
```shell
git clone https://code.03c8.net/KrakensLab/oasis
cd oasis
bash install.sh
cd src/server
npm run dev
```
Once Oasis is started in dev mode, visit [http://localhost:3000](http://localhost:3000).
The backend restarts automatically (via [nodemon](https://nodemon.io)) whenever you save changes to `.js` or `.json` files in `src/backend/`, `src/models/`, `src/views/`, or `src/client/`. Static assets (`src/client/assets/`) do not trigger a restart. Page autoreload is not available because we avoid using JavaScript in the browser — reload the page manually to display your changes.
## Two-process architecture
Oasis runs as two cooperating Node processes:
- **`SSB_server.js`** — boots the local Secure Scuttlebutt sbot (gossip, EBT, friends, blobs, LAN, search, box, query, tangle, links, backlinks). Owns `~/.ssb`.
- **`backend.js`** — Koa HTTP server that connects to the sbot through `ssb-client` and renders pages with hyperaxe. Serves `http://localhost:3000`.
The backend talks to the sbot over a local Unix socket. If you only restart the backend (the default in `npm run dev`), the sbot keeps running. If you change anything under `src/server/` or anything that holds an SSB handle inside a model, restart the sbot too (kill the `SSB_server.js` process and re-run `npm start`).
## npm scripts
Run these from `src/server/`:
- **`npm start`** — boots the SSB sbot in the background, waits ~10 s, then starts the HTTP backend. Use this for an end-to-end local run.
- **`npm run start:ssb`** — start only the SSB sbot.
- **`npm run start:backend`** — start only the HTTP backend (assumes sbot is already running).
- **`npm run dev`** — backend under nodemon watch (auto-restart on file change). Sbot is **not** watched.
The launcher script at the repo root (`oasis.sh`) wraps these and detects whether to start in `server`, `pub`, or `gui` mode.
## Tests
Unit and integration tests live under `test/` at the repo root, grouped per module in `test/mods/`. Coverage spans **40+ modules** including tribes, feed, banking, parliament, courts, jobs, market, shops, media (audio/video/image/document/torrent), maps, pads, calendars, events, tasks, votes, transfers, reports, projects, opinions, activity, AI, CV, LARP, melody, and more.
Run the full suite from the `oasis/` directory:
```sh
# All modules, each in a subprocess with safe ~/.ssb isolation
bash test/run.sh
# Same, but skip the confirmation prompt
bash test/run.sh --yes
# All modules in a single Node process (no isolation, faster but mixes state)
node test/run.js
# A single module
node test/run.js mods/tribes
node test/run.js mods/media/audios
# Per-module shortcut (no isolation, fast iteration)
bash test/mods/<module>/run.sh
```
`test/run.sh` moves your live `~/.ssb` to a timestamped backup (`~/.ssb-bak-YYYYMMDD_HHMMSS`) before the run and creates a fresh empty `~/.ssb` for the tests. **Stop any running Oasis instance first** — only one process can hold `~/.ssb` open at a time. Pass `--restore` to restore the original `~/.ssb` after the run finishes (CI uses this).
What the tests cover, how to add a new module suite, and a record of bugs the test harness has caught are documented in [`test/README.md`](../../test/README.md). When you change a model, add or update its test under `test/mods/<module>/` so the change comes with a regression net.
## Useful commands while developing
- **`npm install`** — install / refresh dependencies.
- **`npm test`** — run automated tests (calls into the `test/` harness).
- **`npm run fix`** — auto-fix formatting and lint issues (also runs as a pre-commit hook).
## Directory map (cheat sheet)
- `src/server/` — SSB sbot entry, ssb-config, secret-stack plugin wiring, vendored `packages/ssb-server`.
- `src/backend/` — Koa HTTP entry (`backend.js`), middleware, blob handler, URL renderer, sanitizer.
- `src/models/` — per-module data access. Factory functions that receive `cooler` (and sometimes `tribeCrypto`, `tribesModel`) and return query/publish methods.
- `src/views/` — hyperaxe view functions. Pure HTML builders.
- `src/AI/` — local LLM service (`ai_service.mjs` on port 4001) and context assembler.
- `src/configs/``oasis-config.json` (module toggles, themes, language) and `config-manager.js`.
- `src/client/assets/` — CSS, theme files, translations (11 languages), static images.
- `docs/` — user and developer documentation (this folder).
- `test/` — test harness (`run.sh`, `run.js`, `seed.js`, `helpers/`) and per-module test suites in `mods/`.
- `scripts/` — build helpers (`build-deb.sh`, node_modules patcher).
## Pre-commit checks
The pre-commit hook runs `cspell` and `prettier`. See [`contributing.md`](./contributing.md) for what to do when a check fails (typos go in `.cspell.json`; formatting via `npm run fix`).

35
docs/devs/maintaining.md Normal file
View file

@ -0,0 +1,35 @@
# Maintaining
Please read the [contract](./contract) that defines the maintainer role in this
project. In short:
- Please merge any patches that reduce the number of problems in this project.
- If you have small nitpicks about a patch, please merge the patch and write a
new patch with your preferred improvements.
- **Take care of yourself and don't burn out.** Please don't sacrifice your
health to improve this project, and know that there are much more important
things in life than merging pull requests quickly.
## Tips
### Checking out a patch
If you want to check out pull request number 42 and you're comfortable running
the code on your local device.
```sh
remote="https://code.03c8.net/krakenlabs/oasis.git"
git fetch "$remote"
git reset --hard $remote master
git pull "$remote" pull/42/head
npm ci && npm test && npm start
```
No need to add their fork as a remote.
Or for ultimate convenience (and github lock-in), use the [github cli tool](https://cli.github.com):
```sh
gh pr list
gh pr checkout 42
```

34
docs/install/install.md Normal file
View file

@ -0,0 +1,34 @@
# Oasis Installation Guide
This guide will walk you through the process of installing **Oasis** on your device.
You can either use the automated installation script or manually download the source code.
---
## 1) Automated Installation (Recommended)
To install **Oasis** with a single command, run:
sh install.sh
---
## 2) Manual Installation
Try to execute the following steps (from a shell):
sudo apt-get install git curl
curl -sL http://deb.nodesource.com/setup_22.x | sudo bash -
sudo apt-get install -y nodejs
git clone https://code.03c8.net/KrakensLab/oasis
cd oasis
npm install .
---
## 3) Run Oasis
To run **Oasis** just launch:
sh oasis.sh

33
docs/security.md Normal file
View file

@ -0,0 +1,33 @@
# Security Policy
## Security Model
Oasis is experimental software, please don't trust it with your life.
If everything is working correctly, it's likely that:
- Only your computer can access Oasis.
- Only you can publish a message to your feed.
- Only the recipients of private messages can read the message.
- Only basic HTML is supported in blobs, which can't access the rest of Oasis.
It's important to know that this is not a silver bullet:
- Your public messages can be read by anyone on the network.
- Your IP address can be seen by anyone that peers with you.
- Your private messages can be read by anyone with access to your private key.
You should also know:
- Information that others can read can be saved, without your permission.
- Encryption techniques that are unbreakable today may become compromised in the future; maybe in dozens or hundreds of years.
- Don't trust your smartphone.
- Oasis is designed to run in a safe environment (SNH kit).
## Supported Versions
Only the latest release is supported.
## Reporting a Vulnerability
Send an email to epsylon@riseup.net to report any security problems. Please do not use the public issue tracker.