Saltar al contenido

Crear paneles

El sandbox

Lo que una página de panel puede y no puede hacer dentro de Deska, regla por regla: la red, CORS, el almacenamiento y los archivos, scripts y estilos, formularios, enlaces, el foco, el portapapeles y los embeds. Cada regla viene con qué hacer en su lugar.

Por qué un sandbox

La gente instala paneles hechos por desconocidos junto a su código y sus agentes. El sandbox es lo que le permite hacerlo sin leer tu código: un panel corre en un marco con un origen opaco, en un proceso propio, bajo una política de seguridad de contenido estricta, y solo habla con Deska a través del puente. Nada dentro de él puede llegar a los archivos de la persona, a otros paneles, agentes, sesiones iniciadas o a su cuenta de Deska, ni ejecutar nada en la computadora.

Una carpeta cargada corre exactamente con las reglas de un paquete instalado, así que te encuentras con cada límite de esta página mientras construyes, no después de publicar.

La política

Cada archivo de un paquete se sirve con esta Content-Security-Policy, donde <package> es el origen propio del paquete y <hosts> es cada host de network como https://host:

Content-Security-Policy
default-src 'none';
script-src <package> 'unsafe-inline';
style-src <package> 'unsafe-inline';
img-src <package> data: blob: <hosts>;
font-src <package> data: <hosts>;
media-src <package> blob: <hosts>;
connect-src <package> <hosts>;
form-action 'none';
frame-src 'none';
worker-src 'none';
object-src 'none';
base-uri 'none';
sandbox allow-scripts allow-forms

El marco en sí es sandbox="allow-scripts allow-forms", sin allow-same-origin, sin ventanas emergentes y sin diálogos modales. Antes de cualquiera de tus scripts, Deska ejecuta en cada página una pequeña protección propia: quita WebRTC, quita los marcos anidados, objetos y sugerencias de precarga en cuanto se agregan, y aplica las reglas de foco de más abajo. Las páginas de otros sitios que muestra un panel no son marcos suyos sino de Deska; consulta Marcos, embeds y reproductores.

Red: hosts declarados, solo https

fetch, imágenes, fuentes, audio y video solo llegan al paquete y a los hosts de network, por https. El http simple se rechaza incluso para un host declarado, y también cualquier host que no listaste, incluidos los subdominios de uno que sí listaste.

  • En su lugar: lista cada host que toca el panel, exacto. La gente ve la lista antes de instalar, así que mientras menos, mejor.
  • Ponle a cada solicitud un tiempo límite y un estado para la falla, sin conexión y resultados parciales. Si un host falla, solo debe marcar lo que venía de él.
net.jsjs
async function getJson(url, ms = 8000) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), ms)
  try {
    const res = await fetch(url, { signal: controller.signal })
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return await res.json()
  } finally {
    clearTimeout(timer)
  }
}

HN Front Page y Service Status muestran tiempos límite, resultados parciales y consultas que siguen la visibilidad.

CORS: la API debe aceptar cualquier origen

El origen de la página es opaco, así que sus solicitudes llevan Origin: null. Un navegador deja que la página lea una respuesta solo cuando el servidor acepta cualquier origen con Access-Control-Allow-Origin: *. Una API que lista los orígenes que acepta, o que responde sin ese encabezado, falla con un error de red que la página no puede ver por dentro.

  • En su lugar: elige APIs que respondan *. Muchas públicas lo hacen (la REST API de GitHub, Open-Meteo, Wikipedia, Hacker News, los resúmenes de Statuspage). Pruébala con la vista previa antes de diseñar alrededor de una.
  • <img>, <audio> y <video> desde un host declarado no necesitan CORS. Leer sus píxeles en un canvas sí.
  • Los encabezados de respuesta que puede leer una página también los limita CORS. GitHub expone los de su límite de uso; Repo Pulse los usa.

Almacenamiento: deska.storage y deska.files

