Base de aplicación: estado implementado¶
Estado: registro actualizado el 23 de septiembre de 2026; los resultados de aceptación anteriores conservan su alcance original. No equivale a una release ni a una auditoría de seguridad. Esta página actualiza las propuestas anteriores para la capa de cliente.
Arquitectura actual¶
CLI ────────────────────────────────┐
Flutter → puente Rust ─┴→ arveil-app → arveil-core
│
└→ transporte Noise/WebSocket → relay Go
arveil-app coordina operaciones y devuelve resultados estructurados. arveil-core conserva identidad, MLS, persistencia y primitivas de entrega. El relay sigue siendo un proceso Go independiente; no contiene las claves E2EE de los clientes. El cliente Flutter abre perfiles cifrados, da de alta por invitación, vincula dispositivos y exporta/restaura kits cifrados de identidad mediante el puente. La interfaz permite crear conversaciones (verificar a quienes participan es opcional y puede hacerse después), leer historial paginado, enviar texto sin conexión y sincronizar.
Cambios realizados y evidencia¶
| Cambio | Implementación y comprobación |
|---|---|
| Extracción del chat de la CLI | arveil-app contiene conversaciones, envío, sincronización, revocaciones y adjuntos. chat.rs adapta argumentos y presenta resultados. |
| Contrato de operaciones | ClientCommand, CommandOutput, ApplicationError, StateChange y MessageReceipt. Los errores conservan partial_result(); la aceptación local se registra después del commit. No se deducen categorías de error del texto. |
| Correlación de entregas | Delivery::pending incluye event_id. El cursor avanza con MAX(actual, nuevo). |
| Configuración explícita del perfil | ProfileConfig aporta ruta, clave, autoridad de TLS y caducidades; la biblioteca no lee ninguna variable de entorno. La CLI traduce las suyas. Debug oculta la clave, y una clave mal formada se rechaza antes de crear nada. |
| Vida de la sesión | Una segunda apertura independiente de la misma ruta canónica devuelve AlreadyOpen, sea cual sea la clave que aporte; compartir consiste en clonar el handle. open abre la base, así que una clave incorrecta falla ahí y no en el primer comando. close deja de admitir trabajo, espera al que corre y une el hilo trabajador, que es quien posee el bloqueo; abandonar el último handle sigue el mismo camino. |
| El alta se reanuda | Una inscripción por perfil, con su fase escrita según cada paso se vuelve durable y la invitación guardada como hash y no como token. Repetir la misma inscripción continúa donde se quedó y conserva una identidad, un buzón y una ruta; otro realm u otra invitación se rechazan y dejan intacta la inscripción registrada. |
| Canjear una invitación dos veces | El relay registra lo que produjo un canje —token, identidad y credencial— dentro de la misma transacción que consume el uso, y responde a la repetición de esa terna exacta con el resultado que registró, no con un conflicto. Otra credencial para la misma identidad sigue siendo conflicto: la igualdad de un hash no es autorización. Una repetición no consume otro uso, y una base escrita por un relay más nuevo se rechaza antes de modificar nada. La creación del buzón ya reutiliza la petición y las capabilities persistidas. El lote inicial de KeyPackages y su estado privado MLS se confirman juntos antes de publicarlos; una respuesta perdida reenvía los mismos bytes, y completar el alta impide generar otro lote. El relay aplica el cupo después de deduplicar y nunca reactiva paquetes consumidos. |
| La clave del perfil pertenece a la plataforma | 32 bytes aleatorios del sistema, generados en Rust y guardados por Keychain o Keystore, sin sincronizar. iOS/Android usan protección ligada al dispositivo; macOS utiliza el llavero clásico y su control de acceso por app. Nunca derivados de una frase. Un perfil cuya clave desapareció se informa, nunca se le da una nueva: eso respondería «aquí no hay nada» a quien tiene su historial en disco. Android rechaza copia en nube y transferencia entre dispositivos; Apple marca el directorio del perfil como excluido en cada arranque. Un acceso denegado al almacén se informa sin recurrir a claves en texto plano. |
| Un pánico termina su sesión | Un comando que entra en pánico se contiene en el límite: quien llamó recibe un fallo tipado con el nombre de la operación, lo que estuviera encolado detrás se responde en lugar de quedarse esperando, y nada más se ejecuta en esa sesión. Una transacción interrumpida por el desenrollado revierte, porque unit_of_work ahora la cierra desde Drop. El perfil queda intacto en disco y vuelve a abrirse tras cerrar la sesión. Esto vale donde la compilación desenrolla; una que aborte en pánico termina el proceso y ningún contrato sobrevive a eso. |
| Progreso durante el trabajo | Una proyección acotada llega a quien observa según se registra cada cambio, no al responder la operación: mensaje encolado y recibido, publicación, estado de entrega, transferencias, sincronización, emparejamiento y pasos del alta. Quien se queda atrás pierde eventos y se le dice cuántos, para que relea en vez de fiarse de una vista parcial; el resultado durable sigue llevándolo todo. |
| Historial paginado | QueryHistoryPage recibe conversación, cursor y límite acotado; los identificadores solo crecen, así que una página no se desplaza cuando llegan eventos mientras alguien lee hacia atrás. Los resúmenes leen un recuento y la fila más reciente en lugar de todos los cuerpos. Las lecturas locales ya no exigen un realm inscrito. |
| Admisión acotada | El trabajo se cuenta por tipo: dos sincronizaciones, treinta y dos mutaciones y ciento veintiocho consultas. Más allá, el comando se rechaza con un Busy tipado que no empezó nada; los huecos se liberan cuando el trabajo termina, no cuando quien llamó se marcha. Las consultas tienen sitio propio y responden mientras las sincronizaciones están saturadas. |
| Ejecutor por perfil | Application comparte ejecutor por ruta canónica. Un runtime de una hebra multiplexa futuros durante la red; los tramos síncronos de MLS/SQLite no se intercalan. Los eventos usan contexto por operación. La API pública de llamada sigue siendo bloqueante. |
| Exclusión por operación | Sincronización, consulta de KeyPackages por red y reposición comparten una exclusión por perfil. CompleteLink y ConfirmPairing comparten otra exclusión, para evitar finalizadores simultáneos. Las consultas pueden avanzar durante esperas de red. |
| Exclusión transaccional | SharedConn::unit_of_work mantiene un mutex reentrante durante toda la transacción; los callbacks de almacenamiento MLS pueden utilizar la misma conexión. Client.conn es privado. |
| Transporte con límites de tiempo | carrier.rs limita conexión, handshake, petición y cierre. Un timeout de petición elimina el socket y el estado Noise. Se exige reconexión. |
| Descargas recuperables | Un error de transporte conserva file-pending y el archivo .part; una sincronización posterior puede reanudar. No se convierte ese fallo transitorio en indisponibilidad definitiva. |
| Alta y vinculación reutilizables | onboarding.rs contiene identidad, inscripción, grants y emparejamiento. link.rs es presentación. |
| Emparejamiento explícito | Inicio, espera, aprobación, consulta, confirmación y cancelación identifican la sesión. Se comprueban código y caducidad antes de iniciar la finalización. Cancelar tras el punto de compromiso devuelve AlreadyCommitted. |
| Finalización reanudable | Grant directo y confirmación comparten complete_device_link. Las fases persistidas avanzan desde Committing hasta Complete; se valida la identidad del grant de reintento. Las pruebas cubren fallo inicial de red, éxito posterior y confirmaciones concurrentes con un solo buzón/ruta. |
| Exclusión entre procesos | ProfileGuard y Application adquieren un bloqueo del SO sobre .arveil-profile.lock, después de canonicalizar el directorio. La CLI protege también comandos legacy. Otro proceso recibe ProfileInUse; el archivo de bloqueo no se elimina para liberar el lock. |
El bloqueo entre procesos permite alternar GUI y CLI sobre el mismo perfil. No permite que ambos procesos lo utilicen simultáneamente. Un acceso simultáneo futuro requeriría un propietario único con IPC, fuera del plan inicial.
Los enlaces a código siguen main del repositorio; este registro local debe integrarse en el mismo PR y merge que los cambios de código correspondientes, o después de ellos. No publicar primero un PR solo documental con enlaces a archivos todavía ausentes en main: Pages se despliega independientemente y MkDocs estricto no comprueba destinos externos. Antes de publicar, verificar que todas las rutas enlazadas existen en el commit de destino; una referencia SHA/tag solo sirve si ya está publicada y contiene esos archivos.
Evidencia de revisión¶
La ejecución original de la base con cargo test --workspace --locked terminó con 72 pruebas correctas (incluida una prueba auxiliar de procesos) y una ignorada; demo, interop, q3-capture y las fases 1–4 también se ejecutaron en local. La aceptación de M3b.0 se ejecutó sobre el propio sistema en macOS y en un emulador Android (Android 15, API 35, arm64); todavía sin teléfono físico. La matriz de plataformas recoge el toolchain fijado y los comandos. git diff --check pasó. Es un resultado del checkout local en ese momento, no una afirmación sobre todas las plataformas o la CI remota.
Pruebas destacadas:
overlapping_pairing_confirmations_share_one_mailbox_and_routeydirect_grant_completion_resumes_after_network_failure, enarveil-app/src/lib.rs.late_response_cannot_contaminate_a_second_request, enarveil-app/src/carrier.rs.- Bloqueo entre procesos: cierre normal, terminación abrupta, perfiles distintos y alias simbólicos en Unix.
- Protección de la CLI legacy.
El implementador informó además de Clippy y fases 1–4 correctos durante las iteraciones. La última revisión no volvió a ejecutar esas comprobaciones; antes de publicar debe registrarse una ejecución de aceptación contra un commit concreto.
Límites que permanecen¶
- El cliente gráfico abre perfiles cifrados y permite el alta por invitación, su reintento y la consulta del avance al reabrir. Emparejamiento y kit de identidad ya tienen interfaz. Conversaciones, contactos, adjuntos y gestión de dispositivos están implementados; la exportación/importación del historial cifrado también está implementada. Existe empaquetado experimental ZIP/APK; falta aceptación en un móvil físico.
- Solo la CLI lee ya variables de entorno, y sigue eligiendo perfil sin cifrar cuando no hay clave. El almacén seguro se ha probado en emulador Android y en macOS con firma ad hoc y llavero clásico. Quedan pendientes teléfono físico y una instalación nueva descargada.
- El puente Rust ejecuta las llamadas bloqueantes fuera del hilo de interfaz y expone un flujo incremental de eventos. Siguen pendientes la cancelación general de operaciones y la aceptación completa del ciclo de vida de cada plataforma.
- Algunos eventos de archivos y membresía necesitan identificadores adicionales para actualizar elementos concretos de la UI. El progreso es una proyección: los cambios que no modela solo llegan en el resultado durable.
- Recuperación de un grupo MLS desincronizado no es sinónimo de
sync; el método ficticiorecover_conversationfue retirado. Sigue pendiente un flujo real. - La sucesión del coordinador depende de revocaciones verificadas; no es una elección automática ante una desconexión.
- El relay aplicaba sus pragmas una vez, así que solo la conexión que los ejecutó tenía tiempo de espera o exigía claves foráneas; ahora van en la cadena de conexión, y una prueba sostiene varias conexiones y comprueba cada una. Las transacciones de escritura también reservan el turno antes de leer (
BEGIN IMMEDIATE), evitando el fallo al pasar de lectura a escritura durante la limpieza u otras peticiones; las lecturas WAL siguen siendo concurrentes. Véase la regresión de concurrencia. El tamaño del pool sigue sin acotar y queda pendiente. - La CLI del kit de identidad ya usa el servicio de aplicación. Archivos y contactos conservan comandos legacy que habrá que exponer por esa capa si la GUI los necesita.
Siguiente fase: plan Flutter. Decisión: ADR-009.
Alta por invitación¶
El formulario mantiene la invitación solo en memoria y la borra al completar el alta o cerrar el perfil. El ejecutor responde con una consulta tipada del estado al abrir y tras los intentos de alta. Los datos de relay/invitación mal formados se rechazan antes de crear la identidad. La interfaz muestra categorías de error sin interpolar rutas ni diagnósticos remotos. Las pruebas cubren reintentos, envíos duplicados, cierre de aperturas tardías y mensajes sin detalles privados. Es la parte de alta por invitación de M3b.2. Los cambios de emparejamiento y kit descritos abajo no cierran el hito: faltan aceptación en dispositivos físicos y comprobaciones de los diálogos nativos. La implementación de KeyPackages se registra más abajo.
Emparejamiento y kit de identidad (23 de septiembre de 2026)¶
El alta ofrece invitación, vinculación y restauración. El dispositivo nuevo compara manualmente el código corto antes de aplicar su autorización; un código incorrecto impide finalizar. El dispositivo nuevo escucha mientras su código es válido (la vida de la cita en el relay, diez minutos por defecto), porque el código viaja a mano hasta el administrador. El código tiene un botón «Copiar código». Salir de la app para enviarlo es lo normal: al abrir la pantalla de vinculación o al volver la app a primer plano, un código todavía válido se vuelve a escuchar sin preguntar. Si esa espera se interrumpe antes de recibir la comparación, «Seguir esperando» también la retoma a mano; una interrupción a mitad del intercambio sigue exigiendo cancelar y generar otro código. La comparación recibida sobrevive a la reapertura; tras confirmar, la finalización retoma el mismo dispositivo y buzón. Cancelar solo detiene el alta local: no revoca una autorización ya emitida por el administrador. Su pantalla lo explica antes de autorizar y muestra después la comparación. También ofrece «Copiar datos del servidor», el bootstrap que el dispositivo nuevo pide primero y que, si no, solo muestra el log del relay. La espera del administrador está acotada a 90 segundos, y la pantalla indica volver mientras tanto a Arveil en el dispositivo nuevo; todavía no dispone de cancelación.
La exportación guarda solo el archivo cifrado mediante el diálogo del sistema. Su clave separada aparece únicamente tras guardar, desaparece al salir o pasar a segundo plano y Arveil no la conserva. Posponer el kit muestra el riesgo de pérdida de identidad. Restaurar exige perfil vacío, kit, clave y bootstrap del relay original, con confirmación de revocación y ausencia del historial anterior. Usa el kit más reciente y expórtalo de nuevo tras cambiar dispositivos. Un kit antiguo puede rechazarse si el relay conoce un manifiesto posterior. Recuperar identidad no restaura historial ni estado de grupos MLS.
CLI y GUI comparten preparación local atómica y un registro durable. Un error de transporte conserva la misma credencial y permite reanudar sin volver a importar el archivo. El relay guarda la petición firmada exacta y la secuencia previa en una sola transacción: acepta su repetición autenticada y rechaza credenciales cambiadas, caducadas o revocadas. Requiere el relay actualizado (esquema 4); los anteriores no garantizan el reintento tras perder la respuesta de recuperación. Haz una copia antes de actualizar: una versión anterior rechaza el esquema nuevo. Las advertencias de retroceso persisten al reabrir.
Las pruebas cubren consentimiento, comparación incorrecta/caducada, cancelación durante la espera, exportación cancelada, eliminación de la clave al pasar a segundo plano, rollback de la preparación y reintentos tras reiniciar cliente y relay. La reproducción nativa está en el README de Flutter. Estos cambios no publican ni reemplazan el candidato alfa existente.
Comprobaciones de este cambio: 87 tests Rust pasan (uno ignorado), Go con detector de carreras, análisis Flutter y 16 pruebas de widgets/unidad, aceptación nativa macOS y emulador Android, fases 2, 3 y 3b y documentación bilingüe en modo estricto. Son resultados locales; la matriz de plataformas delimita su alcance.
Disponibilidad y reposición de KeyPackages (23 de septiembre de 2026)¶
El perfil muestra una consulta fechada de las claves de un solo uso disponibles en el relay. Abrir el perfil lee el estado local sin consultar la red; un dato desconocido se distingue de cero. Si falla la actualización, se conserva el último dato con una advertencia visible. El agotamiento impide que otros dispositivos inicien conversaciones nuevas con este dispositivo; las conversaciones existentes conservan sus propias claves.
Cuando quedan tres paquetes o menos, la reposición prepara los necesarios para llegar a diez. Los bytes públicos del lote y su estado privado MLS se confirman juntos antes de publicar. La sincronización CLI y la GUI comparten ese registro y serializan las operaciones de red. Tras perder una respuesta, reabrir y reintentar envía el mismo lote; el relay no reactiva claves consumidas. Si todo el lote se consumió antes del reintento, la confirmación elimina el registro pendiente y otra reposición explícita puede generar claves nuevas. La interfaz consulta de nuevo tras publicar para mostrar el recuento del relay, teniendo en cuenta que otra persona puede consumir claves mientras tanto.
Comprobaciones actuales: 89 pruebas Rust pasan (una ignorada), Clippy, 20 pruebas
Flutter de widgets/unidad y aceptación nativa macOS y emulador Android. La fase 4 local pasó,
incluyendo consumo real por grupos MLS y reposición CLI; omitió la compilación
Docker al no estar disponible. La matriz de plataformas
delimita la prueba GUI con consumo simulado y la evidencia por plataforma.
Siguen pendientes teléfono físico y diálogos nativos. La versión fuente es
0.1.0+4; el candidato alfa existente no cambia.
Interfaz de conversaciones (M3b.3)¶
El cliente fuente 0.1.0+5 incorpora lista/detalle adaptable, compartir la ruta de
este dispositivo explícitamente, comparación del número de seguridad de cada
contacto, creación verificada de grupos, historial paginado y composición de texto.
Editar las rutas invalida la comparación. Rust valida tamaños, dispositivos
repetidos y los números exactos antes de verificar los contactos. Se utiliza el
protocolo de identidad/MLS existente; no equivale a una auditoría independiente.
Desde el 27 de septiembre de 2026 la comparación es opcional; véase «Conversar
antes de verificar» más abajo.
QueueMessage confirma estado MLS, evento y outbox cifrado antes de devolver el
recibo, sin acceder a la red. Flutter borra solo el borrador aceptado y sincroniza
los sobres guardados. Si la creación falla después del commit, el bridge devuelve
el grupo guardado con un aviso para no crearlo otra vez. La CLI comparte la misma
transacción local de envío. El texto admite hasta 32 KiB de UTF-8.
El historial responde durante una sincronización. La pantalla relee las proyecciones con el progreso y al completar operaciones; en primer plano sincroniza cada diez segundos y al reanudarse, además del reintento manual. No hay push ni entrega en segundo plano. La actualización reconcilia el historial mostrado en páginas de 50, conserva las páginas anteriores que abrió el usuario y se detiene si cambia de pantalla o selección. Se distingue almacenamiento local, aceptación del relay, entrega no disponible y recepción en este dispositivo. No se afirma lectura humana, autor autenticado ni hora del mensaje. Los nombres de contactos y las acciones de adjuntos se implementan en M3b.4 más abajo; la gestión de miembros queda para entregas posteriores.
Las regresiones cubren comparación simétrica y confirmación atómica de contactos, encolado/reapertura sin relay, resultados del bridge tras commit, historial durante red bloqueada, esperas compartidas de sincronización, cancelación del observador antes de su arranque, selecciones tardías, paginación con nuevos mensajes, invalidación de rutas editadas, texto recibido, conservación de borradores y doble envío. Las pruebas de ciclo de vida cubren el paso a segundo plano durante el arranque y la conservación del controlador y borrador cuando Flutter reconstruye la ruta. La matriz de plataformas detalla el alcance nativo y el README Flutter aporta el comando reproducible con un relay aislado.
Contactos locales y selección de destinatarios (primera entrega de M3b.4)¶
El cliente fuente 0.1.0+6 incorpora agenda, alias locales, verificación explícita
del número de seguridad y creación desde contactos guardados. Las conversaciones
muestran nombres y una lista de participantes con identidad, dispositivo y estado
de verificación/revocación. Estos nombres no autentican al autor de cada mensaje
ni se sincronizan con otros perfiles.
Las rutas, incluidas sus capabilities de buzón, se guardan en la tabla nueva
contact_routes dentro del perfil cifrado de la GUI. Las listas solo exponen
identificadores y revocación, sin capabilities. Guardar, renombrar y verificar
pasan por el ejecutor del perfil; contacto, alias, ruta y verificación opcional
se confirman juntos. Editar una ruta invalida la comparación de la UI. Importar
el mismo dispositivo actualiza su ruta sin duplicarlo; un nombre vacío al
importar conserva el alias existente, mientras que renombrar permite borrarlo.
La creación recibe identificadores guardados de identidad/dispositivo. Rust relee sus rutas y exige correspondencia de identidad, raíz y dispositivo, dispositivos distintos y ninguna revocación local conocida antes de crear por red. Hasta el 27 de septiembre de 2026 también exigía contactos verificados; véase «Conversar antes de verificar» más abajo. Se mantiene el recibo con aviso tras commit para evitar que un fallo de publicación invite a crear de nuevo el grupo guardado. Admite hasta 16 dispositivos; revocaciones aún desconocidas y rutas caducadas siguen dependiendo de la sincronización y validación del relay.
Los contactos aprendidos en conversaciones se pueden nombrar y verificar; si no tienen una ruta guardada hay que importar una antes de seleccionarlos. Alias y verificación sobreviven a la reapertura. El asistente de aceptación de conversaciones recorre ahora este flujo antes de texto bidireccional, modo sin red, reconexión y paginación. La entrega de historial y pérdida/recuperación se describe más abajo; sigue abierta la aceptación en dispositivo físico.
Adjuntos explícitos (segunda entrega de M3b.4)¶
El código fuente 0.1.0+7 añade selección de archivo, confirmación, cola local
persistente, descarga explícita, progreso, cancelación y exportación explícita.
El límite son 25 MiB incluido el tag de cifrado de 16 bytes (26.214.384 bytes
del archivo original). Cada archivo se identifica por conversación y event_id:
los nombres iguales no seleccionan ni sobrescriben otra copia privada. Los
selectores del sistema conceden acceso al origen/destino elegido; sus rutas y
URI no se guardan con el mensaje.
Android lee directamente el documento seleccionado mediante el
Storage Access Framework,
en memoria acotada y desde un hilo de trabajo, sin la caché en claro del selector
general ni permisos URI persistentes. El límite se comprueba durante la lectura
aunque el proveedor omita o declare mal el tamaño. macOS lee el archivo elegido
por bloques. El proveedor puede conservar su propia copia; Arveil no controla
el almacenamiento de ese proveedor.
La GUI activa la gestión manual de adjuntos. Sincronizar recibe descriptores, pero nunca descarga automáticamente sus blobs. Descriptores, estados y bloques cifrados viven en tablas nuevas de la misma conexión SQLCipher que los eventos y MLS. La GUI no escribe descargas descifradas. Exportar verifica tamaño, hash y autenticación AEAD antes de entregar bytes al diálogo del sistema. El puente elimina el cuerpo de los eventos de archivo: ni capabilities, claves de archivo ni rutas antiguas del equipo llegan a los widgets del historial.
La cola reserva un único evento persistente antes de usar la red. La subida reanuda desde el offset del relay; perder un acuse o reabrir la app no crea otro mensaje. La transacción final de envío MLS confirma conjuntamente evento, outbox y finalización. Después solo se resincroniza el mensaje guardado. Las descargas conservan bloques cifrados contiguos y verifican el archivo entero antes de permitir exportarlo. Solo corre una transferencia por perfil; consulta del historial y cancelación siguen respondiendo mientras espera a la red. Los archivos interrumpidos requieren una reanudación explícita.
Cancelar se comprueba después de cada espera de red e impide posteriores escrituras locales o confirmar el mensaje. Elimina los datos locales incompletos. No retira un mensaje ya confirmado ni borra inmediatamente bytes que llegaron al relay: su limpieza sigue la política existente de caducidad. Una descarga cancelada se puede solicitar de nuevo. Un 410 se presenta como caducado; un 403 como no disponible/acceso rechazado, sin atribuirlo a caducidad. Ninguna acción renueva capabilities. Los archivos descargados antes por la CLI quedan fuera de este flujo gestionado.
Las regresiones Rust cubren pérdida de acuses, descarga interrumpida y reapertura cifrada, cancelación durante una petición, nombres iguales, ámbito de conversación, tamaños y autenticación inválidos, caducidad y denegación. Los widgets comprueban confirmaciones, reintento del mismo evento, cancelación al guardar, cambio de conversación durante la selección y presentación móvil. El escenario nativo usa dos perfiles y un relay desechable, con sustitutos de selectores en memoria. Los diálogos reales del sistema, el teléfono físico y los paquetes de release en apps distintas siguen teniendo aceptación separada. Las pruebas JVM de Android también cubren entrada vacía, tamaño exacto al límite, flujo demasiado grande sin longitud conocida y cancelación antes de leer.
Dispositivos propios y revocación reanudable (tercera entrega de M3b.4)¶
El código fuente 0.1.0+8 añade Gestionar dispositivos al perfil. El
inventario local identifica el dispositivo actual, la autoridad del administrador
y las revocaciones conocidas. Un perfil vinculado o recuperado puede conocer
credenciales sin sus identificadores de dispositivo; la pantalla cuenta esas
entradas desconocidas y avisa de que el inventario es parcial. Muestra la versión
local del manifiesto, no presencia en línea ni el estado actual del relay.
Solo el administrador puede revocar otro dispositivo propio conocido. La confirmación muestra su identificador completo y explica la permanencia del cambio, la aplicación diferida en el relay y que las copias/historial existentes no se borran. Rust rechaza revocar el dispositivo actual, identificadores desconocidos y operaciones sin la autoridad raíz. Una solicitud antigua de vinculación no puede reautorizar un dispositivo conocido como revocado; hay que vincular un perfil nuevo con claves de dispositivo nuevas.
El manifiesto, las marcas de revocación y el registro de progreso se guardan
en una transacción. Reintentar reutiliza la revocación existente sin firmar otra
versión por haber perdido una respuesta. Cada conversación registra su aviso
junto al estado MLS y los sobres cifrados de salida. El coordinador autorizado
retira la hoja localmente; otros coordinadores deben aplicar la retirada por
separado (sigue disponible chat remove en la CLI). La sincronización normal
reanuda revocaciones confirmadas, publica el manifiesto más reciente antes que
los sobres y reutiliza bytes e identificadores de entrega. Sincronización y
revocación se excluyen entre sí; las consultas locales siguen respondiendo.
La interfaz distingue revocación local, aceptación del relay, conversaciones que aún mantienen la hoja, avisos en cola, rechazados/caducados y falta de rutas. La aceptación del relay no confirma recepción por los participantes. Las rutas faltantes se informan, no se reparan automáticamente. Las hojas revocadas conocidas bloquean nuevos envíos hasta retirarlas; un participante sin conexión puede no conocer todavía la revocación. No se implementan borrado remoto, recuperación automática, importación del historial ni rejoin MLS.
Las regresiones Rust cubren respuestas de manifiesto/sobres perdidas, reapertura
cifrada, rechazo de autoridad/destino, rollback de transacciones fallidas y una
vinculación posterior mientras queda publicación pendiente. Los tests de widgets
cubren confirmación/cancelación, inventario parcial, errores sin datos privados,
reintento mediante sincronización y cierre durante la espera. El escenario nativo
devices vincula perfiles desechables, revoca sin conexión desde la interfaz,
reabre, sincroniza, comprueba el rechazo del dispositivo revocado e intercambia
texto con otro participante tras la retirada MLS. Los resultados por plataforma
se registran aparte en PLATFORMS.
El relay de esta entrega guarda el manifiesto firmado y revoca credenciales y capabilities en una sola transacción. Acepta de nuevo el último manifiesto idéntico tras perderse una respuesta; sigue rechazando otro contenido en la misma versión o una versión anterior. Las regresiones cubren reinicio, rollback y publicación parcial de un relay antiguo. Actualiza el relay junto con este cliente: versiones anteriores rechazan el manifiesto repetido con 409 y no completan este camino de reintento tras una respuesta perdida.
Historial cifrado y recuperación tras pérdida (cuarta entrega de M3b.4)¶
El código 0.1.0+9 añade Historial cifrado al perfil. CLI y GUI comparten
Application::export_archive / import_archive; el bridge expone páginas
acotadas de solo lectura y la exportación explícita de adjuntos. Cada archivo
age tiene una clave nueva e independiente. No contiene claves privadas de
identidad/dispositivo ni estado MLS activo. La pantalla muestra la clave tras
guardar el archivo y la oculta al salir o pasar a segundo plano, sin copiarla
automáticamente al portapapeles.
Se exportan registros activos e importados. Se comprueba la autenticidad de los adjuntos gestionados disponibles antes de incluirlos. Los pendientes, cancelados, caducados y las descargas antiguas de la CLI solo llevan metadatos: no se descarga del relay ni se lee una ruta guardada en un mensaje. La importación sanea nombres y descarta descriptores/rutas antiguos. Los adjuntos importados, incluidos archivos vacíos, permanecen en SQLCipher hasta guardar explícitamente una copia fuera.
El archivo debe pertenecer a la misma identidad: tras una pérdida, restaura antes
su kit. Se valida entero antes de guardar registros y archivos en una transacción.
Los registros existentes con el mismo (group_id, event_id) se conservan sin
cambios y cuentan como duplicados. No se crean mensajes activos, outbox ni estado
MLS. La pantalla paginada no permite enviar; un archivo importado no demuestra la
autoría ni la entrega del texto. La dirección incluye mensajes encolados localmente;
el archivo no conserva acuses de entrega. La reexportación conserva registros y
adjuntos importados.
Límites: entrada cifrada de 64 MiB, 10.000 registros por archivo, texto de 1 MiB,
adjuntos por debajo de 25 MiB y presupuesto conservador de 48 MiB para exportar.
Una exportación demasiado grande falla completa; no se recorta ni divide sola.
Se leen archivos v1; el campo opcional file_present distingue los archivos
vacíos disponibles en exportaciones nuevas. Un archivo antiguo con bytes vacíos
no permite saber si faltaba la copia o si existía un archivo vacío.
Las pruebas Rust cubren otra clave local, identidad/clave incorrectas, alteración,
rollback, duplicados, paginación, reexportación y rechazo de adjuntos corruptos.
Los widgets cubren consentimiento, cancelación, ocultación de secretos, lecturas
acotadas y errores sin datos privados. El escenario nativo archives destruye un
perfil desechable, recupera su identidad con una nueva clave del almacén nativo,
importa desde memoria, reabre, comprueba que no reenvía ni reincorpora al grupo y
envía texto por una conversación nueva creada expresamente. No exporta datos
reales. Consulta la evidencia de plataformas para las ejecuciones.
Versionado del esquema del perfil (25 de septiembre de 2026)¶
La base del perfil guarda la versión de su esquema en PRAGMA user_version, y
arveil_core::schema aplica migraciones ordenadas al abrir una conexión. Antes,
cada apertura volvía a ejecutar CREATE TABLE IF NOT EXISTS. Eso añade tablas
nuevas pero no puede cambiar una existente: la columna contacts.name de la
fase 4 se añadió editando el texto de la tabla, así que un perfil anterior no la
tenía.
- Un perfil sin versión, como los que escribe cualquier build hasta
0.1.0+11, es la versión 0. La migración 1 añade las tablas base que falten, añadecontacts.namesi no existe y compara las columnas de cada tabla con la base. Cualquier otra diferencia se rechaza como perfil de desarrollo no soportado, y el rollback deja sus tablas y filas como estaban. - Cada migración se confirma junto con la versión que fija. Un fallo conserva la versión anterior y la siguiente apertura continúa desde ahí.
- Un perfil con una versión más reciente se rechaza antes de que la conexión
escriba nada, incluido el pragma del modo de diario. La aplicación devuelve
ProfileTooNew; la GUI pide actualizar la app e indica que el perfil no se ha modificado. Una clave incorrecta se sigue informando como antes. - Las builds hasta
0.1.0+11no leen la versión. La versión 1 no cambia ninguna tabla existente salvo esa reparación, así que siguen abriendo un perfil adoptado, pero una migración posterior puede no ser compatible con ellas. No instales una app más antigua sobre una más nueva; Android ya rechaza un número de build menor. - Los textos base (
MLS_SCHEMA,CLIENT_SCHEMA,DELIVERY_SCHEMA) quedan congelados. Un cambio de esquema es una migración nueva al final de la lista, con una prueba que parte de la versión anterior.
Evidencia: las pruebas schema de arveil-core cubren perfiles nuevos,
adoptados, reparados, no soportados, más recientes (sin cifrar y con SQLCipher),
con versión negativa, con una migración interrumpida y reanudada, y reabiertos.
arveil-app comprueba que Application::open rechaza un perfil más reciente y
lo libera, y una prueba de Flutter comprueba el mensaje. Una ejecución local usó
binarios reales de main para crear un perfil CLI cifrado y otro sin cifrar con
una conversación MLS activa. La CLI nueva adoptó ambos y la conversación siguió
sin pérdidas. La CLI antigua siguió leyendo el perfil adoptado, y una copia
marcada como versión 2 se rechazó sin cambiar un byte. Pasaron las pruebas del
workspace, Clippy, el spike MLS, la demo y los scripts de fase. La actualización
de apps empaquetadas con perfiles poblados en Android y macOS sigue siendo un
paso de aceptación del rediseño del cliente.
Remitente y hora de los mensajes (25 de septiembre de 2026)¶
Cada evento del historial indica ahora quién lo escribió y cuándo lo registró
este dispositivo. La migración 2 añade sender_device y sender_identity a
events. Ambas columnas admiten nulos, así que las builds hasta 0.1.0+11
siguen leyendo y escribiendo un perfil en versión 2; una ejecución con la CLI lo
confirmó.
- Al procesar un mensaje de aplicación MLS (texto o anuncio de adjunto) se guarda el dispositivo detrás de la hoja emisora, que MLS acaba de autenticar. También se guarda la identidad que este perfil conoce para ese dispositivo: la propia para este dispositivo y los que autorizó, o la de un participante según el roster de esa conversación. Los mensajes que escribe este dispositivo se guardan como propios.
- Al leer el historial se resuelve lo que no se conocía al llegar: un dispositivo aprendido después se nombra desde el roster en ese momento. Los eventos registrados antes de la versión 2 conservan el remitente vacío, salvo los enviados por este dispositivo, que son propios. Nunca se atribuye una fila recibida por suposición.
HistoryEventViewañade cuatro campos:created_at: segundos Unix en que este dispositivo registró el evento, es decir, la llegada para los recibidos y la creación para los enviados. No es cuándo lo escribió el remitente; el protocolo no transporta esa hora.sender_identity: la identidad que escribió el evento, si se conoce.sender_label: el nombre local del contacto o un identificador corto; no existe para los eventos propios ni para remitentes desconocidos.own: escrito por esta identidad, desde cualquiera de sus dispositivos.- La pantalla de conversación:
- Alinea en el lado propio los mensajes propios, también los de otro dispositivo de la misma identidad.
- Nombra al autor de los mensajes recibidos cuando escribe más de una identidad ajena en la conversación.
- Muestra la hora registrada.
El rediseño completo de esta pantalla es un paso posterior del plan del cliente. - Los registros importados del historial llevan su hora pero todavía no su remitente. Cambiar el formato del archivo es un paso aparte.
Evidencia:
- Las pruebas del core cubren el remitente guardado y leído, la búsqueda de la identidad de un dispositivo y la migración 2.
- Una prueba de aplicación usa un grupo MLS real en proceso. Cubre otra identidad conocida solo después de su mensaje, otro dispositivo de la misma identidad, filas antiguas y el cambio de nombre de un contacto.
- La conversión del puente y las pruebas de widgets de Flutter cubren los campos nuevos, el nombre del autor, la alineación propia y el formato de la hora.
- Una actualización desde binarios de
mainadoptó un perfil a la versión 2. El siguiente mensaje recibido llevó el dispositivo y la identidad del participante, el siguiente enviado quedó como propio, las filas anteriores siguieron vacías y la CLI antigua siguió leyendo el perfil.
Resumen de conversaciones y no leídos (25 de septiembre de 2026)¶
Cada fila de conversación incluye ahora tres cosas: su evento más reciente, cuántos mensajes quedan sin leer y cuándo tuvo actividad por última vez. El puente ordena la lista de la GUI por esa actividad. La capa de aplicación conserva el orden en que se iniciaron las conversaciones, porque la línea de comandos las lista así y el script de aceptación de la fase 4 las elige por posición.
- La migración 3 añade
read_markers, un cursor por conversación. Las conversaciones que ya existían cuentan como leídas hasta su evento más reciente, así que una actualización no convierte mensajes antiguos en nuevos. mark_read(group, cursor)solo hace avanzar el marcador, y nunca más allá del evento más reciente. Una pantalla desfasada o una llamada excesiva no pueden, por tanto, ocultar lo que llegue después. La llamada devuelve el marcador vigente y los no leídos que quedan.- Los no leídos son los eventos posteriores al marcador escritos por otra identidad. Nunca cuentan los tipos propios de este dispositivo ni los mensajes de otro dispositivo de la misma identidad. Las filas recibidas antes de guardar el remitente cuentan si quedan después del marcador.
ConversationViewañade tres campos:last_event: el tipo, una vista previa de texto en una línea de como mucho 120 caracteres, el nombre del adjunto, la etiqueta del remitente, si es propio, la hora y los estados de entrega.unread: el número de mensajes sin leer.last_activity: la hora del evento más reciente, o de cuándo este dispositivo empezó a guardar la conversación.
Las filas de la GUI se ordenan por actividad. El orden de los eventos deshace los empates dentro del mismo segundo, y un empate completo conserva el orden de inicio. Como en el historial, solo cruzan el puente cuerpos de texto. - La GUI marca como leída la conversación abierta hasta el evento más reciente que muestra. Lo hace solo en primer plano y una vez por cursor. Un fallo deja el marcador donde estaba, y la siguiente lectura lo reintenta. Las filas muestran la vista previa (con el autor en grupos y «Tú:» en los propios), la hora y un recuento de no leídos que los lectores de pantalla anuncian como parte de la fila.
Evidencia:
- Una prueba del puente cubre el orden por actividad y sus desempates.
- Las pruebas del core cubren marcadores que solo avanzan y se detienen en el evento más reciente, no leídos que excluyen tipos y dispositivos propios, y la migración 3.
- Una prueba de aplicación pasa por el ejecutor. Cubre el orden de inicio, las vistas previas, las marcas desfasadas, los marcadores tras reabrir y que los mensajes nuevos vuelvan a quedar sin leer.
- Una prueba del puente comprueba que la vista previa corta con seguridad el texto multibyte, ocupa una línea y nunca expone un cuerpo que no sea texto.
- Las pruebas de Flutter cubren el marcado único, las vistas previas de las filas y los recuentos de no leídos.
- Una actualización desde binarios de
mainadoptó un perfil CLI a la versión - Lo que contenía antes contó como leído, el primer mensaje posterior quedó sin leer y la CLI antigua siguió leyendo el perfil.
Recordatorio del kit, avisos de dispositivos y estado de sincronización (25 de septiembre de 2026)¶
- Estado del kit de identidad.
- La migración 4 añade
kit_exports. Exportar un kit lo registra como pendiente, junto con la secuencia de manifiesto que contiene. Soloconfirm_kit_saved, que la GUI llama cuando el usuario afirma haber guardado el archivo y su clave, lo convierte en el kit guardado. - El estado de alta informa de
kit_saved_atykit_staleen el dispositivo administrador. Un kit está desactualizado cuando el manifiesto de dispositivos avanzó después de guardarlo. - La pantalla de perfil listo recuerda guardarlo si no hay kit o si está desactualizado. «Más tarde» oculta el recordatorio hasta volver a abrir el perfil.
- La exportación de la CLI no confirma nada.
- Avisos de cambios de dispositivos.
- La migración 5 guarda el conjunto de dispositivos activos de cada manifiesto de contacto que este perfil aceptó. El siguiente manifiesto aceptado indica cuántos dispositivos añadió y retiró. El primero que se conoce es la referencia y no anuncia nada.
- Un manifiesto que cambia, llegue por un grupo o desde el relay, registra un
aviso local
devices-changeden todas las conversaciones compartidas con esa identidad. El aviso se confirma en la misma transacción que el manifiesto. - Los avisos llevan recuentos, nunca identificadores de dispositivo. Tienen un identificador estable por conversación y secuencia, nunca cuentan como no leídos y quedan fuera de los archivos de historial.
- La GUI los muestra como una nota centrada. Si el contacto está verificado,
señala la firma de su identidad verificada; si no, propone comparar el
número de seguridad.
chat historyen la CLI los imprime en una línea. - Estado de sincronización. El controlador de conversaciones mantiene una proyección solo de presentación: nunca sincronizado, sincronizando, sincronizado (con la hora del último éxito), sin conexión o rechazado. Solo un error de transporte tipado cuenta como sin conexión; cualquier otro fallo viene de un servidor que respondió. La pantalla muestra una línea sobre la última sincronización correcta y nunca afirma que algo se haya entregado.
Evidencia:
- Una prueba de aplicación pasa por el ejecutor. Cubre kits pendientes y confirmados, una autorización de dispositivo que desactualiza el kit y una confirmación nueva que lo resuelve.
- Las pruebas del core cubren cambios de dispositivos con manifiestos reales de contacto (referencia, manifiesto repetido, dispositivo vinculado y dispositivo revocado), que los avisos nunca cuentan como no leídos y que sus identificadores son estables.
- Una prueba de aplicación con un grupo MLS real en proceso cubre un manifiesto de referencia sin aviso y un dispositivo vinculado anunciado en las dos conversaciones compartidas, sin no leídos y sin duplicado al repetirse.
- Las pruebas de Flutter cubren el recordatorio del kit y su confirmación, el texto de los avisos con sus variantes verificada y sin verificar, y la proyección de sincronización, que distingue sin conexión de rechazado sin exponer diagnósticos.
- Una ejecución de la CLI contra un relay real: bob aprendió el manifiesto de
referencia de alice sin aviso. Después de que alice vinculara un portátil, el
historial de bob mostró un aviso de un dispositivo añadido, y otra
sincronización no lo repitió. Una actualización desde binarios de
mainadoptó perfiles a la versión 5, y la CLI antigua siguió leyéndolos.
Autores en el historial cifrado (25 de septiembre de 2026)¶
Los registros exportados del historial indican ahora su autor cuando el dispositivo que exporta lo conocía, igual que el historial en vivo: la identidad guardada con el evento, la identidad del roster para su dispositivo o esta identidad para lo que envió.
- El autor es un campo opcional
sender_identitydentro de la versión 1 del formato, siguiendo el precedente defile_present, en lugar de una versión nueva. Un registro sin autor escribe exactamente los mismos bytes que antes. Los archivos anteriores se importan sin autor, y las builds anteriores al campo lo ignoran. - La migración 6 añade
sender_identityaarchived_events. La importación conserva la primera copia de un registro, así que un archivo posterior que nombre otro autor para el mismo registro es un duplicado y no cambia nada. Un autor que no sea una identidad de 32 bytes se rechaza, y no se importa nada de ese archivo. - La página del historial cifrado y las conversaciones importadas muestran el autor por nombre local o identificador corto, marcado como versión del archivo («según el archivo»): un archivo es historial aportado por el usuario, no prueba de autoría.
- Los avisos de cambios de dispositivos nunca entran en una exportación. El exportador rechaza tipos desconocidos, así que dejarlos fuera también evita que falle una exportación después de un aviso.
Evidencia: una prueba del core lee y escribe el formato en ambos sentidos.
Una prueba de aplicación exporta filas con autor, propias, sin atribuir y
avisos, las importa en un perfil restaurado, comprueba las etiquetas en la
página del archivo y en las conversaciones importadas, confirma que se ignora
un autor posterior en conflicto y rechaza un autor malformado. Una prueba de
Flutter cubre la etiqueta. Los flujos de archivo de la fase 2 pasan con la
CLI, y una actualización desde binarios de main adoptó perfiles a la
versión 6.
Base de diseño (25 de septiembre de 2026)¶
El cliente dibuja ya con su propio sistema de diseño, en lugar de un esquema Material derivado de un color y las fuentes del sistema.
- Fuentes. Newsreader, Instrument Sans e IBM Plex Mono van empaquetadas
sin modificar (SIL OFL 1.1), desde
google/fontsen un commit registrado y con su SHA-256 enclients/flutter/assets/fonts/README.md. Sus licencias se registran en la página de licencias de Flutter. No se descarga nada en tiempo de ejecución. El peso de las fuentes variables se fija mediante sus ejes. - Tokens.
ArveilColorscontiene los tokens claros y oscuros del plan, además de los colores de avatar y de remitente, elegidos de forma estable por identidad. Se añadiólineStrongpara los bordes de controles, yonDanger,dangerSoftyonDangerSoftpara los estados de error.ArveilThemetraslada los tokens a Material, y la app sigue el modo claro u oscuro del sistema. - Componentes. Marca (a partir del trazado del propio icono), avatar, marca de verificado, etiqueta sin verificar, contador de no leídos, icono de entrega, separador de fecha, nota, aviso, línea de sincronización, rejilla del número de seguridad, estado vacío, grupo y fila de ajustes, burbuja con agrupación, fila de conversación y compositor. Los iconos son el conjunto de contorno de Material que Flutter ya incluye.
- Pantalla de carga. En Android se muestra el verde pino del icono con la marca, mediante la API de splash del sistema a partir de Android 12. Detrás del primer frame de Flutter la ventana usa el color de fondo, así que no hay destello blanco. macOS no tiene pantalla de carga.
Evidencia:
- Una prueba unitaria comprueba el contraste WCAG AA de cada par de texto y fondo, y 3:1 en los bordes de controles, en ambos temas.
- Los goldens de una lámina de conversación y otra de identidad, en claro y oscuro, muestran las fuentes y los iconos empaquetados. El comparador tolera como mucho un 0,5 % de píxeles distintos.
- Las pruebas de comportamiento cubren la traducción de estados de entrega (nunca «leído»), las iniciales, las etiquetas habladas y que los componentes caben en un móvil de 390 px con el texto al 200 %.
Las pantallas actuales solo adoptan los colores y la tipografía nuevos; las pantallas rediseñadas son los paquetes C del plan del cliente.
Español e inglés (25 de septiembre de 2026)¶
El cliente muestra su interfaz en español o en inglés según el idioma del sistema.
- Mensajes. Todas las cadenas visibles están en
clients/flutter/lib/l10n/app_es.arbyapp_en.arb.gen-l10ngenera el código, que se incluye en el repositorio; el español es la plantilla. Los recuentos usan plurales ICU y las fechas siguen el orden de cada idioma: «20/9/2026» en español y «Sep 20, 2026» en inglés, con el mes abreviado porque los dígitos solos admiten dos lecturas. La hora sigue el ajuste de 24 o 12 horas del sistema («18:30», «6:30 p. m.», «6:30 PM»). Las cuentas atrás de los códigos se muestran en minutos y segundos («Caduca en 9:59.») y el lector de pantalla las lee en minutos enteros, que cambian una vez por minuto. - Elección del idioma. Solo se usa el inglés si el sistema lo prefiere. En cualquier otro caso, incluido un sistema en catalán, gallego o euskera, la interfaz aparece en español. D1 añadirá la elección manual.
- Código fuera de los widgets. La sesión del perfil, el controlador de
conversaciones y los diálogos nativos de archivos leen
currentStrings, que la app actualiza con el idioma en uso. Un componente montado fuera de la app recurre a esas mismas cadenas. - Glosario. La interfaz ya no muestra nombres del protocolo: dice servidor en lugar de relay, kit de identidad, claves para grupos nuevos e historial cifrado, como fija el plan del cliente. Los términos técnicos se mantienen en los diagnósticos y en la documentación de operación.
Evidencia:
- Las pruebas de widgets siguen buscando el texto en español, la lengua
normativa, mediante
test/flutter_test_config.dart. test/l10n_test.dartcomprueba que ambos ARB tienen los mismos mensajes con los mismos marcadores, la resolución del idioma y la app completa en inglés, incluido el texto que se genera fuera de los widgets.- El CI vuelve a generar las localizaciones y falla si difieren de las incluidas en el repositorio o si queda algún mensaje sin traducir.
Navegación adaptativa y atajos (25 de septiembre de 2026)¶
Un perfil listo abre la navegación principal, con Chats, Contactos y Ajustes. Sin identidad solo se muestran la bienvenida y el alta, y un alta que termina lleva directamente a Chats.
- Tamaños. Por debajo de 600 dp hay una barra inferior, y una conversación
abierta ocupa toda la pantalla sin ella. Entre 600 y 839 dp la estructura es
la misma, con márgenes mayores. Desde 840 dp un raíl lateral acompaña a la
lista y a la conversación, que comparten la ventana. Los límites están en
WindowSize, dentro del sistema de diseño. - Estado. Cada destino se construye al visitarlo por primera vez y después sigue montado: la conversación continúa sincronizando y conserva su borrador mientras se ven los contactos o los ajustes, y también cuando la ventana cambia de tamaño. El gesto de volver cierra primero la conversación y, desde otro destino, vuelve a Chats.
- Ajustes. Reúne lo que antes mostraba la pantalla del perfil: dispositivos, historial cifrado, claves para grupos nuevos, el kit de identidad, la vinculación de otro dispositivo y «Cerrar perfil». El recordatorio del kit y el aviso de recuperación aparecen sobre la lista de chats, y «Guardar kit» lleva al panel del kit en Ajustes.
- Atajos de escritorio (⌘ en macOS, Ctrl en los demás sistemas): ⌘N abre un chat nuevo, ⌥↑ y ⌥↓ pasan al chat anterior o siguiente, ⌘, abre Ajustes y Esc cierra la conversación; los diálogos se cierran con Esc por sí mismos. Esc no cierra pantallas con formularios, para no perder lo escrito. En escritorio Intro envía y Mayús+Intro no envía; mientras un método de entrada compone texto, Intro confirma la composición. En el móvil Intro inserta una línea. ⌘K llegará con la búsqueda de chats (C2).
- Foco. El raíl, la lista y la conversación forman grupos de recorrido, así que el tabulador avanza del raíl a la lista y de ahí a la conversación.
Evidencia:
test/navigation_test.dartrecorre la app a 390×844 y a 1280×800: barra o raíl, conversación a pantalla completa en el móvil, gesto de volver, cambio de tamaño sin perder el borrador, los atajos en macOS y Linux, Intro en macOS, Windows y Android, y el orden del tabulador.- Las pruebas del kit, la recuperación, la vinculación y las claves siguen el camino nuevo hasta Ajustes.
Lista de chats (25 de septiembre de 2026)¶
La lista de chats usa ya los componentes del sistema de diseño.
- Filas. Cada fila lleva avatar, nombre, vista previa del último evento, hora, no leídos y, si el último mensaje salió de este dispositivo, su estado de entrega (nunca «leído»). El avatar toma el tono de la otra persona o, en un grupo, del grupo. La marca de verificado aparece cuando se han verificado todas las demás personas, y «Sin verificar» cuando falta alguna. La hora es la de hoy, «Ayer» o la fecha.
- Búsqueda. Filtra por el nombre de la conversación o de cualquier persona, sin distinguir mayúsculas ni acentos, y dice cuándo no hay coincidencias. ⌘K (Ctrl+K fuera de macOS) pone el cursor en ella, cerrando en el móvil la conversación que tape la lista; Esc la vacía.
- Encabezado. La línea de sincronización indica con un punto si se llegó al servidor. Debajo aparecen los avisos: el kit de identidad pendiente o desactualizado, con «Guardar kit» y «Más tarde»; la advertencia tras una recuperación; y la falta de conexión o el rechazo del servidor.
- Vacío. Sin conversaciones, la lista explica cómo empezar y ofrece «Nueva conversación»; en escritorio, el panel sin conversación abierta también usa el estado vacío.
ArveilColors.ofrecurre a los tokens por defecto del brillo en uso si un widget se muestra fuera del tema de Arveil, como en una prueba aislada.
Evidencia: test/chat_list_test.dart cubre las filas (avatar, verificación,
entrega y no leídos), las horas, la búsqueda sin acentos y su vaciado, ⌘K y
Esc en macOS y Windows, el estado vacío con su acción, el aviso del kit y la
lista con el texto al 200 % en un móvil.
Conversación (25 de septiembre de 2026)¶
La conversación abierta usa ya los componentes del sistema de diseño.
- Historial. Los mensajes forman burbujas con el ancho de su contenido. Los de un mismo autor, con menos de diez minutos de diferencia y el mismo día, se agrupan: solo la primera burbuja lleva la cola y, en un grupo, el nombre del autor en su color. Un separador indica «Hoy», «Ayer» o la fecha cuando cambia el día local. Los avisos de dispositivos aparecen centrados y siempre sueltos. Se conserva la carga de mensajes anteriores.
- Entrega. Cada burbuja muestra la hora y, si salió de este dispositivo, un icono de estado. La pulsación larga o el clic secundario abren sus detalles: la hora a la que se registró aquí, el estado de cada buzón (aceptado por el servidor, pendiente, rechazado o caducado, nunca «leído»), y la opción de copiar el texto.
- Adjuntos. Van en una burbuja con icono, nombre, tamaño, estado, progreso y las mismas acciones de antes. La burbuja no fusiona su semántica, así que cada botón sigue siendo accesible por separado.
- Cabecera y detalles. La cabecera muestra avatar, nombre y si la otra persona está verificada o cuántas personas tiene el grupo. Los detalles enumeran a cada persona con su verificación y sus dispositivos, tus dispositivos y los archivos del historial cargado. Desde 1200 dp quedan en un panel junto a la conversación; en el móvil se abren en una hoja y en el resto de tamaños en un diálogo.
- Sin conexión. En una sola columna, la conversación muestra el aviso de falta de conexión o de rechazo del servidor, que en dos columnas ya aparece sobre la lista. El compositor es el del sistema de diseño, con los mismos límites y sin que el teclado aprenda lo que se escribe.
- En escritorio, cada columna tiene su propia barra: la lista con sus acciones, la conversación con su cabecera y los detalles con su título.
Evidencia:
test/conversation_view_test.dartcubre la agrupación y los separadores, los nombres en el primer mensaje de cada grupo, los detalles por buzón y la copia con el clic secundario, el panel desde 1200 dp y el diálogo por debajo, el aviso sin conexión en el móvil y la conversación vacía.- Los goldens de componentes se regeneraron con las burbujas ajustadas a su contenido.
Contactos, verificación y ajustes (25 de septiembre de 2026)¶
- Contactos. La lista empieza por «Tu ruta de contacto», que muestra y copia la ruta de este dispositivo (sale de la barra de chats). Cada contacto lleva su avatar, la marca de verificado o «Sin verificar» y cuántos dispositivos tiene disponibles. Sin contactos se ofrece añadir uno.
- Verificación. Al añadir un contacto o abrir sus datos se ve su identidad y el número de seguridad en la rejilla de cuatro filas, con «Coinciden» y «No coinciden». En un alta, «Coinciden» hace que el contacto se guarde verificado; en un contacto guardado, lo verifica. «No coinciden» explica que no debe verificarse y que conviene pedir otra vez la ruta por otro canal. Guardarlo sin verificar sigue siendo posible y nunca verifica.
- Ajustes por secciones. Tu identidad (si este dispositivo administra o está vinculado, y tu ruta), Seguridad y recuperación (kit de identidad, dispositivos, vincular otro dispositivo e historial cifrado), Conexión (claves para grupos nuevos) y Aplicación (actualizaciones, apariencia, diagnóstico y «Acerca de Arveil»), y al final «Cerrar perfil». Cada fila dice su estado: el kit sin guardar, desactualizado o la fecha en que se guardó, con aspecto de aviso cuando requiere atención, y la última consulta de claves.
- Acerca de Arveil. La versión, quién la hace (KaiCorp Labs) y enlaces a la web, a la política de privacidad en el idioma de quien lee y al código fuente, que se abren en el navegador (o se copian si no lo hay), además de las licencias de código abierto que exigen las bibliotecas y tipografías. También se abre desde la bienvenida, antes de que exista una identidad.
- Pantallas propias. El kit, la vinculación y las claves se abren en su propia pantalla con barra, y la actividad y los errores del perfil se muestran allí. «Guardar kit» en el aviso de la lista abre directamente la pantalla del kit. Dispositivos e historial cifrado usan los márgenes, tarjetas y avisos del sistema de diseño.
SettingsGroupes ahora unMaterial, para que se vea la respuesta al pulsar sus filas.
Evidencia: test/settings_test.dart cubre las secciones y los estados del kit
(guardado, sin guardar y dispositivo vinculado), cada pantalla con su barra,
la ruta propia desde contactos, la lista con verificación, la lista vacía y el
alta que solo se guarda verificada tras «Coinciden». Las pruebas del kit, las
claves, la vinculación y los contactos siguen el camino nuevo.
Bienvenida y alta (25 de septiembre de 2026)¶
- Bienvenida. Con el perfil cerrado se ven la marca, qué es Arveil en una frase, la nota sobre la clave del perfil y «Abrir perfil».
- Tres entradas. Un perfil sin identidad pregunta cómo empezar: unirse con una invitación, vincular con otro dispositivo o restaurar desde un kit. Cada entrada tiene «Volver al alta» arriba.
- Invitación en dos pasos. Primero los datos del servidor y después la invitación, cada paso con su propia validación y un indicador «Paso 1 de 2». «Atrás» conserva lo escrito. Un alta que quedó a medias se retoma como antes, con ambos campos en una pantalla, el estado guardado y los errores tipados.
- Kit tras el alta. Cuando una identidad queda lista en este dispositivo y es administradora sin kit vigente, antes de entrar se ofrece guardar el kit («Guardar kit» abre su pantalla) con el riesgo de posponerlo a la vista; «Más tarde» lleva a Chats, donde el aviso sigue hasta que se guarde. Reabrir un perfil ya listo entra directamente.
ProfilePagepasa alib/src/onboarding.dart;main.dartsolo arranca la app.
Evidencia: test/onboarding_test.dart cubre la bienvenida con la marca, las
tres entradas y su vuelta, los dos pasos conservando el servidor, la oferta del
kit con «Más tarde» y con «Guardar kit», y la reapertura sin oferta. La prueba
del alta reanudable recorre los pasos y la de restauración pasa por la oferta.
Apariencia (25 de septiembre de 2026)¶
Ajustes → Aplicación → Apariencia reúne la personalización de la primera versión, con una vista previa que cambia al momento.
- Tema: sistema, claro u oscuro.
- Acento: pino (el de la marca), lago, ciruela, arcilla, musgo y pizarra,
cada uno con variante clara y oscura. La prueba de contraste recorre todos
los pares de la interfaz con cada acento y en ambos temas; no hay selector
libre de color porque no podría garantizarlo. La prueba incluye ahora el
texto atenuado y el acento sobre una fila seleccionada, y por eso el tinte
oscuro del pino pasó de
#1D4740a#1A423B. - Fondo de la conversación: liso, arcos, puntos, ondas o rombos, dibujados
por la app con el color de línea decorativa. Burbujas, fechas y avisos son
opacos encima. El motivo va tras un
RepaintBoundaryy no se repinta al desplazar los mensajes. - Tamaño del texto: del 90 % al 130 %, multiplicado por el del sistema.
- Idioma: el del sistema, español o inglés.
Las preferencias son globales y se leen antes de abrir el perfil, porque la
bienvenida ya usa el tema. Se guardan como appearance.json en el directorio
de soporte de la app, fuera de profile/, escribiendo primero un archivo
temporal que después sustituye al anterior. Solo contienen esos cinco valores:
ningún identificador, nombre, ruta ni imagen. Un campo ausente o desconocido
toma su valor por defecto sin afectar a los demás, y un archivo ilegible deja
todos los valores por defecto. Si no se puede guardar, el cambio sigue vigente
hasta cerrar la app.
Evidencia: test/appearance_test.dart (contraste de cada acento en ambos
temas, lectura campo a campo, sustitución atómica y archivo roto, cambios
inmediatos de tema, acento, tamaño e idioma, y el fondo que no se repinta al
desplazar) y el golden test/goldens/wallpapers.png con dos fondos en claro y
oscuro.
Accesibilidad automatizada (25 de septiembre de 2026)¶
- Mensajes. Cada burbuja se lee como una frase: quién, cuándo y qué («Lucía, 18:40: ¿Vienes?»), y en lo enviado desde este dispositivo, su estado («… Aceptado por el servidor»). La acción de pulsación larga abre sus detalles también con el lector de pantalla. Los avatares son decorativos.
- Movimiento. Si el sistema pide reducir el movimiento, las pantallas
aparecen sin desplazarse ni escalarse (
MotionAwareTransitions). - Texto grande. Con el texto al 200 % las pantallas principales no se
desbordan: la etiqueta «Sin verificar» cede cuando falta sitio (
NameLine), la barra del alta muestra un icono con descripción en lugar de «Cerrar perfil», y el nombre de una fila aprovecha todo el ancho disponible en vez de la mitad. - Objetivos táctiles. Las pantallas principales de un móvil cumplen las pautas de Flutter de 48 dp y de elementos pulsables con etiqueta; para ello las muestras de acento y el campo del compositor pasan a medir 48 dp.
- Ajustes. Cada fila es una parada propia con su título, su estado y su acción, y el título de cada grupo es un encabezado aparte. Antes, un grupo con una sola fila pulsable, como «Conexión», se leía como un único encabezado pulsable. El control del tamaño del texto dice qué ajusta y su valor una sola vez («Tamaño del texto, 100 %»); el valor también se ve junto al control.
Evidencia: test/accessibility_test.dart. La revisión con TalkBack está en la
matriz de plataformas; VoiceOver queda sin probar.
Diagnóstico sin secretos (25 de septiembre de 2026)¶
Ajustes → Aplicación → Diagnóstico muestra un informe antes de que salga del
dispositivo y lo guarda con el diálogo nativo (arveil-diagnostic.txt).
- Qué contiene: versión y commit de la compilación (inyectados por
scripts/package_clients.py; «local build» si no), sistema, idioma, si el perfil está abierto, la fase del alta, si el dispositivo administra o está vinculado, el número de conversaciones y de dispositivos activos, el estado del kit, el nivel de claves para grupos nuevos y los códigos de los últimos fallos. - Códigos de fallo: tipo y operación («transport:sync»), nunca el mensaje ni el motivo, que pueden llevar rutas, direcciones o texto remoto. Una operación que no sea un nombre simple se registra como «unknown». Se guardan los veinte últimos, solo en memoria.
- Qué no contiene: claves, identificadores, rutas, direcciones, invitaciones, nombres ni contenido. El informe se escribe con claves en inglés, como el resto del diagnóstico técnico.
Evidencia: test/diagnostics_test.dart crea un perfil con identificadores,
nombres, mensajes, ruta, número de seguridad, servidor, rutas del sistema y
motivos de error marcados como secretos, y comprueba que ninguno aparece en el
informe y que los recuentos sí; también que la pantalla guarda exactamente lo
que muestra.
Capturas y cierre del rediseño (25 de septiembre de 2026)¶
Las pantallas principales tienen goldens completos en móvil (390×844) y
escritorio (1280×800), en claro y oscuro (test/goldens/screens_test.dart),
con un círculo inventado de personas y mensajes y horas fijas en hora local,
así que se ven igual en cualquier zona horaria. Las capturas de la
documentación son copias de esos goldens: scripts/update_screenshots.sh las
regenera y una prueba comprueba que no se desfasan. Desde el 1 de octubre de
2026 los goldens se generan en inglés y en español (test/goldens/screens/en/
y es/), incluyen la tarjeta de contacto, su código y un enlace de contacto
recibido, y scripts/readme_media.py enmarca a partir de ellos las imágenes
del README y el recorrido animado (docs/assets/readme/); una segunda prueba
comprueba que salen de los goldens actuales.


