npm.io
0.3.5 • Published 17m agoCLI

dotrino-content

Licence
MIT
Version
0.3.5
Deps
3
Size
132 kB
Vulns
0
Weekly
0

dotrino-content

Nodo de contenido del ecosistema Dotrino: guarda y sirve media pesada (video/imagen/audio/archivos) direccionada por hash de contenido (cid), autohospedado por el usuario. Diseño completo en docs/DISENO.md; estado/continuación en docs/HANDOFF.md.

Estado: Fase 2 (aparato del vault) + vistas previas públicas. Al core local se le suma la identidad: el node se enrola a tu bóveda y se puede administrar desde tus apps por el proxy, sin abrir puertos. El HTTP de administración sigue escuchando solo en 127.0.0.1; con --public se abre, aparte, un puerto que sirve únicamente vistas previas para que un enlace compartido tenga tarjeta en las redes. El transporte P2P entre aparatos es lo que queda de la Fase 3.

Uso

# una vez: enlazar este node a tu bóveda (saca el código de `dotrino-vault pair`)
npx dotrino-content enroll <código>

npx dotrino-content start [--port 3777] [--dir ~/.dotrino-content] \
  [--max-gb 50] [--max-blob-mb 512] [--gc-min 60] [--no-agent]

# con vistas previas públicas (ver más abajo antes de encenderlo)
npx dotrino-content start --public --public-port 3778 --public-egress-gb 5 \
  --public-url https://content.tudominio.com

Al enrolar, el node genera su propia llave y muestra un código que tienes que tipear en la bóveda para aprobarlo: ese código no viaja por la red, así que aprobar exige tener delante esta máquina. La clave maestra nunca llega aquí — solo un certificado con caducidad, que el node renueva solo y que puedes revocar (dotrino-vault revoke <deviceId>) sin tocar el resto de tus aparatos.

Env: DOTRINO_CONTENT_DIR (datos), DOTRINO_CONTENT_LINK_DIR (enlace), PORT. Requiere Node ≥ 22.5. El core usa solo node:crypto y node:sqlite; la identidad viene de los pilares del ecosistema (@dotrino/remote-agent, @dotrino/identity).

Plano de control (Fase 2, por el proxy, cifrado)

Con el node enlazado, tus propias apps —cualquier aparato con un certificado de la misma bóveda— pueden administrarlo a distancia dentro de una sesión cifrada:

hello                    quién es el node (owner, versión, uso de disco, tope)
put {data,mime,…}        guardar algo pequeño desde otro aparato tuyo (≤ 256 KB)
get <cid>                leerlo de vuelta (≤ 256 KB)
list · stat <cid> · stats  qué guarda
pin <cid> · unpin <cid>  retener / soltar
remove <cid>             borrar
acl <cid> public|private abrir o cerrar un blob (público es opt-in explícito)
meta <cid> {…}           nombre/título/descripción para la tarjeta de la vista previa
thumb <cid> <thumbCid>   enlazar la miniatura (otro blob, público por su cuenta)
gc                       recolectar vencidos ahora

put y get tienen un tope duro de 256 KB, y eso NO es un límite a subir: es la frontera. El plano de control es el proxy del ecosistema —trama de 1 MB, cola de mensajes, no un almacén—, así que por aquí pasa lo que es un mensaje: un post (un eco pesa cientos de bytes) y una miniatura (decenas de KB). Por eso no hay subida por partes: trocear sería disimular la frontera y acabar usando la infraestructura del ecosistema como transporte. Los originales suben en local por HTTP y, entre aparatos, por P2P.

Vistas previas públicas (--public, apagado por defecto)

Para qué es: para que un enlace que compartes tenga TARJETA en X, LinkedIn, WhatsApp o Telegram. No es para servir tu contenido — eso se sigue abriendo en la app, con la referencia en el #fragment, que nunca llega a ningún servidor. Lo que sale por este puerto es la miniatura que tú marcaste pública, no el archivo.

GET|HEAD /c/<cid>   los bytes, si pasan TODOS los cerrojos de abajo
GET      /p/<cid>   permalink: tarjeta (og:*) + botón "Abrir" hacia la app
GET      /robots.txt · /health

Cinco cerrojos, y ninguno se puede saltar desde fuera:

Solo lo público y en claro lo cifrado no sale ni marcado público a mano en el índice
Solo imágenes de mapa de bits JPEG, PNG, GIF, WebP, AVIF. SVG no: es un documento que ejecuta scripts
El tipo se comprueba en los bytes el Content-Type lo declara quien sube, así que no se cree: un HTML subido como image/png responde 404
Tope de tamaño (--public-max-kb, 512) es lo que hace que esto sea un servidor de miniaturas y no un CDN. 0 lo quita y entonces sirve originales: el ancho de banda lo pagas tú
Límite por IP + techo diario --public-rate (60/min) y --public-egress-gb, que se persiste y corta antes de mandar una respuesta que no quepa