Con un origen opaco, localStorage y sessionStorage lanzan un error, indexedDB no está disponible y las cookies nunca se guardan.

  • En su lugar: deska.storage, 256 KB de JSON por paquete en esta computadora, compartidos por cada copia del panel. Guarda un documento con versión por tema y mígralo al cargar (Habit Streaks).
  • Guarda las imágenes y otros archivos en deska.files: hasta 32 MB por archivo, 256 MB y 2,000 archivos por paquete. En deska.storage guarda solo sus ids.
  • Guarda en el almacenamiento lo que te dio la red, para que una recarga o un inicio sin conexión dibuje de inmediato.

Archivos que entran y salen

  • Un selector de archivos (<input type="file">), pegar (clipboardData.files en el propio evento paste de la página) y soltar sobre la página (dataTransfer.files, después de preventDefault() en dragover) llegan a la página. Cada uno empieza con la persona.
  • Las descargas se rechazan, incluido <a download>. En su lugar: deska.files.save(blob, name) desde el clic. Deska muestra el diálogo de guardar del sistema, la persona elige dónde, y la página nunca sabe la ruta.
  • Ningún otro archivo de la persona está a su alcance: un panel solo ve lo que le entregaron y lo que guarda él mismo.

Scripts: solo desde el paquete

Los scripts solo se cargan desde el paquete; los scripts en línea sí corren. Un script de un CDN se rechaza, y también el código armado desde texto en tiempo de ejecución: eval, new Function y temporizadores con texto. WebAssembly todavía no está disponible.

  • En su lugar: copia cada librería al paquete como archivo .js o .mjs e impórtala con una ruta relativa. Si usas un bundler, incluye lo que genera y deja el código fuente fuera de la carpeta que cargas.
  • Evita librerías que compilan plantillas en el navegador; usa su versión precompilada.
  • Los workers se rechazan (worker-src 'none'). Divide el trabajo pesado en partes entre cuadros.

Estilos, fuentes, imágenes y medios

  • Las hojas de estilo solo se cargan desde el paquete; los estilos en línea funcionan. Una hoja de estilo de otro host, incluida la de Google Fonts, se rechaza aunque el host esté declarado. En su lugar: incluye la fuente como woff2 con un @font-face en tu propio CSS, o usa var(--deska-font-ui).
  • Las imágenes vienen del paquete, de data:, de blob: y de hosts declarados. Las fuentes de íconos de un CDN no cargan: dibuja los íconos como SVG en línea con currentColor (Weather Now).
  • El audio y el video se transmiten desde hosts declarados o desde blob:. Un paquete no puede llevar archivos de medios, así que Net Radio transmite y Noise Machine genera su sonido con Web Audio.
  • El sonido puede necesitar un clic primero: audio.play() y un AudioContext nuevo pueden rechazarse hasta que la persona haga clic en el panel. Muestra un botón de reproducir cuando pase.

Los formularios nunca navegan

form-action 'none' rechaza todo envío real. Los formularios, los campos y la validación del navegador siguen funcionando; solo desaparece la navegación.

search.jsjs
form.addEventListener('submit', (event) => {
  event.preventDefault()
  search(form.elements.query.value)
})

En su lugar: maneja submit con script y preventDefault(), y envía los datos con fetch a un host declarado.

El foco y el teclado

El teclado se queda donde lo puso la persona. Un panel sin foco no puede tomarlo: focus(), window.focus(), abrir un diálogo o un popover, y click() no hacen nada hasta que la persona usa el panel. En el lienzo, el primer clic sobre un panel sin foco solo le da el foco; el siguiente llega a tu página.

  • En su lugar: mueve el foco dentro de tu página en respuesta a las teclas y los clics de la persona, y pide atención con deska.notify.
  • Los atajos de teclado funcionan mientras el panel tiene el foco. Prompt Shelf maneja la búsqueda, las flechas, Enter y Escape dentro del sandbox.