test/hygiene_test.dart comprueba que fuera de design/ y l10n/ no hay
colores ni textos visibles escritos a mano (solo se permiten
Colors.transparent y el formato técnico arveil-bootstrap:v0:…). El nombre
de la app y los nombres de los idiomas también salen de los ARB.
Búsqueda dentro de una conversación (25 de septiembre de 2026)¶
- Rust.
Application::search_historybusca en los mensajes de texto de una conversación los que contienen el texto pedido, sin distinguir mayúsculas ni acentos, y los devuelve del más nuevo al más antiguo. Cada llamada lee como mucho 5000 eventos (MAX_SEARCH_SCAN) y dice dónde se detuvo, así que un historial largo responde en un tiempo acotado. No entran avisos, adjuntos ni otras conversaciones. El puente la expone comosearchHistory, con la misma forma que una página del historial. - Interfaz. El botón de búsqueda de la cabecera, o ⌘F (Ctrl+F fuera de macOS) en escritorio, cambia el historial por un campo y los resultados: quién y cuándo, y el texto. Tocar uno abre sus detalles. «Buscar más atrás» continúa cuando la lectura se detuvo antes del principio, y la pantalla dice cuándo no hay coincidencias. Esc, el botón de cerrar o el gesto de volver en el móvil regresan a la conversación.
- Llevar al mensaje dentro del historial queda para más adelante: exige cargar las páginas de alrededor.
Evidencia: dos pruebas en arveil-app (coincidencia sin mayúsculas ni
acentos, solo texto de esa conversación, continuación tras el límite y lectura
acotada a 5000 eventos por llamada) y test/conversation_search_test.dart
(resultados, sin coincidencias, buscar más atrás, ⌘F y Esc en macOS y Linux,
y volver en el móvil).
Visibilidad de lectura y actualización de dispositivos (26 de septiembre de 2026)¶
Leer el historial local o sincronizar ya no avanza por sí solo el marcador de lectura. La conversación comunica el cursor del historial mostrado después del fotograma, solo mientras Chats está visible, su pantalla es la actual, la búsqueda está cerrada y la aplicación está activa. Los mensajes recibidos detrás de Ajustes, Contactos, la búsqueda u otra pantalla siguen sin leer hasta volver al historial. La sincronización continúa al navegar y una consulta que termina tarde no puede marcar una conversación oculta como leída.
Las operaciones de dispositivos recargan el estado compartido del perfil al terminar, también si falla la red después de guardar una revocación local. Ajustes y el recordatorio de la lista de chats muestran entonces que hay que actualizar el kit de identidad guardado. El estado también se recarga si se sale de la pantalla de dispositivos antes de que termine la operación.
Las regresiones de widgets están en test/conversation_visibility_test.dart y
test/settings_test.dart. El escenario nativo de conversaciones abre ahora la
aplicación completa y usa su navegación y cuadrícula de números de seguridad
actuales; comprueba el contador real de Rust mientras Ajustes oculta un mensaje
entrante y después de volver a mostrar la conversación. Estas pruebas no
acreditan la aceptación en dispositivos físicos ni la actualización de paquetes.
Poner nombre a quien no lo tiene (26 de septiembre de 2026)¶
Los nombres siguen siendo locales: cada perfil nombra a sus contactos, el nombre no sale del dispositivo y no autentica a nadie. Lo que cambia es cuánto se ve. Antes, una persona sin nombre aparecía como ocho caracteres hexadecimales, y quienes probaron la beta lo leyeron como una función que faltaba.
- Rust indica, para cada persona de una conversación, si este perfil le puso
nombre (
PeerView.named), en vez de dejar que la app lo adivine por la etiqueta. - Una persona sin nombre se lee Sin nombre · a1b2c3d4 en la lista de chats, en el título de la conversación y en Contactos, con el icono de persona en vez de iniciales.
- Una conversación abierta con alguien sin nombre muestra un aviso con Ponle nombre, que abre un diálogo donde se dice que el nombre solo se ve en este perfil. Si faltan varios, Poner nombres abre los detalles.
- Los detalles de la conversación ponen nombre a quien no lo tiene y cambian el de quien ya lo tiene.
- Una conversación nueva creada con rutas pegadas pide un nombre local por persona, relleno con el que ya tenga un contacto guardado. Si no se pueden leer los contactos, los campos quedan vacíos y la conversación se crea igual.
Un nombre compartido, que cada persona elige y que viaja a sus conversaciones, cambia el protocolo y se propone aparte en el ADR-011.
Las pruebas están en test/names_test.dart, y las capturas de la conversación
de escritorio muestran la acción de cambiar el nombre en los detalles.
Conversar antes de verificar (27 de septiembre de 2026)¶
Primero conectar, verificar cuando se quiera. La verificación condicionaba cada conversación nueva, y eso bloqueaba (quien compartía su ruta no veía el número de seguridad hasta tener también la de la otra persona) o enseñaba a marcar «Hemos comparado» sin comparar. Ahora comparar es opcional al crear la conversación y sigue disponible desde ella. No cambian el protocolo, el relay ni el formato de los mensajes.
- Rutas pegadas. Al preparar se siguen viendo el número de seguridad de cada ruta y un campo para el nombre local. Cada persona tiene una casilla opcional, Lo hemos comparado por otro canal y coincide, y la conversación se crea sin ella. Solo quienes se marcan quedan fijados como verificados; el resto se guarda como contacto sin verificar, con el nombre escrito. Una ruta de una identidad ya verificada con otra raíz se sigue rechazando, se haya comparado o no, y no se guarda nada. Dejar sin marcar a alguien ya verificado no le quita la verificación.
- Contactos guardados. El selector ofrece también los contactos sin verificar. Siguen haciendo falta una ruta guardada y un dispositivo que no conste como revocado, y esa ruta tiene que seguir correspondiendo a la identidad, la raíz y el dispositivo del contacto.
- Visible, sin bloquear. Mientras alguien de la conversación abierta esté sin verificar, la línea bajo su nombre dice Sin verificar · Verificar, o 1 sin verificar · Verificar en un grupo. Tocar la cabecera abre los detalles. Nada es modal, el campo para escribir sigue disponible y el aviso Ponle nombre no cambia.
- Verificar desde los detalles. Cada persona sin verificar muestra el número de seguridad en la cuadrícula de siempre, con Coinciden y No coinciden. Coinciden hace la misma comprobación que Contactos; No coinciden avisa y no verifica nada. Quien recibe la invitación no tiene ruta guardada de los demás miembros, solo el roster: el número se calcula sobre la raíz que nombró el roster, y verificar fija esa raíz. Todo miembro conocido en una conversación ya tiene un contacto sin verificar creado a partir del roster; si faltara, verificar lo guarda antes con la raíz del roster.
- API de Rust.
create_verified_conversationpasa a sercreate_route_conversation, con un número comparado opcional por ruta (ConversationRoute);createConversationen el puente recibe unString?por ruta.PeerView.safetyNumberlleva el número que hay que comparar con cada persona.create_contact_conversationya no exige contactos verificados. La CLI no cambia:chat startnunca exigió verificación.
Evidencia: las pruebas de arveil-app cubren un contacto guardado sin
verificar que llega al paso de red, rutas pegadas que se guardan sin verificar
salvo que se comparen, una ruta comparada que queda fijada como verificada, una
ruta de una identidad verificada con otra raíz rechazada sin guardar nada, y la
verificación de un miembro conocido solo por el roster, con contacto y sin él.
test/verification_test.dart cubre la línea de la cabecera, la verificación
desde los detalles en un teléfono y junto a una conversación ancha, No
coinciden, un guardado fallido y los grupos; test/conversations_test.dart,
test/names_test.dart y test/contacts_test.dart cubren crear sin comparar y
los contactos sin verificar en el selector. Las comprobaciones de accesibilidad
abren ahora una conversación con alguien sin verificar. El asistente de
aceptación de conversaciones elige un contacto guardado sin verificar, lo
verifica después y verifica al creador desde el perfil que recibe. Las capturas
de la conversación en teléfono y escritorio muestran la nueva línea y la
comparación en los detalles.
Códigos QR y enlaces (27 de septiembre de 2026)¶
La ADR-012 sustituye las cadenas largas que había que llevar de una app a otra. Un único payload viaja como código QR, enlace https o texto pegado, para unirse, vincular un dispositivo y añadir un contacto.
- Alta.
arveil-relay inviteimprime una línealink:y, en una terminal, su código QR. El campo del alta acepta el enlace, o un mensaje entero que lo contenga, y rellena los datos del servidor y la invitación. Un código que no es una invitación, uno más nuevo, uno dañado o dos a la vez lo dicen. - Vincular un dispositivo. Ajustes › Vincular otro dispositivo muestra un código QR, una cuenta atrás y Copiar enlace. El dispositivo nuevo elige Vincular con mi otro dispositivo y lo escanea (Android) o pega el enlace. Los dos muestran el mismo número mientras el nuevo espera; el que tiene la raíz pregunta ¿Vincular «Pixel 8 · Android 15»? y no firma nada hasta Vincular. Rechazar detiene al nuevo enseguida. Un código escaneado se aplica al llegar la concesión; uno pegado se confirma también en el dispositivo nuevo. El flujo anterior, en el que el nuevo muestra un código, queda a un toque en las dos pantallas y ahora se confirma igual.
- Contactos. Contactos › Mi tarjeta de contacto ofrece Mostrar mi código (un QR válido diez minutos, una vez y mientras la pantalla esté abierta) y Compartir mi contacto (un enlace por la hoja de compartir del sistema, válido 30 días, listado y revocable), con un nombre opcional para la tarjeta. Escanear un código (Android) y Abrir un enlace de contacto muestran a quién nombra la tarjeta y el número de seguridad antes de Empezar a hablar. Escanear en persona verifica a las dos personas; los detalles dicen Verificado en persona. Desaparece la casilla que marcaba un número como comparado al crear una conversación: se verifica desde los detalles o en persona.
- Solicitudes. Una conversación de alguien que no es un contacto elegido espera en Solicitudes, arriba de la lista de chats: «Ana quiere hablar contigo», con qué enlace compartido usó o que no usó ninguno, y Aceptar o Rechazar. Las conversaciones rechazadas desaparecen y no entregan nada más.
- Abrir enlaces. En Android, los enlaces a
arveil.kaicorplabs.com/join,/linky/contactabren la app directamente; las dos plataformas aceptan el esquemaarveil:del botón Abrir en Arveil de la página. Cada enlace rellena la pantalla que corresponde y espera un toque. - Cámara. Solo en Android, pedida tras Escanear; los fotogramas se leen en el núcleo Rust y nada sale del dispositivo. Sin cámara, o con el permiso rechazado, siempre se puede pegar.