Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Guía de contribución

Gracias por contribuir a NotesChain.
Gracias por contribuir a Selnote.

## Flujo de ramas

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 NotesChain
Copyright (c) 2026 Selnote

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
<p align="center">
<img src="assets/banner.svg" alt="NotesChain — notas cifradas y sincronización directa" width="100%">
<img src="assets/banner.svg" alt="Selnote — notas cifradas y sincronización directa" width="100%">
</p>

<p align="center">
Expand All@@ -15,14 +15,16 @@
</p>

<p align="center">
<img src="screenshot.png" alt="Interfaz visual de NotesChain" width="600">
<img src="screenshot.png" alt="Interfaz visual de Selnote" width="600">
</p>

---

## Qué hace

NotesChain está pensado para conservar notas y archivos bajo tu control: cifra los datos en el dispositivo, guarda una copia local y replica los cambios directamente entre dispositivos emparejados. La interfaz convierte el conjunto de notas en un mapa visual para navegar el contenido y entender qué dispositivos forman parte del cerebro.
Selnote está pensado para conservar notas y archivos bajo tu control: cifra los datos en el dispositivo, guarda una copia local y replica los cambios directamente entre dispositivos emparejados. La interfaz convierte el conjunto de notas en un mapa visual para navegar el contenido y entender qué dispositivos forman parte del cerebro.

El cambio de nombre a Selnote conserva la base de datos local y el espacio de descubrimiento usados por versiones anteriores. Actualizar la aplicación no migra ni borra las notas existentes, y los dispositivos ya emparejados continúan compartiendo el mismo protocolo.

## Funcionalidades principales

Expand Down
4 changes: 2 additions & 2 deletions assets/banner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/networking.md
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
# Red y despliegue

NotesChain cifra el contenido antes de persistirlo o transferirlo. La red sirve para descubrir pares y transportar bytes cifrados; no reemplaza la verificación criptográfica de la aplicación.
Selnote cifra el contenido antes de persistirlo o transferirlo. La red sirve para descubrir pares y transportar bytes cifrados; no reemplaza la verificación criptográfica de la aplicación.

## Rutas de conexión

Expand All@@ -11,7 +11,7 @@ NotesChain cifra el contenido antes de persistirlo o transferirlo. La red sirve
| TURN | Reenvía tráfico cuando no existe ruta directa | Metadatos de conexión y volumen aproximado; transporta bytes cifrados de extremo a extremo. |
| WebRTC directo | Envía datos entre pares | Los pares conocen sus direcciones candidatas y el flujo de tráfico. |

El cifrado de NotesChain protege el contenido de notas y adjuntos frente a esos intermediarios, pero no elimina la exposición de metadatos de red.
El cifrado de Selnote protege el contenido de notas y adjuntos frente a esos intermediarios, pero no elimina la exposición de metadatos de red.

## GitHub Pages y PWA

Expand Down
2 changes: 1 addition & 1 deletion docs/threat-model.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

## Objetivo

NotesChain busca mantener notas y adjuntos cifrados en el navegador y replicarlos entre dispositivos autorizados. No promete ocultar toda la actividad de red ni recuperar información borrada del almacenamiento del usuario.
Selnote busca mantener notas y adjuntos cifrados en el navegador y replicarlos entre dispositivos autorizados. No promete ocultar toda la actividad de red ni recuperar información borrada del almacenamiento del usuario.

## Activos protegidos

Expand Down
2 changes: 1 addition & 1 deletion icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 18 additions & 13 deletions index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>NotesChain</title>
<title>Selnote</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self' wss:; worker-src 'self';" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%E2%9B%93%3C/text%3E%3C/svg%3E" />
<!-- instalable como app y arranca sin internet (service worker) -->
Expand DownExpand Up@@ -394,7 +394,7 @@
<canvas id="brain"></canvas>

<div id="topbar">
<span class="title">NOTESCHAIN</span>
<span class="title">SELNOTE</span>
<span>rol <b id="role">—</b></span>
<span>neuronas <b id="height">0</b></span>
<span>sinapsis <b id="peers">0</b></span>
Expand DownExpand Up@@ -540,7 +540,7 @@ <h2 id="dead-title">el cerebro murió</h2>