El portapapeles

  • navigator.clipboard se rechaza, y nada puede leer el portapapeles por su cuenta. Lo que la persona pega en el panel llega al evento paste de la página, como en cualquier página.
  • Sin clipboard-write, el execCommand('copy') y el ('cut') de la página no hacen nada, para que un botón no pueda reemplazar lo que copió la persona. Seleccionar texto y copiarlo con el teclado funciona como en cualquier página.
  • En su lugar: pide clipboard-write y llama a deska.clipboard.write(text) desde el clic, hasta 64 KB, o a deska.clipboard.writeImage(blob) para un PNG o JPEG de hasta 32 MB.

Marcos, embeds y reproductores

Un panel nunca enmarca nada por su cuenta. Ningún <iframe>, <object> ni <embed> sobrevive: la política mantiene frame-src 'none' y la protección quita cualquiera que se agregue.

En su lugar: para mostrar la página de otro sitio, como un reproductor de YouTube, un mapa o un tablero público, pon su host en embeds dentro de panel.json y pídeselo a Deska con deska.embed. Deska dibuja un marco propio sobre el panel, donde la página lo pidió, y lo mantiene seguro:

  • La página incrustada corre sin sesión iniciada, con cookies y almacenamiento propios que se descartan en cada carga. Nunca ve las cuentas de la persona, la sesión de Deska ni los datos de otro panel.
  • Puede reproducirse sola y usar imagen en imagen, y nada más: sin ventanas emergentes, descargas, cámara, micrófono, portapapeles ni pantalla completa. Los enlaces que abren una ventana nueva no hacen nada.
  • El panel no puede leerla, controlarla con scripts, hacerla navegar ni dibujar encima. Solo dice dónde va el marco, y Deska lo mantiene dentro del panel.
  • Solo páginas https de los hosts en embeds, en el puerto por defecto. Los sitios de Deska y las páginas de inicio de sesión y de pago nunca se permiten.
  • La pregunta antes de instalar nombra cada host del que el panel muestra páginas, y una actualización que agrega uno vuelve a preguntar.
  • Un host que incrusta puede recibir lo que el panel ponga en la dirección, así que trata cada uno como un host de network.
  • Publicar un panel con embeds necesita una cuenta que Deska aprobó para publicar.
  • Una página en localhost (tu servidor de desarrollo) no puede incrustar; una carpeta cargada sí.

Sin embeds, reproduce archivos de medios desde un host declarado con <audio> o <video> (Window View), dibuja lo que devuelve una API, y abre el sitio completo con deska.openUrl. Para navegar un sitio web entero en el escritorio, el lugar es el panel de navegador de Deska, no un panel de la comunidad.

Dispositivos y funciones del sistema

FunciónEn un panelEn su lugar
Micrófono, cámara, pantallaRechazadoNo está disponible para los paneles.
UbicaciónRechazadoPide una ciudad y geocodifícala (Weather Now).
Notificaciones del navegadorRechazadodeska.notify
WebRTCQuitadoNo está disponible para los paneles.
DescargasRechazadodeska.files.save
Service workers, workersRechazadoHaz el trabajo en la página, por partes.

Sin secretos

Un panel no tiene dónde guardar un token de forma segura. deska.storage es JSON simple en la computadora que lee cada copia del paquete, todo lo que va en el paquete es público, y todo lo que se escribe en la página vive en su memoria.

AtenciónNunca le pidas a la gente una clave de API o una contraseña en un panel. Usa APIs públicas que no necesiten clave y quédate dentro de sus límites anónimos, como hace Repo Pulse con las 60 solicitudes por hora de GitHub.

En marcha, oculto y recargado

  • Un panel corre mientras alguno de sus marcos está montado. Fuera de la pantalla, detrás de una tarjeta de la vista lejana o en un worktree oculto, sigue corriendo y recibe visible: false. Detén ahí los temporizadores, animaciones y consultas.
  • Puede recargarse en cualquier momento (al pasar a un dock, dividirse, separarse, actualizarse o reiniciarse). Reconstruye a partir de deska.ready() y del almacenamiento.
  • Corre en un proceso propio. Un ciclo que nunca cede congela el panel, no a Deska.
  • Las páginas en localhost corren sin esta política ni la protección; lee Pruebas y depuración antes de confiar en una.