Lo privado responde 404, nunca 403: un 403 confirmaría que ese cid está aquí. Y robots.txt prohíbe todo (las tarjetas funcionan igual: los rastreadores de las redes piden la página cuando alguien pega el enlace, no indexan). --public-index lo levanta.

La miniatura la genera tu app al subir (con un canvas) y se sube como otro blob, que se enlaza con la op thumb. El node no decodifica imágenes: así no arrastra dependencias nativas. Enlazar una miniatura no la publica — se marca pública por su cuenta.

API HTTP (localhost)

POST   /c?ttl=<ms>&enc=1   subir (streaming; Content-Type = mime) → { cid, size, mime, existed }
GET    /c/<cid>            descargar/streamear (Range → 206; ETag = cid, immutable)
HEAD   /c/<cid>            size/mime/etag sin cuerpo
DELETE /c/<cid>            borrar
GET    /list               índice de blobs
POST   /pin/<cid>          retención (excluye del GC)     POST /unpin/<cid>
GET    /stats              nº de blobs, bytes usados, cuota
  • cid = sha256-<hex> (prefijo de algoritmo → extensible a BLAKE3 después; se usa SHA-256 de node:crypto porque no exige dependencias nativas y el .npmrc del ecosistema bloquea build scripts de npm).
  • Disco: blobs/<aa>/<bb>/<cid> (sharding); índice en SQLite (index.db).
  • Dedup por contenido: re-subir el mismo archivo devuelve existed: true.
  • Cuota (--max-gb): al no caber, el GC desaloja no-pineados más viejos; los pineados jamás se borran (si solo quedan pineados → 507).
  • TTL opcional por blob (?ttl=<ms>): vencido = candidato a GC.
  • El cifrado E2E es del lado del cliente (el node solo ve ciphertext si subes cifrado y marcas enc=1); la llave viaja en el #fragment del enlace.
  • owner y acl: con el node enlazado, todo lo que se sube queda estampado con el ownerId de tu bóveda (la mitad izquierda de la referencia compartible ownerId + cid). El acl nace privado: lo que no se marca public a mano no sale del node cuando llegue el modo público, y un blob cifrado no puede marcarse público (nadie sin la llave podría leerlo).

@dotrino/content-client (lo que usa una app)

El cliente de navegador vive en lib/ y se publica desde este mismo repo, igual que dotrino-vault publica su lib. Está aquí a propósito: el protocolo —los nombres de las ops, el tope de 256 KB, el formato de la referencia— es de las dos puntas, y separarlas en dos repos es la forma de que acaben diciendo cosas distintas.

import { ContentClient, buildUrl } from '@dotrino/content-client'

const cc = await ContentClient.connect({ link })   // link del vault: { id, cert, iss }
const ref = await cc.put(bytes, { mime: 'image/png' })   // cifra por defecto
const url = buildUrl(ref)      // https://eco.dotrino.com/#<owner>/<cid>/<llave>
const back = await cc.get(ref) // comprueba el hash antes de devolver nada
  • Cifra por defecto (§4): el node guarda ciphertext y la llave sale en la referencia, para el #fragment. Con encrypt: false queda en claro, que es lo que hace falta para poder marcarlo public y que tenga tarjeta.
  • connect() falla con code: 'no-node' si no tienes ninguno encendido, en vez de esperar. Es lo esperable y la app tiene que saber seguir sin node: al @dotrino/store va lo que debe estar siempre disponible; aquí van los bytes.
  • get() comprueba el hash de lo que llega: el cid es el hash, así que unos bytes que no cuadren se rechazan vengan de donde vengan.
  • Miniaturas (@dotrino/content-client/thumb): makeThumbnail() en canvas y putImageWithThumbnail(), que sube el original cifrado y privado y la miniatura en claro y pública — que es el reparto que hace que haya tarjeta sin publicar el archivo.
  • Lo PÚBLICO de otro usuario ya se lee por la red (fetchPublic en la lib, DISENO §16): un tercero con tu enlace encuentra tu node por el proxio y le pide el cid; el node contesta solo lo marcado público y en claro, y los bytes se verifican contra el cid. Lo privado sigue siendo de tus aparatos.

Tests

npm test

Fases

  1. Core local: blobs por cid, Range/206, índice, cuota+GC.
  2. Aparato del vault (este estado): enrolamiento, plano de control cifrado, owner + acl.
  3. Exposición: P2P/swarm por WebRTC + modo público HTTP opt-in + sembrador 24/7.
  4. Integración con eco (la app que resuelve el #fragment) + catálogo.

Licencia MIT.

Keywords