<script type="module">
/* ============================================================
NotesChain v2 — nodo completo cifrado en el navegador.
Selnote v2 — nodo completo cifrado en el navegador.
- E2E: AES-256-GCM sobre el contenido; la llave solo viaja en el
enlace secreto (#fragment — nunca llega a ningún servidor).
- Descubrimiento ciego: el topic P2P se deriva de la llave de
Expand All@@ -556,7 +556,7 @@ <h2 id="dead-title">el cerebro murió</h2>
} from './scripts/p2p-validation.mjs';

const $ = (id) => document.getElementById(id);
const log = (m) => { $('log').textContent = m; console.log('[noteschain]', m); };
const log = (m) => { $('log').textContent = m; console.log('[selnote]', m); };

// Aviso visible en pantalla (la línea de abajo se corta en móvil). Los errores
// salen en rojo, sacuden el dispositivo y vibran si el móvil lo permite.
Expand All@@ -566,7 +566,7 @@ <h2 id="dead-title">el cerebro murió</h2>
el.textContent = msg;
$('toasts').append(el);
setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 400); }, ms);
console[isError ? 'warn' : 'log']('[noteschain]', msg);
console[isError ? 'warn' : 'log']('[selnote]', msg);
if (isError) shake();
}
function shake() {
Expand DownExpand Up@@ -652,6 +652,8 @@ <h2 id="dead-title">el cerebro murió</h2>
// topic de descubrimiento derivado de la LLAVE, no del chainId:
// sin llave no puedes ni anunciarte en el mismo canal.
async topic(keyHex) {
// Espacio de nombres heredado del protocolo: cambiarlo separaría a los
// pares existentes en otro enjambre. Se conserva por compatibilidad.
return (await sha256Hex('noteschain-v2-topic:' + keyHex)).slice(0, 40);
},
};
Expand DownExpand Up@@ -768,6 +770,7 @@ <h2 id="dead-title">el cerebro murió</h2>
/* ---------- IndexedDB ---------- */
const STORES = ['blocks', 'meta', 'blobs'];
const openIdb = () => new Promise((res, rej) => {
// Nombre heredado de la base: se conserva para abrir los datos locales existentes.
const r = indexedDB.open('noteschain-v2', 2);
r.onupgradeneeded = () => {
const db = r.result;
Expand DownExpand Up@@ -1098,14 +1101,14 @@ <h2 id="dead-title">el cerebro murió</h2>
// La sentencia de muerte se valida por FIRMA del owner, no por su posición en
// la cadena: así un lector atrasado (iOS suspendido) también la puede aceptar.
async function verifyDestroy(block, ownerPubKey) {
if (!block || block.type !== 'DESTROY') { console.warn('[noteschain] destroy: tipo inválido', block?.type); return false; }
if (!block || block.type !== 'DESTROY') { console.warn('[selnote] destroy: tipo inválido', block?.type); return false; }
const h = await hashBlock(block);
if (h !== block.hash) { console.warn('[noteschain] destroy: hash no coincide', h, block.hash, JSON.stringify(block.payload)); return false; }
if (h !== block.hash) { console.warn('[selnote] destroy: hash no coincide', h, block.hash, JSON.stringify(block.payload)); return false; }
try {
const ok = await crypto.subtle.verify('Ed25519', await ownerVerifyKey(ownerPubKey), hexBytes(block.signature), enc(block.hash));
if (!ok) console.warn('[noteschain] destroy: firma no válida para owner', ownerPubKey.slice(0, 8));
if (!ok) console.warn('[selnote] destroy: firma no válida para owner', ownerPubKey.slice(0, 8));
return ok;
} catch (e) { console.warn('[noteschain] destroy: excepción', e.message); return false; }
} catch (e) { console.warn('[selnote] destroy: excepción', e.message); return false; }
}

// El cerebro murió: explosión, aviso, desconexión total y borrado de la
Expand DownExpand Up@@ -1488,6 +1491,8 @@ <h2 id="dead-title">el cerebro murió</h2>
// El primero es NUESTRO (Cloudflare Worker propio) — rápido y siempre disponible;
// los públicos quedan de respaldo. Ninguno ve datos: solo el apretón de manos.
const TRACKERS = [
// Host heredado de infraestructura: se conserva hasta migrar la señalización;
// identifica un endpoint compatible, no la marca visible de la aplicación.
'wss://noteschain-tracker.onilabs-dev.workers.dev',
'wss://tracker.openwebtorrent.com',
'wss://tracker.webtorrent.dev',
Expand DownExpand Up@@ -1687,7 +1692,7 @@ <h2 id="dead-title">el cerebro murió</h2>
// diagnóstico visible: sin esto un fallo de ICE era indistinguible de "aún negociando"
function iceLog(pc, tag) {
pc.oniceconnectionstatechange = () => {
console.log('[noteschain] ICE', tag, pc.iceConnectionState);
console.log('[selnote] ICE', tag, pc.iceConnectionState);
if (pc.iceConnectionState === 'failed') chip('pair-fail', 'sin ruta al dispositivo (ICE)', true);
};
}
Expand DownExpand Up@@ -1776,7 +1781,7 @@ <h2 id="dead-title">el cerebro murió</h2>
// Los mensajes de cadena se procesan EN SERIE: evita la carrera donde dos
// NEW_BLOCK simultáneos pasaban la validación a la vez y duplicaban bloques.
let rxSeq = Promise.resolve();
const enqueue = (fn) => { rxSeq = rxSeq.then(fn).catch((e) => console.warn('[noteschain]', e)); };
const enqueue = (fn) => { rxSeq = rxSeq.then(fn).catch((e) => console.warn('[selnote]', e)); };

// chips de estado (dispositivos emparejando / en línea)
function chip(id, text, pulse) {
Expand DownExpand Up@@ -1844,7 +1849,7 @@ <h2 id="dead-title">el cerebro murió</h2>
if (tombstone) {
try {
dc.send(JSON.stringify({ t: 'DESTROYED', block: tombstone.block }));
console.log('[noteschain] sentencia enviada a', id.slice(0, 4));
console.log('[selnote] sentencia enviada a', id.slice(0, 4));
tombKilled++;
paintTomb();
saveTomb();
Expand DownExpand Up@@ -3350,7 +3355,7 @@ <h2 id="dead-title">el cerebro murió</h2>
// anterior), se descarta y se resincroniza desde la red — nada se pierde,
// la cadena buena vive en tus otros dispositivos.
if (blocks.length && (await verifyChain(blocks, meta.chainId))) {
console.warn('[noteschain] réplica local corrupta — descartada, resincronizando');
console.warn('[selnote] réplica local corrupta — descartada, resincronizando');
blocks = [];
await dbWrite((tx) => tx.objectStore('blocks').clear());
}
Expand Down
4 changes: 2 additions & 2 deletions manifest.webmanifest
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"name": "NotesChain",
"short_name": "NotesChain",
"name": "Selnote",
"short_name": "Selnote",
"description": "Notas cifradas y sincronización directa entre dispositivos.",
"start_url": "./",
"scope": "./",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
{
"name": "noteschain-web",
"name": "selnote-web",
"private": true,
"version": "0.0.0",
"description": "Repository governance and offline validation scripts for NotesChain.",
"description": "Repository governance and offline validation scripts for Selnote.",
"type": "module",
"scripts": {
"check": "node scripts/check-repository.mjs",
Expand Down
2 changes: 1 addition & 1 deletion sw.js
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
const CACHE_NAME = 'noteschain-shell-v1';
const CACHE_NAME = 'selnote-shell-v1';
const APP_SHELL = [
'./',
'./index.html',
Expand Down
2 changes: 1 addition & 1 deletion tests/check-repository.test.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ const requiredDocuments = [
];

function repositoryFixture(html = '<script>const valid = true;</script><img src="assets/logo.svg">') {
const root = mkdtempSync(join(tmpdir(), 'noteschain-check-'));
const root = mkdtempSync(join(tmpdir(), 'selnote-check-'));
for (const file of requiredDocuments) {
const pathname = join(root, file);
mkdirSync(join(pathname, '..'), { recursive: true });
Expand Down