diff --git a/i18n/en.json b/i18n/en.json index 21bc3e2..4063033 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -10,7 +10,7 @@ "notIncludedLabel": "Not included", "notIncluded": { "0": "Inference service access (GPUs)", - "1": "Workspaces and app deployment", + "1": "An included workspace (workspace slots are sold separately)", "2": "API key for model calls" }, "faqUpgradeQ": "How do I move to the inference plan later?", @@ -410,12 +410,30 @@ "3,000M token allowance per billing period", "400M tokens per rolling 4h window: the limit a coding agent reaches first", "1M context · 10 concurrent requests", + "One free Micro workspace (1 vCPU · 2 GiB RAM · 10 GiB disk)", "Everything in nan_member", "Access switches on a few minutes after payment" ], "premiumCond": "GLM 5.3 premium (200€/mo) is offered to members as seats open. Join the waitlist and we will flag you for this tier.", "premiumCta": "Join the waitlist", - "premiumBadge": "glm 5.3 · premium" + "premiumBadge": "glm 5.3 · premium", + "workspaces": { + "label": "workspaces · add-on", + "title": "Your own cloud machine.", + "sub": "A dedicated, isolated dev box reachable over SSH and from the browser, with Pi, Hermes, gentle-shell and Herdr ready to use. Any paying member can buy slots; each slot keeps one workspace running.", + "per": "/mo", + "headSize": "size", + "headSpecs": "vCPU · RAM · disk", + "headSlot": "slot", + "headBackups": "daily backups", + "notes": [ + "Premium (200€) includes one free Micro workspace.", + "Slots pay for the machine only. Agents use NaN inference only on an inference plan (70€ or 200€).", + "Backups are optional: one snapshot a day, kept 7 days, restore it yourself.", + "Monthly, billed in euros. Cancel a slot anytime." + ], + "cta": "Read the workspaces guide" + } }, "who": { "label": "who's behind", @@ -471,6 +489,10 @@ "q": "Can I train models?", "a": "No. NaN is inference, not training. The GPUs are optimized to run existing models at high speed, not for fine-tuning." }, + { + "q": "What is a workspace?", + "a": "Your own cloud machine for development: a dedicated, isolated box you reach over SSH or from the browser, with coding agents such as Pi and Hermes ready to use. Any paying member can buy workspace slots from 4,99€ a month, and the premium plan includes one free Micro workspace. A slot does not include inference: agents use NaN models only on an inference plan. The workspaces guide in the docs has sizes, prices, network rules and how backups work." + }, { "q": "What payment methods do you accept?", "a": "Credit or debit card. New memberships are billed in euros from any region, taxes included; subscriptions opened earlier in US dollars keep their original price. No commitment: cancel anytime." diff --git a/i18n/es.json b/i18n/es.json index 6bcfda8..f7eb3cc 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -10,7 +10,7 @@ "notIncludedLabel": "Qué NO incluye", "notIncluded": { "0": "Acceso al servicio de inferencia (GPUs)", - "1": "Workspaces y despliegue de apps", + "1": "Un workspace incluido (los slots de workspace se venden aparte)", "2": "API key para llamadas a modelos" }, "faqUpgradeQ": "¿Cómo paso al plan de inferencia más adelante?", @@ -410,12 +410,30 @@ "Asignación de 3.000M de tokens por periodo de facturación", "400M de tokens por ventana deslizante de 4h: el límite que un agente de código alcanza primero", "Contexto de 1M · 10 peticiones en paralelo", + "Un workspace Micro gratis (1 vCPU · 2 GiB de RAM · 10 GiB de disco)", "Todo lo incluido en nan_member", "El acceso se activa pocos minutos después del pago" ], "premiumCond": "GLM 5.3 premium (200€/mes) se ofrece a miembros según haya plazas. Únete a la waitlist y te marcaremos para este tier.", "premiumCta": "Únete a la waitlist", - "premiumBadge": "glm 5.3 · premium" + "premiumBadge": "glm 5.3 · premium", + "workspaces": { + "label": "workspaces · complemento", + "title": "Tu propia máquina en la nube.", + "sub": "Una máquina de desarrollo dedicada y aislada, accesible por SSH y desde el navegador, con Pi, Hermes, gentle-shell y Herdr listos para usar. Cualquier miembro de pago puede comprar slots; cada slot mantiene un workspace en marcha.", + "per": "/mes", + "headSize": "tamaño", + "headSpecs": "vCPU · RAM · disco", + "headSlot": "slot", + "headBackups": "backups diarios", + "notes": [ + "Premium (200€) incluye un workspace Micro gratis.", + "Los slots pagan solo la máquina. Los agentes usan la inferencia de NaN solo con un plan de inferencia (70€ o 200€).", + "Los backups son opcionales: una instantánea al día, guardada 7 días, y la restauras tú.", + "Mensual, se factura en euros. Cancela un slot cuando quieras." + ], + "cta": "Lee la guía de workspaces" + } }, "who": { "label": "quién está detrás", @@ -471,6 +489,10 @@ "q": "¿Puedo entrenar modelos?", "a": "No. NaN es inferencia, no entrenamiento. Las GPUs están optimizadas para correr modelos existentes a alta velocidad, no para fine-tuning." }, + { + "q": "¿Qué es un workspace?", + "a": "Tu propia máquina en la nube para desarrollar: una máquina dedicada y aislada a la que entras por SSH o desde el navegador, con agentes de código como Pi y Hermes listos para usar. Cualquier miembro de pago puede comprar slots de workspace desde 4,99€ al mes, y el plan premium incluye un workspace Micro gratis. Un slot no incluye inferencia: los agentes usan los modelos de NaN solo con un plan de inferencia. La guía de workspaces de la documentación tiene los tamaños, los precios, las reglas de red y cómo funcionan los backups." + }, { "q": "¿Qué métodos de pago aceptáis?", "a": "Tarjeta de crédito o débito. Las altas nuevas se facturan en euros desde cualquier región, impuestos incluidos; las suscripciones abiertas antes en dólares mantienen su precio original. Sin compromiso: cancela cuando quieras." diff --git a/public/docs/workspaces/create-1-name-ssh.webp b/public/docs/workspaces/create-1-name-ssh.webp new file mode 100644 index 0000000..1796936 Binary files /dev/null and b/public/docs/workspaces/create-1-name-ssh.webp differ diff --git a/public/docs/workspaces/create-2-agents.webp b/public/docs/workspaces/create-2-agents.webp new file mode 100644 index 0000000..14830d2 Binary files /dev/null and b/public/docs/workspaces/create-2-agents.webp differ diff --git a/public/docs/workspaces/create-3-config.webp b/public/docs/workspaces/create-3-config.webp new file mode 100644 index 0000000..66aa760 Binary files /dev/null and b/public/docs/workspaces/create-3-config.webp differ diff --git a/public/docs/workspaces/phone-01-keychain-menu.webp b/public/docs/workspaces/phone-01-keychain-menu.webp new file mode 100644 index 0000000..63d7369 Binary files /dev/null and b/public/docs/workspaces/phone-01-keychain-menu.webp differ diff --git a/public/docs/workspaces/phone-02-generate-key.webp b/public/docs/workspaces/phone-02-generate-key.webp new file mode 100644 index 0000000..4b94014 Binary files /dev/null and b/public/docs/workspaces/phone-02-generate-key.webp differ diff --git a/public/docs/workspaces/phone-03-add-ssh-key.webp b/public/docs/workspaces/phone-03-add-ssh-key.webp new file mode 100644 index 0000000..a5799b9 Binary files /dev/null and b/public/docs/workspaces/phone-03-add-ssh-key.webp differ diff --git a/public/docs/workspaces/phone-04-new-host-menu.webp b/public/docs/workspaces/phone-04-new-host-menu.webp new file mode 100644 index 0000000..eb3f45f Binary files /dev/null and b/public/docs/workspaces/phone-04-new-host-menu.webp differ diff --git a/public/docs/workspaces/phone-05-host-address.webp b/public/docs/workspaces/phone-05-host-address.webp new file mode 100644 index 0000000..43fc92a Binary files /dev/null and b/public/docs/workspaces/phone-05-host-address.webp differ diff --git a/public/docs/workspaces/phone-06-host-credentials.webp b/public/docs/workspaces/phone-06-host-credentials.webp new file mode 100644 index 0000000..631fd10 Binary files /dev/null and b/public/docs/workspaces/phone-06-host-credentials.webp differ diff --git a/public/docs/workspaces/phone-07-connected.webp b/public/docs/workspaces/phone-07-connected.webp new file mode 100644 index 0000000..d2ebaa4 Binary files /dev/null and b/public/docs/workspaces/phone-07-connected.webp differ diff --git a/public/docs/workspaces/phone-08-herdr-computer.webp b/public/docs/workspaces/phone-08-herdr-computer.webp new file mode 100644 index 0000000..29e2837 Binary files /dev/null and b/public/docs/workspaces/phone-08-herdr-computer.webp differ diff --git a/public/docs/workspaces/phone-09-herdr-phone.webp b/public/docs/workspaces/phone-09-herdr-phone.webp new file mode 100644 index 0000000..a697458 Binary files /dev/null and b/public/docs/workspaces/phone-09-herdr-phone.webp differ diff --git a/src/components/docs/Screenshot.astro b/src/components/docs/Screenshot.astro new file mode 100644 index 0000000..558df7d --- /dev/null +++ b/src/components/docs/Screenshot.astro @@ -0,0 +1,54 @@ +--- +/** + * A product screenshot with a caption. + * + * A component rather than raw `
`/`` in the MDX: the text extractor + * (src/lib/mdxToText.ts) only understands headings and a handful of inline + * tags, so raw block HTML would throw and take /api/docs down for the page. + * Here the extractor serves it as a markdown image plus an italic caption. + * + * Lazy and async like every other docs image, with its intrinsic size so the + * page does not reflow while it loads. + */ +interface Props { + src: string; + alt: string; + width: number; + height: number; + caption?: string; + /** A phone screen: shown narrower, the way it looks in the hand. */ + variant?: 'default' | 'phone'; +} + +const { src, alt, width, height, caption, variant = 'default' } = Astro.props; +--- + +
+ {alt} + {caption &&
{caption}
} +
+ + diff --git a/src/components/nan/home/Pricing.astro b/src/components/nan/home/Pricing.astro index 55bdf44..a498108 100644 --- a/src/components/nan/home/Pricing.astro +++ b/src/components/nan/home/Pricing.astro @@ -56,6 +56,21 @@ const tiers = [ primary: false, }, ]; + +// Workspace slots, sold to any paying member (helmcode/nan workspaces launch). +// Sizes mirror cloud-api's tier catalogue (internal/workspace/tiers.go); the +// prices are the live EUR Stripe prices of the slot and of the backup add-on +// (cmd/stripe-bootstrap/workspace_backups.go). The premium plan's free +// workspace is the Micro size. +const workspaceSlots = [ + { size: 'micro', vcpu: 1, ram: 2, disk: 10, slot: '4,99€', backups: '0,99€' }, + { size: 'nano', vcpu: 2, ram: 4, disk: 20, slot: '8,99€', backups: '1,49€' }, + { size: 'basic', vcpu: 4, ram: 8, disk: 40, slot: '16,99€', backups: '2,49€' }, + { size: 'medium', vcpu: 8, ram: 16, disk: 80, slot: '32,99€', backups: '3,99€' }, + { size: 'large', vcpu: 8, ram: 32, disk: 160, slot: '60,99€', backups: '6,99€' }, +]; +const ws = tt.workspaces; +const wsDocs = lang === 'es' ? '/es/docs/workspaces' : '/docs/workspaces'; ---
@@ -98,6 +113,43 @@ const tiers = [ ))} + +
+
+ {ws.label} +

{ws.title}

+

{ws.sub}

+
+ +
+ + + + + + + + + + + {workspaceSlots.map((row) => ( + + + + + + + ))} + +
{ws.headSize}{ws.headSpecs}{ws.headSlot}{ws.headBackups}
{row.size}{row.vcpu} · {row.ram} GiB · {row.disk} GiB{row.slot}{ws.per}+{row.backups}{ws.per}
+
+ +
    + {ws.notes.map((note) =>
  • {note}
  • )} +
+ + {ws.cta} +
@@ -217,10 +269,84 @@ const tiers = [ min-height: 3em; /* reserva 2 líneas -> condición de altura igual en los 3 tiers */ } + /* Workspace slots: an add-on to the tiers above, so a table, not a fourth card. */ + .pws { + max-width: 1080px; + margin: var(--space-16) auto 0; + padding: var(--space-8) var(--space-6); + border: 1px solid var(--color-line); + background: var(--color-surface); + } + .pws__label { font-family: var(--font-mono); font-size: 12px; color: var(--color-text-dim); } + .pws__title { + margin-top: var(--space-3); + font-family: var(--font-serif); + font-weight: 400; + color: var(--color-text); + font-size: clamp(28px, 3vw, 44px); + line-height: 1.05; + } + .pws__sub { + margin-top: var(--space-4); + max-width: 64ch; + color: var(--color-text); + font-size: 15px; + line-height: 1.6; + } + /* Only the table scrolls on a narrow phone, never the page. */ + .pws__tablewrap { margin-top: var(--space-8); overflow-x: auto; } + .pws__table { + width: 100%; + border-collapse: collapse; + font-family: var(--font-mono); + font-size: 13px; + color: var(--color-text); + } + .pws__table th, + .pws__table td { + text-align: left; + padding: var(--space-3) var(--space-4) var(--space-3) 0; + border-bottom: 1px solid var(--color-line); + white-space: nowrap; + } + .pws__table thead th { font-weight: 400; color: var(--color-muted); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; } + .pws__table tbody th { font-weight: 600; } + .pws__table strong { font-weight: 700; } + .pws__per { color: var(--color-text-dim); margin-left: 2px; } + .pws__notes { + list-style: none; + margin: var(--space-6) 0 0; + padding: 0; + display: grid; + gap: var(--space-2); + } + .pws__notes li { + position: relative; + padding-left: 18px; + font-size: 14px; + line-height: 1.5; + color: var(--color-text); + } + .pws__notes li::before { + content: ''; + position: absolute; + left: 0; + top: 0.55em; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-violet); + } + .pws__cta { margin-top: var(--space-6); } + @media (max-width: 900px) { .pricing { padding-block: var(--space-16); } } @media (max-width: 640px) { .ptier { min-height: 0; } + .pws { padding: var(--space-6) var(--space-4); } + .pws__table { font-size: 12px; } + .pws__table th, + .pws__table td { padding-right: var(--space-3); } } diff --git a/src/content/docs-es/apps.md b/src/content/docs-es/apps.md index 76c9be4..09529d0 100644 --- a/src/content/docs-es/apps.md +++ b/src/content/docs-es/apps.md @@ -1,7 +1,7 @@ --- title: Apps description: Despliega tus apps desde GitHub a NaN Cloud en minutos. -order: 19 +order: 20 group: Guías --- diff --git a/src/content/docs-es/examples.md b/src/content/docs-es/examples.md index 99edf8e..c1cf3ff 100644 --- a/src/content/docs-es/examples.md +++ b/src/content/docs-es/examples.md @@ -1,7 +1,7 @@ --- title: Ejemplos description: Fragmentos de código para conectarte a la API de NaN con Python, Node.js, curl y más. -order: 18 +order: 19 group: Guías --- diff --git a/src/content/docs-es/intro.md b/src/content/docs-es/intro.md index 1e8c728..1d1fb3a 100644 --- a/src/content/docs-es/intro.md +++ b/src/content/docs-es/intro.md @@ -30,5 +30,6 @@ Los límites van por API key — un tope de peticiones por minuto y un máximo d - [CLI de NaN](/es/docs/nan-cli): la herramienta oficial de terminal, que además configura varias de ellas por ti. - [Referencia de la API](/es/docs/api): todos los endpoints, campo a campo. - [Modelos](/es/docs/models): las fichas técnicas y los límites. +- [Workspaces](/es/docs/workspaces): tu propia máquina en la nube, por SSH y en el navegador, con agentes de código listos para usar. - [Ejemplos](/es/docs/examples): fragmentos en Python, Node.js y curl. - Soporte: reporta incidencias en `#support` de Discord. diff --git a/src/content/docs-es/workspaces.mdx b/src/content/docs-es/workspaces.mdx new file mode 100644 index 0000000..8863eff --- /dev/null +++ b/src/content/docs-es/workspaces.mdx @@ -0,0 +1,311 @@ +--- +title: Workspaces +description: Tu propia máquina en la nube para desarrollar, accesible por SSH y desde el navegador, con agentes de código listos para usar. +order: 18 +group: Guías +--- + +import Steps from '../../components/docs/Steps.astro'; +import Details from '../../components/docs/Details.astro'; +import Callout from '../../components/docs/Callout.astro'; +import Screenshot from '../../components/docs/Screenshot.astro'; + +# Workspaces. + +Un **workspace** es tu propia máquina en la nube: un Linux dedicado y aislado, con su CPU, su memoria y su disco, que sigue encendido cuando cierras el portátil. Entras por SSH desde tu terminal o desde el navegador, y viene con los agentes de código que elijas ya instalados: [Pi](/es/docs/pi), [Hermes](/es/docs/hermes), gentle-shell y Herdr. + +Todo se gestiona desde [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces): crear, arrancar, parar, conectarte, instalar software, activar backups y borrar. + +## Quién puede tener uno + +Cualquier **miembro de pago** de NaN puede comprar slots de workspace: comunidad (14,99€), inferencia (70€) y premium (200€). + +| Tu plan | Workspace gratis | Puede comprar slots | Los agentes usan la inferencia de NaN | +|---|---|---|---| +| Comunidad, 14,99€ | No | Sí | No | +| Inferencia, 70€ | No | Sí | Sí | +| Premium, 200€ | **Uno, incluido** | Sí | Sí | + +Un slot paga **solo la máquina**. No incluye inferencia: los agentes de tu workspace llaman a los modelos de NaN solo si tu membresía es un plan de inferencia (70€ o 200€). Con el plan de comunidad puedes usar el workspace igualmente como máquina de desarrollo y apuntar los agentes a cualquier proveedor que ya tengas. + +## Tamaños y precios + +Cada slot mantiene **un** workspace de su tamaño en marcha. Compra los slots que quieras, mezclando tamaños. Los precios son mensuales y se facturan en euros. + +| Tamaño | vCPU | RAM | Disco | Slot | Backups (opcional) | +|---|---|---|---|---|---| +| Micro | 1 | 2 GiB | 10 GiB | 4,99€ / mes | 0,99€ / mes | +| Nano | 2 | 4 GiB | 20 GiB | 8,99€ / mes | 1,49€ / mes | +| Basic | 4 | 8 GiB | 40 GiB | 16,99€ / mes | 2,49€ / mes | +| Medium | 8 | 16 GiB | 80 GiB | 32,99€ / mes | 3,99€ / mes | +| Large | 8 | 32 GiB | 160 GiB | 60,99€ / mes | 6,99€ / mes | + +El workspace gratis del plan premium es del tamaño **Micro** (1 vCPU, 2 GiB de RAM, 10 GiB de disco). Sus backups cuestan lo mismo que los de Micro, 0,99€ / mes. + +Los slots se reutilizan: al borrar un workspace su slot queda libre y puedes crear otro workspace en él al momento. + +## Crea tu workspace + +El asistente de creación tiene cuatro pasos: **Name & SSH Key**, **Agent Selection**, **Agent Config** y **Review & Create**. + + + +### Consigue un slot o usa tu workspace gratis + +Entra en [Workspaces](https://cloud.nan.builders/workspaces) y empieza un workspace nuevo. Con el plan premium, el asistente te indica arriba que este es **tu workspace gratis incluido** (Micro: 1 vCPU, 2 GiB de RAM, 10 GiB de disco). Si quieres más espacio, o no tienes premium, sigue **Need a bigger workspace? Buy a slot**: elige un tamaño, paga con Stripe y vuelves al asistente con el slot listo. + +### Nombre y clave SSH + +Ponle nombre al workspace: **minúsculas, números y guiones, 20 caracteres como máximo**. Después pega tu clave SSH **pública**, la línea que empieza por `ssh-ed25519` y termina en algo como `tu@portatil`. + +Para ver la que ya tienes: + +```bash +cat ~/.ssh/id_ed25519.pub +``` + +Si ese archivo no existe, crea primero una clave (pulsa Enter para aceptar los valores por defecto) y vuelve a ejecutar el `cat`: + +```bash +ssh-keygen -t ed25519 +``` + +Nunca pegues el archivo sin `.pub`: esa es tu clave privada. Más adelante puedes añadir más claves, una por dispositivo, desde la pestaña **SSH keys**. + + + +### Elige tus agentes + +Marca los agentes y herramientas que quieras, cada uno con la versión que se va a instalar: + +- **Hermes**: un agente siempre encendido, con memoria, skills y Telegram. +- **Pi**: un agente de código para la terminal. +- **Herdr**: lanza y vigila varias sesiones de agentes en paralelo. + +Nada de esto es definitivo: puedes instalar, actualizar o quitar cualquiera de ellos después desde el panel de **Software**. + + + +### Configúralos + +Cada agente que hayas elegido tiene su propio bloque: + +- **Hermes**: el **modelo** que usa (con un plan de inferencia), un **token de bot de Telegram** si quieres hablar con él desde Telegram (opcional; crea un bot con [@BotFather](https://t.me/BotFather) y pega el token que te da) y su **soul**, el prompt de sistema que fija su personalidad e instrucciones (opcional). +- **Pi**: un **nombre** (por defecto, el del workspace), el **modelo** y **Install gentle-shell**, que añade un comando `gentle-shell` aparte con subagentes, guardarraíles y memoria persistente que se queda dentro de tu workspace. `pi` sigue igual. + + + +### Revisa y crea + +El último paso lo resume todo: la clave SSH, los agentes, los recursos y los modelos. Revísalo y pulsa **Create Workspace**. El workspace arranca en unos segundos. Si en ese momento la plataforma está llena, espera a que haya capacidad y arranca solo: no tienes que hacer nada. + + + +## Conéctate desde el móvil y mantén tus agentes 24/7 con Herdr + +Tu workspace sigue encendido cuando cierras el portátil, así que tus agentes pueden seguir trabajando mientras no estás. Con una app de SSH en el móvil y Herdr puedes ver cómo van, contestarles y retomar exactamente la misma sesión desde cualquier dispositivo. + + + +### Instala una app de SSH en el móvil + +Vale cualquier cliente SSH. Te recomendamos [Termius](https://termius.com/download), disponible para Android e iOS. + +### Genera una clave en Termius + +Ve a **Vaults**, luego a **Keychain**, pulsa **+** y elige **Generate Key**. + + + +### Ponle un nombre + +Rellena solo el **Label** (por ejemplo, el nombre de tu móvil) y confirma. Deja el resto como está: el tipo es ED25519. + + + +### Añade la clave pública a tu workspace + +Selecciona la clave que acabas de crear. Puedes enviártela por email o copiar la clave **pública**. Después entra en [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces), abre tu workspace y ve a la pestaña **SSH keys**. Pega la clave pública en **Add a key**, ponle un nombre y pulsa **+ Add key**. + + +Comparte y pega solo la clave **pública**. La clave privada nunca sale de tu móvil. + + + + +### Crea un host en Termius + +Ve a **Hosts** y añade un host nuevo. + + + +### Rellena los datos de conexión + +| Campo | Valor | +|---|---| +| Label | Lo que quieras | +| IP or Hostname | Siempre `ssh.nan.builders` | +| Port | `30222` | +| Username | Lo que va antes de la `@` en el comando SSH de tu workspace, en la pestaña **SSH keys** (el panel también puede mostrarlo como Username) | +| Key (SSH ID, Key, Certificate, FIDO2) | La clave que generaste antes | + +Deja todo lo demás como está: **Use SSH** activado y **Use Mosh** desactivado. La conexión solo funciona por SSH. + + + + + +### Conéctate + +En la lista de hosts, pulsa el host que has creado. Ya estás dentro de tu workspace. + + + +### Lanza tus agentes dentro de Herdr + +Ya puedes hablar con tus agentes. Arranca primero [Herdr](https://herdr.dev) y lanza dentro Pi, gentle-shell o cualquier agente tuyo: + +```bash +herdr +``` + +Los agentes siguen funcionando dentro de Herdr 24/7, aunque cierres la app o pierdas la cobertura. Vuelve a conectarte desde cualquier dispositivo y ejecuta `herdr` otra vez para volver a entrar: aterrizas exactamente en la misma sesión. Por ejemplo, arranca gentle-shell desde el ordenador y sigue con ella desde el móvil. + + + + + + + +Algunos consejos: + +- El primer `herdr` crea una sesión; cada `herdr` posterior vuelve a entrar en ella. Perder la conexión no para a tus agentes. +- Usa SSH normal, no Mosh: la conexión a tu workspace es solo por SSH. +- Las claves del móvil también valen: se aceptan claves ED25519, RSA y ECDSA (P-256), así que puedes usar la que ha generado la app. + +## Conectarte + +### SSH desde tu terminal + +La pestaña **Overview** muestra el comando SSH exacto de tu workspace, con un botón para copiarlo. Tiene esta forma: + +```bash +ssh @ssh.nan.builders -p 30222 +``` + +Cópialo del panel en vez de escribirlo a mano. La conexión pasa por la pasarela SSH de NaN, que es la única entrada desde fuera. + +### Claves SSH + +La pestaña **SSH keys** guarda las claves públicas que pueden entrar en ese workspace (hasta 10). Añade una por cada dispositivo desde el que te conectes y quita las que ya no uses. Quitar una clave bloquea las conexiones nuevas con ella; las sesiones ya abiertas siguen hasta que terminan. + + +La pasarela comprueba tu clave contra la lista de la pestaña SSH keys. Editar `~/.ssh/authorized_keys` dentro del workspace **no tiene ningún efecto** en el acceso por SSH. Nunca pegues una clave privada: solo el archivo `.pub`. + + +### Terminal web + +La pestaña **Console** abre una terminal en el navegador. Funciona sin clave SSH, así que también es la forma de volver a entrar si quitaste todas tus claves o estás en un equipo sin tu clave. + +## Agentes y el panel de Software + +El panel **Software**, en la pestaña Overview del workspace, lista los agentes y herramientas instalados y sus versiones. Desde ahí puedes: + +- **Instalar** un agente que no elegiste al crear el workspace. +- **Actualizar** uno, o todos a la vez, cuando sale una versión nueva. Las actualizaciones conservan tus archivos, sesiones y ajustes. +- **Quitar** uno que ya no uses. + +gentle-shell es un complemento de Pi (memoria entre sesiones con Engram), así que necesita Pi instalado. Si tu Pi es demasiado antiguo, el panel te ofrece actualizarlo primero. + +Con un plan de inferencia, los agentes vienen configurados para usar los modelos de NaN con tu membresía. Las guías de [Pi](/es/docs/pi) y [Hermes](/es/docs/hermes) explican cómo cambiar el modelo o el proveedor a mano. + +## Copias de seguridad + +Los workspaces **no tienen copias de seguridad** salvo que actives el **complemento de backups** de ese workspace, desde su pestaña **Backups**. + +- Se toma una instantánea del disco del workspace **una vez al día** y se conserva **7 días**. +- Puedes **restaurar** cualquiera de esas instantáneas tú mismo desde la pestaña Backups, hasta **3 veces por workspace en cualquier periodo de 24 horas**. No hay copias bajo demanda. +- Restaurar devuelve todo lo que había en el disco en ese momento, incluido cualquier acceso que hayas configurado tú dentro de la máquina. Las claves de la pestaña **SSH keys** no están en el disco, así que restaurar no las cambia. Las instantáneas son consistentes ante caídas: restaurar una equivale a que la máquina se quede sin corriente en ese momento. +- Activar el complemento cobra el primer pago al momento (prorrateado hasta la renovación de tu slot cuando se añade a un slot). Desactivarlo lo quita al instante, **sin reembolso** del mes en curso, y **borra todas sus instantáneas**. +- Si el workspace entra en un periodo de gracia, no se toman nuevas instantáneas y las existentes se conservan hasta que se elimine el workspace. Cuando el workspace vuelve a estar al día, vuelve a activar el complemento en un plazo de **14 días** o sus instantáneas se borran. + +Las reglas completas están en los [términos del servicio](/es/terms#workspace-backups). + +## Reglas de red + +Tu workspace puede salir a internet para el trabajo de desarrollo normal: + +- **Salida permitida:** HTTP y HTTPS (puertos 80 y 443) y SSH (puerto 22). Eso cubre `git`, `npm`, `pip`, `cargo`, los gestores de paquetes del sistema y las llamadas a cualquier API. +- **Bloqueado:** todo lo demás, incluido el **correo saliente (SMTP)**. Un workspace no puede enviar correo directamente; usa la API de un proveedor de email por HTTPS. +- **Límites de ritmo:** las conexiones salientes nuevas tienen un límite por workspace, más bajo para el SSH saliente. El uso normal nunca llega a ellos; un escaneo sí. +- **Entrada:** solo se entra por la pasarela SSH de NaN y por la pestaña Console. Los workspaces no tienen puertos abiertos a internet. +- **Túneles inversos** (por ejemplo Tailscale, cloudflared o ngrok) están permitidos para tu propio acceso y desarrollo, dentro de la [política de uso aceptable](/es/terms#workspace-acceptable-use). + + +Nada de minar criptomonedas, escanear ni atacar a nadie, spam, phishing ni malware, proxies abiertos, VPN ni nodos de salida de Tor, ni revender la máquina. El abuso supone la suspensión o el borrado sin aviso y sin reembolso. Lee la [política de uso aceptable](/es/terms#workspace-acceptable-use) antes de empezar. + + +## Ciclo de vida: gracia y borrado + +Un workspace nunca se borra en el momento en que se deja de pagar. Primero se **para** y empieza un **periodo de gracia de 7 días**. La página del workspace muestra la fecha exacta en que se borrarán sus datos. + +| Qué pasa | Qué supone para el workspace | Cómo conservarlo | +|---|---|---| +| Cancelas un slot | Sigue en marcha hasta el final del mes pagado. Después se para y se borra 7 días más tarde. | Compra un slot nuevo **del mismo tamaño** antes de la fecha indicada. Un slot de otro tamaño empieza vacío. | +| Falla el pago de un slot | Se para y empiezan los 7 días de gracia. | Actualiza tu método de pago en el portal de facturación antes de la fecha indicada. | +| Tu plan premium termina (lo cancelas o cambias de plan) | Tu workspace gratis se para y se borra 7 días más tarde. | Vuelve a premium o compra un slot **Micro**: tu workspace gratis pasa a ese slot con sus datos. | +| El workspace se suspende por abuso | Se para al momento. | Consulta la [política de uso aceptable](/es/terms#workspace-acceptable-use). | + +Durante el periodo de gracia puedes iniciar una **sesión de rescate**: el workspace arranca durante un máximo de 2 horas (hasta 3 veces al día, nunca más allá de la fecha de borrado) para que copies tus datos por SSH o desde la pestaña Console. + +Cuando termina el periodo de gracia, el workspace, su disco y sus copias de seguridad se **borran de forma permanente**. No podemos recuperarlos. + +## Preguntas frecuentes + +
+ +No. Cada workspace es una máquina dedicada y aislada, con su CPU, su memoria y su disco. Los demás miembros no pueden verlo ni llegar a él. + +
+ +
+ +No. Un slot paga la máquina. Los agentes usan los modelos de NaN solo si tu membresía es un plan de inferencia (70€ o 200€). Con el plan de comunidad puedes instalar los agentes igualmente y conectarlos a otro proveedor. + +
+ +
+ +Sí. Compra un slot por workspace, mezclando tamaños. Los miembros premium tienen además un workspace Micro gratis aparte de sus slots. + +
+ +
+ +No sobre la marcha. Compra un slot del tamaño nuevo, crea un workspace en él, copia tus datos y borra el antiguo. Al cancelar el slot antiguo, su facturación termina al final del mes. + +
+ +
+ +Sí. Los slots de workspace se facturan en euros, así que el portal pasa primero tu plan de comunidad a euros (14,99€ / mes) y luego te deja comprar. Te lo avisa antes de cobrar nada. + +
+ +
+ +Por SMTP no: los puertos de correo saliente están bloqueados. Usa la API por HTTPS de un proveedor de email. + +
+ +
+ +Los workspaces no tienen puertos públicos. Para tu propio acceso y tus pruebas puedes usar un túnel inverso. Para publicar una app para otras personas, usa [Apps](/es/docs/apps). + +
+ +
+ +Escribe en `#support` en Discord. + +
diff --git a/src/content/docs/apps.md b/src/content/docs/apps.md index c3124b8..1dd74bf 100644 --- a/src/content/docs/apps.md +++ b/src/content/docs/apps.md @@ -1,7 +1,7 @@ --- title: Apps description: Deploy your apps from GitHub to NaN Cloud in minutes. -order: 19 +order: 20 group: Guides --- diff --git a/src/content/docs/examples.md b/src/content/docs/examples.md index 6094bb4..3a4ab7d 100644 --- a/src/content/docs/examples.md +++ b/src/content/docs/examples.md @@ -1,7 +1,7 @@ --- title: Examples description: Code snippets to connect to the NaN API with Python, Node.js, curl, and more. -order: 18 +order: 19 group: Guides --- diff --git a/src/content/docs/intro.md b/src/content/docs/intro.md index fe7eba1..979ead2 100644 --- a/src/content/docs/intro.md +++ b/src/content/docs/intro.md @@ -30,5 +30,6 @@ Limits are per API key — a cap on requests per minute and a maximum number of - [NaN CLI](/docs/nan-cli): the official terminal tool, which also configures several of them for you. - [API reference](/docs/api): every endpoint, field by field. - [Models](/docs/models): the spec sheets and the limits. +- [Workspaces](/docs/workspaces): your own cloud machine, over SSH and in the browser, with coding agents ready to use. - [Examples](/docs/examples): snippets in Python, Node.js and curl. - Support: report issues in `#support` on Discord. diff --git a/src/content/docs/workspaces.mdx b/src/content/docs/workspaces.mdx new file mode 100644 index 0000000..9ad10fe --- /dev/null +++ b/src/content/docs/workspaces.mdx @@ -0,0 +1,311 @@ +--- +title: Workspaces +description: Your own cloud machine for development, reachable over SSH and from the browser, with coding agents ready to use. +order: 18 +group: Guides +--- + +import Steps from '../../components/docs/Steps.astro'; +import Details from '../../components/docs/Details.astro'; +import Callout from '../../components/docs/Callout.astro'; +import Screenshot from '../../components/docs/Screenshot.astro'; + +# Workspaces. + +A **workspace** is your own machine in the cloud: a dedicated, isolated Linux box with its own CPU, memory and disk, that stays on when you close your laptop. You reach it over SSH from your terminal or from the browser, and it comes with the coding agents you choose already installed: [Pi](/docs/pi), [Hermes](/docs/hermes), gentle-shell and Herdr. + +Everything is managed from [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces): create, start, stop, connect, install software, turn on backups and delete. + +## Who can get one + +Any **paying member** of NaN can buy workspace slots: community (14,99€), inference (70€) and premium (200€). + +| Your plan | Free workspace | Can buy slots | Agents use NaN inference | +|---|---|---|---| +| Community, 14,99€ | No | Yes | No | +| Inference, 70€ | No | Yes | Yes | +| Premium, 200€ | **One, included** | Yes | Yes | + +A slot pays for the **machine only**. It does not include inference: the agents in your workspace call NaN models only if your membership is an inference plan (70€ or 200€). On the community plan you can still use the workspace as a dev box, and point the agents at any provider you already have. + +## Sizes and prices + +Each slot keeps **one** workspace of its size running. Buy as many slots as you want, in any mix of sizes. Prices are monthly and billed in euros. + +| Size | vCPU | RAM | Disk | Slot | Backups (optional) | +|---|---|---|---|---|---| +| Micro | 1 | 2 GiB | 10 GiB | 4,99€ / month | 0,99€ / month | +| Nano | 2 | 4 GiB | 20 GiB | 8,99€ / month | 1,49€ / month | +| Basic | 4 | 8 GiB | 40 GiB | 16,99€ / month | 2,49€ / month | +| Medium | 8 | 16 GiB | 80 GiB | 32,99€ / month | 3,99€ / month | +| Large | 8 | 32 GiB | 160 GiB | 60,99€ / month | 6,99€ / month | + +The free workspace that comes with the premium plan is the **Micro** size (1 vCPU, 2 GiB RAM, 10 GiB disk). Its backups cost the Micro price, 0,99€ / month. + +Slots are reusable: deleting a workspace frees its slot, and you can create a new workspace on it right away. + +## Create your workspace + +The create wizard has four steps: **Name & SSH Key**, **Agent Selection**, **Agent Config** and **Review & Create**. + + + +### Get a slot, or use your free workspace + +Open [Workspaces](https://cloud.nan.builders/workspaces) and start a new workspace. On the premium plan, the wizard tells you at the top that this one is **your included free workspace** (Micro: 1 vCPU, 2 GiB RAM, 10 GiB disk). If you want more room, or you are not on premium, follow **Need a bigger workspace? Buy a slot**: pick a size, pay with Stripe, and you come back to the wizard with the slot ready. + +### Name and SSH key + +Give the workspace a name: **lowercase letters, numbers and hyphens, 20 characters at most**. Then paste your SSH **public** key, the line that starts with `ssh-ed25519` and ends in something like `you@laptop`. + +To see the one you already have: + +```bash +cat ~/.ssh/id_ed25519.pub +``` + +If that file does not exist, create a key first (press Enter to accept the defaults) and run the `cat` again: + +```bash +ssh-keygen -t ed25519 +``` + +Never paste the file without `.pub`: that is your private key. You can add more keys later, one per device, from the **SSH keys** tab. + + + +### Choose your agents + +Tick the agents and tools you want, each with the version that will be installed: + +- **Hermes**: an always-on agent with memory, skills and Telegram. +- **Pi**: a coding agent for the terminal. +- **Herdr**: runs and watches several agent sessions side by side. + +Nothing here is final: you can install, update or remove any of them later from the **Software** panel. + + + +### Configure them + +Each agent you picked gets its own block: + +- **Hermes**: the **model** it uses (on an inference plan), a **Telegram bot token** if you want to talk to it from Telegram (optional; create a bot with [@BotFather](https://t.me/BotFather) and paste the token it gives you), and its **soul**, the system prompt that sets its personality and instructions (optional). +- **Pi**: a **name** (it defaults to the workspace name), the **model**, and **Install gentle-shell**, which adds a separate `gentle-shell` command with subagents, guardrails and persistent memory that stays inside your workspace. `pi` itself stays unchanged. + + + +### Review and create + +The last step sums it all up: the SSH key, the agents, the resources and the models. Check it and press **Create Workspace**. The workspace starts in a few seconds. If the platform is full at that moment, it waits for capacity and starts by itself: you do not need to do anything. + + + +## Connect from your phone and keep agents running 24/7 with Herdr + +Your workspace stays on when you close your laptop, so your agents can keep working while you are away. With an SSH app on your phone and Herdr you can check on them, answer them and pick up the very same session from any device. + + + +### Install an SSH app on your phone + +Any SSH client works. We recommend [Termius](https://termius.com/download), available for Android and iOS. + +### Generate a key in Termius + +Go to **Vaults**, then **Keychain**, tap **+** and choose **Generate Key**. + + + +### Give it a name + +Fill in only the **Label** (for example, the name of your phone) and confirm. Leave the rest as it is: the type is ED25519. + + + +### Add the public key to your workspace + +Select the key you just created. You can email it to yourself or copy the **public** key. Then open [cloud.nan.builders/workspaces](https://cloud.nan.builders/workspaces), go into your workspace and open the **SSH keys** tab. Paste the public key under **Add a key**, give it a name and click **+ Add key**. + + +Share and paste only the **public** key. The private key never leaves your phone. + + + + +### Create a host in Termius + +Go to **Hosts** and add a new host. + + + +### Fill in the connection details + +| Field | Value | +|---|---| +| Label | Anything you like | +| IP or Hostname | Always `ssh.nan.builders` | +| Port | `30222` | +| Username | The part before the `@` in your workspace's SSH command, in the **SSH keys** tab (the dashboard may also show it as Username) | +| Key (SSH ID, Key, Certificate, FIDO2) | The key you generated before | + +Leave everything else as it is: **Use SSH** on and **Use Mosh** off. The connection only works over SSH. + + + + + +### Connect + +In the hosts list, tap the host you created. You are now in your workspace. + + + +### Run your agents inside Herdr + +Now you can talk to your agents. Start [Herdr](https://herdr.dev) first, and run Pi, gentle-shell or any agent of your own inside it: + +```bash +herdr +``` + +The agents keep running inside Herdr 24/7, even when you close the app or lose signal. Reconnect from any device and run `herdr` again to reattach: you land in exactly the same session. For example, start gentle-shell from your computer and carry on with it from your phone. + + + + + + + +A few tips: + +- The first `herdr` starts a session; every later `herdr` reattaches to it. Losing the connection does not stop your agents. +- Use plain SSH, not Mosh: the connection to your workspace is SSH only. +- Phone keys work too: ED25519, RSA and ECDSA (P-256) keys are accepted, so you can use the one the app generated. + +## Connect + +### SSH from your terminal + +The **Overview** tab shows the exact SSH command for your workspace, with a copy button. It looks like this: + +```bash +ssh @ssh.nan.builders -p 30222 +``` + +Copy it from the dashboard rather than typing it. The connection goes through the NaN SSH gateway, which is the only way in from outside. + +### SSH keys + +The **SSH keys** tab holds the public keys allowed to log in to that workspace (up to 10). Add one per device you connect from, and remove the ones you no longer use. Removing a key blocks new connections with it; sessions already open stay open until they end. + + +The gateway checks your key against the list in the SSH keys tab. Editing `~/.ssh/authorized_keys` inside the workspace has **no effect** on SSH logins. Never paste a private key: only the `.pub` file. + + +### Web terminal + +The **Console** tab opens a terminal in the browser. It works without any SSH key, so it is also the way back in if you removed all your keys or are on a machine without your key. + +## Agents and the Software panel + +The **Software** panel on the workspace's Overview tab lists the agents and tools installed and their versions. From there you can: + +- **Install** an agent you did not pick when creating the workspace. +- **Update** one, or everything at once, when a new version is out. Updates keep your files, sessions and settings. +- **Remove** one you no longer use. + +gentle-shell is an add-on for Pi (memory between sessions with Engram), so it needs Pi installed. If your Pi is too old for it, the panel offers to update Pi first. + +On an inference plan, the agents come set up to use NaN models with your membership. The setup guides for [Pi](/docs/pi) and [Hermes](/docs/hermes) explain how to change the model or the provider by hand. + +## Backups + +Workspaces have **no backups** unless you turn on the **backup add-on** for that workspace, from its **Backups** tab. + +- A snapshot of the workspace disk is taken **once a day** and kept for **7 days**. +- You can **restore** any of those snapshots yourself from the Backups tab, up to **3 times per workspace in any 24 hours**. There are no on-demand backups. +- Restoring brings back everything that was on the disk at that time, including any access you configured yourself inside the machine. The keys in the **SSH keys** tab are not on the disk, so a restore does not change them. Snapshots are crash-consistent: restoring one is like the machine losing power at that moment. +- Turning the add-on on charges the first payment right away (prorated to your slot's renewal date when it is added to a slot). Turning it off removes it at once, with **no refund** for the current month, and **deletes all its snapshots**. +- If the workspace enters a grace period, no new snapshots are taken and the existing ones are kept until the workspace is deleted. Once the workspace is back in good standing, turn the add-on on again within **14 days** or its snapshots are deleted. + +The full rules are in the [terms of service](/terms#workspace-backups). + +## Network rules + +Your workspace can reach the internet for normal development work: + +- **Allowed outbound:** HTTP and HTTPS (ports 80 and 443) and SSH (port 22). That covers `git`, `npm`, `pip`, `cargo`, system package managers and calls to any API. +- **Blocked:** everything else, including **outgoing email (SMTP)**. A workspace cannot send mail directly; use an email API over HTTPS instead. +- **Rate limits:** new outbound connections are limited per workspace, with a lower limit for outbound SSH. Normal use never hits them; scanning does. +- **Inbound:** the only way in is the NaN SSH gateway and the Console tab. Workspaces have no ports open to the internet. +- **Reverse tunnels** (for example Tailscale, cloudflared or ngrok) are allowed for your own access and development, within the [acceptable use policy](/terms#workspace-acceptable-use). + + +No crypto mining, no scanning or attacking anyone, no spam, phishing or malware, no open proxies, VPN or Tor exits, and no reselling the machine. Abuse means suspension or deletion without notice and without refund. Read the [acceptable use policy](/terms#workspace-acceptable-use) before you start. + + +## Lifecycle: grace and deletion + +A workspace is never deleted the moment a payment stops. It is **stopped** first, and a **7-day grace period** starts. The workspace page shows the exact date its data will be deleted. + +| What happens | What it means for the workspace | How to keep it | +|---|---|---| +| You cancel a slot | It keeps running until the end of the paid month. Then it is stopped and deleted 7 days later. | Buy a new slot **of the same size** before the date shown. A slot of another size starts empty. | +| A slot payment fails | It is stopped and the 7-day grace starts. | Update your payment method in the billing portal before the date shown. | +| Your premium plan ends (you cancel or move to another plan) | Your free workspace is stopped and deleted 7 days later. | Go back to premium, or buy a **Micro** slot: your free workspace moves onto it with its data. | +| The workspace is suspended for abuse | It is stopped at once. | See the [acceptable use policy](/terms#workspace-acceptable-use). | + +During a grace period you can start a **rescue session**: the workspace runs for up to 2 hours (up to 3 times a day, never past the deletion date) so you can copy your data out over SSH or the Console tab. + +When the grace period ends, the workspace, its disk and its backups are **deleted permanently**. We cannot recover them. + +## FAQ + +
+ +No. Each workspace is a dedicated, isolated machine with its own CPU, memory and disk. Other members cannot see or reach it. + +
+ +
+ +No. A slot pays for the machine. The agents use NaN models only if your membership is an inference plan (70€ or 200€). On the community plan you can still install the agents and connect them to another provider. + +
+ +
+ +Yes. Buy one slot per workspace, in any mix of sizes. Premium members also get one free Micro workspace on top of their slots. + +
+ +
+ +Not in place. Buy a slot of the new size, create a workspace on it, copy your data over and delete the old one. Canceling the old slot ends its billing at the end of the month. + +
+ +
+ +Yes. Workspace slots are billed in euros, so the portal first moves your community plan to euros (14,99€ / month) and then lets you buy. It tells you before charging anything. + +
+ +
+ +Not over SMTP: outgoing mail ports are blocked. Use the HTTPS API of an email provider. + +
+ +
+ +Workspaces have no public ports. For your own access and testing you can use a reverse tunnel. To publish an app for other people, use [Apps](/docs/apps). + +
+ +
+ +Write in `#support` on Discord. + +
diff --git a/src/lib/__fixtures__/screenshot.expected.md b/src/lib/__fixtures__/screenshot.expected.md new file mode 100644 index 0000000..5563158 --- /dev/null +++ b/src/lib/__fixtures__/screenshot.expected.md @@ -0,0 +1,9 @@ +Before. + +![The create form](/docs/example/shot.webp) + +*Step 1: name it.* + +![No caption](/docs/example/bare.webp) + +After. \ No newline at end of file diff --git a/src/lib/__fixtures__/screenshot.mdx b/src/lib/__fixtures__/screenshot.mdx new file mode 100644 index 0000000..3d659e8 --- /dev/null +++ b/src/lib/__fixtures__/screenshot.mdx @@ -0,0 +1,9 @@ +import Screenshot from '../../components/docs/Screenshot.astro'; + +Before. + + + + + +After. diff --git a/src/lib/mdxToText.test.ts b/src/lib/mdxToText.test.ts index 0cfe40f..3bbe318 100644 --- a/src/lib/mdxToText.test.ts +++ b/src/lib/mdxToText.test.ts @@ -19,6 +19,7 @@ const KNOWN_COMPONENTS = [ 'Steps', 'Details', 'BrandIntro', + 'Screenshot', ]; const FIXTURES = [ @@ -31,6 +32,7 @@ const FIXTURES = [ 'agentgrid', 'steps', 'details', + 'screenshot', 'raw-html-heading', 'raw-html-inline', 'composite', diff --git a/src/lib/mdxToText.ts b/src/lib/mdxToText.ts index ecddac5..b9bb74e 100644 --- a/src/lib/mdxToText.ts +++ b/src/lib/mdxToText.ts @@ -24,6 +24,7 @@ const KNOWN_COMPONENTS = new Set([ 'Steps', 'Details', 'BrandIntro', + 'Screenshot', ]); const AUTHOR_HTML_BLOCK_TAGS = new Set(['h1', 'h2', 'h3', 'h4']); @@ -268,6 +269,8 @@ function componentToBlockMd(node: MdxNode, rateLimits: RateLimitsConfig): string return detailsToMd(node); case 'BrandIntro': return brandIntroToMd(node); + case 'Screenshot': + return screenshotToMd(node); default: throw new Error(`Unknown MDX block component: <${name || '?'}>`); } @@ -418,6 +421,13 @@ function brandIntroToMd(node: MdxNode): string { return `![${alt}](${src})` + String.fromCharCode(10, 10) + inner + String.fromCharCode(10); } +function screenshotToMd(node: MdxNode): string { + const src = String(getAttr(node, 'src') ?? ''); + const alt = String(getAttr(node, 'alt') ?? ''); + const caption = stripInlineHtml(String(getAttr(node, 'caption') ?? '')).trim(); + return caption ? `![${alt}](${src})\n\n*${caption}*\n` : `![${alt}](${src})\n`; +} + function rateLimitsToMd(config: RateLimitsConfig): string { // The canonical text is English-only, so the labels resolve to the English // table: the per-key block and the card must not be able to drift apart. diff --git a/src/middleware.ts b/src/middleware.ts index fc480ea..a5086a7 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -95,8 +95,7 @@ function hackatonRedirect(context: Parameters[0]): Response | * `/docs/mcp` documentaba el servidor MCP de NaN, cuya única herramienta era la * búsqueda web; se retiraron los dos (el endpoint `/mcp` y `POST /v1/search`). * `/docs/agents` documentaba los agentes v1 que se gestionaban desde el panel; - * se retiraron y los sustituyen los Workspaces, que no tienen página de docs, así - * que se manda a la raíz de las docs (destino `''`). + * se retiraron y los sustituyen los Workspaces, así que se manda a su página. * * Los enlaces siguen circulando, así que se mandan con 301 en vez de dejar un * 404. Se conserva el prefijo `/es`, y cualquier subruta (las capturas de @@ -104,7 +103,7 @@ function hackatonRedirect(context: Parameters[0]): Response | */ export const REMOVED_DOCS: Readonly> = { mcp: 'agent-setup', - agents: '', + agents: 'workspaces', }; function removedDocsRedirect(context: Parameters[0]): Response | null { @@ -113,9 +112,7 @@ function removedDocsRedirect(context: Parameters[0]): Respons if (!m) return null; const [, prefix = '', slug] = m; if (!Object.hasOwn(REMOVED_DOCS, slug)) return null; - const target = REMOVED_DOCS[slug]; - const path = target ? `${prefix}/docs/${target}` : `${prefix}/docs`; - return context.redirect(`${path}${url.search}`, 301); + return context.redirect(`${prefix}/docs/${REMOVED_DOCS[slug]}${url.search}`, 301); } /** diff --git a/src/pages/es/terms.astro b/src/pages/es/terms.astro index d3a5454..686b0f3 100644 --- a/src/pages/es/terms.astro +++ b/src/pages/es/terms.astro @@ -2,7 +2,7 @@ import Legal from '../../layouts/NanLegal.astro'; --- - +

NaN está operada por Helmcode S.L. (NIF B44788511), Avenida Quinto Centenario 27, Santiago del Teide, España. Contacto: info@helmcode.com. @@ -35,8 +35,11 @@ import Legal from '../../layouts/NanLegal.astro'; (crash-consistent): restaurar una equivale a que la máquina se quede sin corriente en el momento en que se tomó, así que los datos que el workspace aún no había escrito en disco no se incluyen y las bases de datos en ejecución se recuperan como tras un corte de luz. Restaurar una instantánea recupera todo lo que había en el - disco en ese momento, incluido cualquier acceso que hayas configurado tú (por ejemplo claves SSH o ajustes del - servidor SSH que añadieras); la plataforma solo vuelve a aplicar tu clave SSH actual de NaN. Puedes restaurar + disco en ese momento, incluido cualquier acceso que hayas configurado tú dentro de la máquina (por ejemplo claves en + ~/.ssh/authorized_keys o ajustes del servidor SSH que añadieras). Restaurar no cambia las claves SSH que + gestionas para el workspace en la plataforma de miembros: no se guardan en el disco del workspace, así que tras + una restauración pueden entrar las claves que haya en esa lista en ese momento. Las claves añadidas dentro de la + máquina no tienen efecto en el acceso a través de la pasarela SSH de NaN. Puedes restaurar hasta 3 veces por workspace en cualquier periodo de 24 horas; no hay copias bajo demanda. Las instantáneas solo se pueden restaurar mientras el complemento esté activo: durante un periodo de gracia tienes que asociar el workspace a un slot activo (o renovar la membresía, si es un workspace gratuito) y volver a activar el @@ -54,6 +57,39 @@ import Legal from '../../layouts/NanLegal.astro'; Las instantáneas borradas no se pueden recuperar.

+

Uso aceptable de los workspaces

+

+ Este apartado se aplica desde el 6 de octubre de 2026. Los workspaces son máquinas personales para tu propio + trabajo de desarrollo. Eres responsable de todo lo que se ejecuta en tu workspace y de todo el tráfico que envía, + incluido lo que los agentes ejecuten en tu nombre. No puedes usar un workspace para: +

+
    +
  • minar criptomonedas;
  • +
  • escanear, sondear o atacar sistemas de terceros, workspaces de otros miembros o nuestra propia infraestructura, ni intentar acceder a ellos sin autorización;
  • +
  • lanzar ataques de denegación de servicio (DDoS) o participar en ellos;
  • +
  • enviar spam, hacer phishing, ni alojar o distribuir malware;
  • +
  • ofrecer proxies abiertos o servicios de VPN a terceros, ni ejecutar nodos de salida o relés de Tor;
  • +
  • hacer credential stuffing, ataques de fuerza bruta a inicios de sesión u otros ataques automatizados contra cuentas;
  • +
  • alojar, almacenar o distribuir contenido ilegal;
  • +
  • revender, subarrendar o compartir con terceros la capacidad de cómputo o el acceso a la red del workspace.
  • +
+

+ Los túneles inversos solo están permitidos para darte acceso a tu propio workspace y para tu propio uso de + desarrollo, y nunca para exponer ninguno de los servicios o actividades anteriores. El correo saliente (SMTP) está + bloqueado y todos los workspaces tienen límites de red; intentar saltárselos es un incumplimiento de esta política. +

+

+ Si detectamos un abuso o recibimos un aviso de él, podemos suspender o eliminar el workspace, sus copias de + seguridad y los slots implicados sin aviso previo, y suspender tu membresía. La suspensión o eliminación por abuso + no da derecho a reembolso. Colaboramos con los avisos de abuso de terceros y con las autoridades competentes, y + facilitamos la información que exige la ley. +

+

+ Cuando se elimina un workspace, ya sea por ti, al final de su periodo de gracia o por nosotros, su disco y todas + sus copias de seguridad se borran de forma permanente y no se pueden recuperar. Guarda tu propia copia de todo lo + que no puedas permitirte perder. +

+

Propiedad intelectual

El contenido del sitio es propiedad de Helmcode y no puede reproducirse sin permiso por escrito, salvo para diff --git a/src/pages/terms.astro b/src/pages/terms.astro index 5a5d592..42f5b85 100644 --- a/src/pages/terms.astro +++ b/src/pages/terms.astro @@ -2,7 +2,7 @@ import Legal from '../layouts/NanLegal.astro'; --- - +

NaN is operated by Helmcode S.L. (NIF B44788511), Avenida Quinto Centenario 27, Santiago del Teide, Spain. Contact: info@helmcode.com. @@ -34,8 +34,10 @@ import Legal from '../layouts/NanLegal.astro'; Snapshots are crash-consistent: restoring one is equivalent to the machine losing power at the moment the snapshot was taken, so data the workspace had not yet written to disk is not included and running databases recover as after a power cut. Restoring a snapshot brings back everything that was on the disk at that time, - including any access you configured yourself (for example SSH keys or SSH server settings you added); only - your current NaN SSH key is re-applied by the platform. You can restore up to 3 times per workspace in any + including any access you configured yourself inside the machine (for example keys in ~/.ssh/authorized_keys or + SSH server settings you added). A restore does not change the SSH keys you manage for the workspace in the member + platform: they are not stored on the workspace disk, so after a restore the keys that can log in are the ones in + that list at that moment. Keys added inside the machine have no effect on logins through the NaN SSH gateway. You can restore up to 3 times per workspace in any 24 hours; there are no on-demand backups. Snapshots can only be restored while the add-on is active: during a grace period you must attach the workspace to an active slot (or renew the membership, for a free workspace) and turn the add-on on again, which is billed again. Enabling the add-on charges the first payment right away @@ -49,6 +51,38 @@ import Legal from '../layouts/NanLegal.astro'; older kept snapshots are deleted with the next daily backup. Deleted snapshots cannot be recovered.

+

Workspace acceptable use

+

+ This section applies from October 6, 2026. Workspaces are personal machines for your own development work. You + are responsible for everything that runs on your workspace and for all the traffic it sends, including what + agents run on your behalf. You may not use a workspace to: +

+
    +
  • mine cryptocurrency;
  • +
  • scan, probe or attack third-party systems, other members' workspaces or our own infrastructure, or try to gain unauthorized access to them;
  • +
  • launch or take part in denial-of-service (DDoS) attacks;
  • +
  • send spam, run phishing, or host or distribute malware;
  • +
  • run open proxies, offer VPN services to others, or run Tor exit or relay nodes;
  • +
  • carry out credential stuffing, brute-force logins or other automated attacks on accounts;
  • +
  • host, store or distribute illegal content;
  • +
  • resell, sublet or share the workspace's compute or network access with third parties.
  • +
+

+ Reverse tunnels are allowed only to give you access to your own workspace and for your own development use, and + never to expose any of the services or activities listed above. Outgoing email (SMTP) is blocked and network + limits apply to every workspace; trying to get around them is a breach of this policy. +

+

+ If we detect abuse or receive a report of it, we may suspend or delete the workspace, its backups and the slots + involved without prior notice, and suspend your membership. A suspension or deletion for abuse gives no right to a + refund. We cooperate with abuse reports from third parties and with the competent authorities, and we share the + information the law requires. +

+

+ When a workspace is deleted, whether by you, at the end of its grace period, or by us, its disk and all its + backups are deleted permanently and cannot be recovered. Keep your own copy of anything you cannot afford to lose. +

+

Intellectual property

Site content is owned by Helmcode and may not be reproduced without written permission, except for diff --git a/src/tests/landing/pricing.test.ts b/src/tests/landing/pricing.test.ts index 941528d..69488db 100644 --- a/src/tests/landing/pricing.test.ts +++ b/src/tests/landing/pricing.test.ts @@ -228,3 +228,45 @@ describe('Pricing copy — both locales stay parallel', () => { expect(en.length).toBe(es.length); }); }); + +/** + * Workspaces general launch: the pricing section sells workspace slots to any + * paying member and the premium tier lists its free workspace. The numbers + * themselves are pinned against the docs in workspacesDocs.test.ts. + */ +describe.each(locales)('Pricing copy — workspaces (%s)', (locale) => { + test('the premium tier lists the free Micro workspace', () => { + const copy = tArr('nan.pricing.premiumIncludes', locale).join(' '); + expect(copy).toMatch(/(One free Micro workspace|Un workspace Micro gratis)/); + expect(copy).toContain('10 GiB'); + }); + + test('the member and community tiers do not promise a free workspace', () => { + for (const key of ['memberIncludes', 'communityIncludes']) { + expect(tArr(`nan.pricing.${key}`, locale).join(' '), key).not.toMatch(/workspace/i); + } + }); + + test('the workspaces block says slots do not include inference and premium has one free', () => { + const notes = tArr('nan.pricing.workspaces.notes', locale).join(' '); + expect(notes).toMatch(/Premium \(200€\)/); + expect(notes).toMatch(/(machine only|solo la máquina)/); + expect(notes).toMatch(/euros/); + }); + + test('the FAQ explains workspaces', () => { + expect(faqAnswers(locale)).toMatch(/4,99€/); + }); + + test('no em-dashes in the workspaces block', () => { + const ws = tObj>('nan.pricing.workspaces', locale); + expect(JSON.stringify(ws)).not.toContain('—'); + }); +}); + +describe('Pricing section — workspaces block', () => { + test('renders after the tiers, with an anchor and the docs link', () => { + expect(source).toContain('id="workspaces"'); + expect(source.indexOf('id="workspaces"')).toBeGreaterThan(source.indexOf('tiers.map(')); + }); +}); diff --git a/src/tests/layouts/workspacesDocs.test.ts b/src/tests/layouts/workspacesDocs.test.ts new file mode 100644 index 0000000..fe74cc0 --- /dev/null +++ b/src/tests/layouts/workspacesDocs.test.ts @@ -0,0 +1,320 @@ +import { describe, expect, test } from 'vitest'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { parseFrontmatter } from '@astrojs/markdown-remark'; +import { REMOVED_DOCS } from '../../middleware'; + +/** + * The Workspaces guide is the page the portal, the pricing section and the + * terms point members at when Workspaces open to the whole community. These + * tests pin the facts it must state and keep them in agreement with the other + * surfaces that quote the same numbers (the pricing section and the terms). + * + * Sizes come from cloud-api's tier catalogue (internal/workspace/tiers.go), + * backup prices from cmd/stripe-bootstrap/workspace_backups.go, slot prices + * from the live EUR Stripe prices. + */ +const here = dirname(fileURLToPath(import.meta.url)); +const src = resolve(here, '../..'); +const read = (p: string) => readFileSync(resolve(src, p), 'utf-8'); + +const pages = { + en: { file: 'content/docs/workspaces.mdx', terms: 'pages/terms.astro', prefix: '' }, + es: { file: 'content/docs-es/workspaces.mdx', terms: 'pages/es/terms.astro', prefix: '/es' }, +} as const; + +/** The catalogue the guide and the pricing section must both publish. */ +const CATALOGUE = [ + { size: 'micro', vcpu: 1, ram: 2, disk: 10, slot: '4,99€', backups: '0,99€' }, + { size: 'nano', vcpu: 2, ram: 4, disk: 20, slot: '8,99€', backups: '1,49€' }, + { size: 'basic', vcpu: 4, ram: 8, disk: 40, slot: '16,99€', backups: '2,49€' }, + { size: 'medium', vcpu: 8, ram: 16, disk: 80, slot: '32,99€', backups: '3,99€' }, + { size: 'large', vcpu: 8, ram: 32, disk: 160, slot: '60,99€', backups: '6,99€' }, +]; + +/** The size rows of the guide's price table, as `size|vcpu|ram|disk|slot|backups`. */ +function tableRows(body: string): string[] { + return body + .split('\n') + .filter((l) => /^\|\s*(Micro|Nano|Basic|Medium|Large)\s*\|/.test(l)) + .map((l) => + l + .split('|') + .slice(1, -1) + .map((c) => c.trim().replace(/\s*\/\s*(month|mes)$/, '').replace(/ GiB$/, '')) + .join('|') + .toLowerCase(), + ); +} + +const pub = resolve(src, '../public'); +const CREATE_SHOTS = ['create-1-name-ssh', 'create-2-agents', 'create-3-config']; +const PHONE_SHOTS = [ + 'phone-01-keychain-menu', + 'phone-02-generate-key', + 'phone-03-add-ssh-key', + 'phone-04-new-host-menu', + 'phone-05-host-address', + 'phone-06-host-credentials', + 'phone-07-connected', + 'phone-08-herdr-computer', + 'phone-09-herdr-phone', +]; +/** Screenshots of a computer screen; every other phone-* one is a phone screen. */ +const DESKTOP_SHOTS = new Set(['phone-03-add-ssh-key', 'phone-08-herdr-computer']); +const SHOTS = [...CREATE_SHOTS, ...PHONE_SHOTS]; +/** Things that must never be published: workspace ids (UUIDs) and private addresses. */ +const PRIVATE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}|\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b|\bws-[0-9a-f]{8}\b/i; +const INTERNAL = /firecracker|micro-?vms?\b|\bvm\b|\bkvm\b|kubernetes|\bk8s\b|\br2\b|nan-vmd/i; + +interface Shot { + src: string; + alt: string; + caption: string; + width: number; + height: number; + variant: string; +} + +/** Every of a page, in order. */ +function screenshots(body: string): Shot[] { + return [...body.matchAll(/]*?)\/>/g)].map((m) => { + const attr = (name: string) => new RegExp(`${name}="([^"]*)"`).exec(m[1])?.[1] ?? ''; + const num = (name: string) => Number(new RegExp(`${name}=\\{(\\d+)\\}`).exec(m[1])?.[1]); + return { + src: attr('src'), + alt: attr('alt'), + caption: attr('caption'), + variant: attr('variant') || 'default', + width: num('width'), + height: num('height'), + }; + }); +} + +/** Pixel size of a WebP (lossy VP8, lossless VP8L or extended VP8X). */ +function webpSize(file: string): { width: number; height: number } { + const b = readFileSync(file); + expect(b.toString('ascii', 0, 4)).toBe('RIFF'); + expect(b.toString('ascii', 8, 12)).toBe('WEBP'); + const chunk = b.toString('ascii', 12, 16); + if (chunk === 'VP8 ') return { width: b.readUInt16LE(26) & 0x3fff, height: b.readUInt16LE(28) & 0x3fff }; + if (chunk === 'VP8L') { + const bits = b.readUInt32LE(21); + return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 }; + } + return { width: b.readUIntLE(24, 3) + 1, height: b.readUIntLE(27, 3) + 1 }; +} + +const expectedRows = CATALOGUE.map((t) => [t.size, t.vcpu, t.ram, t.disk, t.slot, t.backups].join('|')); + +describe.each(Object.entries(pages))('workspaces guide (%s)', (locale, page) => { + const raw = read(page.file); + const fm = parseFrontmatter(raw).frontmatter as { order?: number; group?: string }; + const body = raw.replace(/^---[\s\S]*?\n---\n/, ''); + const flat = body.replace(/\s+/g, ' '); + + test('exists and sits in the guides group, before examples and apps', () => { + expect(fm.group).toBe(locale === 'en' ? 'Guides' : 'Guías'); + for (const sibling of ['examples.md', 'apps.md']) { + const dir = page.file.replace('workspaces.mdx', ''); + const other = parseFrontmatter(read(dir + sibling)).frontmatter as { order: number }; + expect(fm.order!).toBeLessThan(other.order); + } + }); + + test('publishes every size with its specs, slot price and backup price', () => { + expect(tableRows(body)).toEqual(expectedRows); + }); + + test('the free workspace is the premium benefit, Micro sized', () => { + expect(flat).toMatch(/\| (Premium), 200€ \| \*\*(One, included|Uno, incluido)\*\* \|/); + expect(flat).toMatch(/\| (Inference|Inferencia), 70€ \| No \|/); + expect(flat).toMatch(/\| (Community|Comunidad), 14,99€ \| No \|/); + expect(flat).toContain(locale === 'en' ? '1 vCPU, 2 GiB RAM, 10 GiB disk' : '1 vCPU, 2 GiB de RAM, 10 GiB de disco'); + }); + + test('says a slot does not include inference', () => { + expect(flat).toContain(locale === 'en' ? 'It does not include inference' : 'No incluye inferencia'); + }); + + test('documents how to connect: SSH command, SSH keys tab, web terminal', () => { + expect(body).toContain('ssh @ssh.nan.builders -p 30222'); + expect(body).toContain('**SSH keys**'); + expect(body).toContain('**Console**'); + expect(flat).toContain('authorized_keys'); + }); + + test('documents the Software panel and the agents', () => { + expect(body).toContain('**Software**'); + for (const agent of ['Pi', 'Hermes', 'gentle-shell', 'Herdr']) expect(body).toContain(agent); + }); + + test('documents the network rules', () => { + expect(flat).toMatch(/80 (and|y) 443/); + expect(flat).toContain('SMTP'); + expect(flat).toMatch(/Tailscale, cloudflared (or|o) ngrok/); + }); + + test('documents backups: daily, 7 days, 3 restores per 24 hours, 14-day re-enable', () => { + expect(flat).toMatch(/\*\*(once a day|una vez al día)\*\*/); + expect(flat).toMatch(/\*\*7 (days|días)\*\*/); + expect(flat).toMatch(/\*\*3 (times per workspace in any 24 hours|veces por workspace en cualquier periodo de 24 horas)\*\*/); + expect(flat).toMatch(/\*\*14 (days|días)\*\*/); + }); + + test('shows every screenshot once, in order: the create wizard, then the phone guide', () => { + const shots = screenshots(body); + expect(shots.map((s) => s.src)).toEqual(SHOTS.map((n) => `/docs/workspaces/${n}.webp`)); + }); + + test('phone screens are shown as phone screenshots, computer screens are not', () => { + for (const shot of screenshots(body)) { + const name = shot.src.replace(/^.*\/|\.webp$/g, ''); + const phone = name.startsWith('phone-') && !DESKTOP_SHOTS.has(name); + expect(shot.variant, name).toBe(phone ? 'phone' : 'default'); + } + }); + + test('no workspace id or private address in any file name, alt text or caption', () => { + for (const shot of screenshots(body)) { + expect(`${shot.src} ${shot.alt} ${shot.caption}`).not.toMatch(PRIVATE); + } + expect(body).not.toMatch(PRIVATE); + }); + + test('the phone guide sits right after creating the workspace, before Connect', () => { + const create = flat.indexOf(locale === 'en' ? '## Create your workspace' : '## Crea tu workspace'); + const phone = flat.indexOf( + locale === 'en' + ? '## Connect from your phone and keep agents running 24/7 with Herdr' + : '## Conéctate desde el móvil y mantén tus agentes 24/7 con Herdr', + ); + expect(create).toBeGreaterThan(-1); + expect(phone).toBeGreaterThan(create); + const connect = body.search(locale === 'en' ? /^## Connect$/m : /^## Conectarte$/m); + expect(connect).toBeGreaterThan(body.indexOf('phone-09-herdr-phone')); + }); + + test('the phone guide gives the exact connection details and the Herdr basics', () => { + expect(flat).toContain('https://termius.com/download'); + expect(flat).toContain('`ssh.nan.builders`'); + expect(flat).toContain('`30222`'); + expect(flat).toMatch(/Use Mosh\*\* (off|desactivado)/); + expect(body).toContain('```bash\nherdr\n```'); + expect(flat).toContain('https://herdr.dev'); + expect(flat).toMatch(locale === 'en' ? /Only the public key/ : /Solo la clave pública/); + }); + + test('the SSH command includes the gateway port', () => { + expect(body).toContain('ssh @ssh.nan.builders -p 30222'); + }); + + test.each(SHOTS)('screenshot %s: file exists, size matches, alt and caption present', (name) => { + const shot = screenshots(body).find((s) => s.src === `/docs/workspaces/${name}.webp`)!; + const file = resolve(pub, `docs/workspaces/${name}.webp`); + expect(existsSync(file)).toBe(true); + expect(webpSize(file)).toEqual({ width: shot.width, height: shot.height }); + expect(shot.alt.length).toBeGreaterThan(30); + expect(shot.caption.length).toBeGreaterThan(5); + // Member-facing: no infrastructure words in what a reader or a screen reader gets. + expect(`${shot.alt} ${shot.caption}`).not.toMatch(INTERNAL); + }); + + test('explains each step of the wizard', () => { + expect(flat).toMatch(locale === 'en' ? /## Create your workspace/ : /## Crea tu workspace/); + expect(flat).toMatch( + locale === 'en' + ? /lowercase letters, numbers and hyphens, 20 characters at most/ + : /minúsculas, números y guiones, 20 caracteres como máximo/, + ); + expect(body).toContain('cat ~/.ssh/id_ed25519.pub'); + expect(body).toContain('ssh-keygen -t ed25519'); + expect(flat).toContain('Need a bigger workspace? Buy a slot'); + expect(flat).toContain('@BotFather'); + expect(flat).toContain('Install gentle-shell'); + expect(flat).toContain('**Create Workspace**'); + }); + + test('says a restore does not change the SSH keys managed in the panel', () => { + expect(flat).toContain( + locale === 'en' + ? 'The keys in the **SSH keys** tab are not on the disk, so a restore does not change them.' + : 'Las claves de la pestaña **SSH keys** no están en el disco, así que restaurar no las cambia.', + ); + }); + + test('documents the 7-day grace, the rescue session and permanent deletion', () => { + expect(flat).toMatch(/\*\*(7-day grace period|periodo de gracia de 7 días)\*\*/); + expect(flat).toMatch(/(rescue session|sesión de rescate)/); + expect(flat).toMatch(/\*\*(deleted permanently|borran de forma permanente)\*\*/); + }); + + test('links to anchors that exist in the terms of the same locale', () => { + const terms = read(page.terms); + const anchors = [...body.matchAll(new RegExp(`\\(${page.prefix}/terms#([\\w-]+)\\)`, 'g'))].map((m) => m[1]); + expect(anchors).toEqual(expect.arrayContaining(['workspace-backups', 'workspace-acceptable-use'])); + for (const a of anchors) expect(terms, a).toContain(`id="${a}"`); + }); + + test('links only to docs pages that exist in the same locale', () => { + const dir = page.file.replace('workspaces.mdx', ''); + const slugs = [...body.matchAll(new RegExp(`\\(${page.prefix}/docs/([\\w-]+)\\)`, 'g'))].map((m) => m[1]); + expect(slugs.length).toBeGreaterThan(0); + for (const slug of slugs) { + expect(existsSync(resolve(src, dir, `${slug}.md`)) || existsSync(resolve(src, dir, `${slug}.mdx`)), slug).toBe(true); + } + }); + + test('no em-dashes in the copy', () => { + expect(body).not.toContain('—'); + }); + + test('the docs introduction links to it', () => { + const intro = read(page.file.replace('workspaces.mdx', 'intro.md')); + expect(intro).toContain(`(${page.prefix}/docs/workspaces)`); + }); +}); + +describe('the pricing section publishes the same workspace catalogue', () => { + const pricing = read('components/nan/home/Pricing.astro'); + + test('same sizes, specs and prices as the guide', () => { + const rows = [ + ...pricing.matchAll( + /\{ size: '(\w+)', vcpu: (\d+), ram: (\d+), disk: (\d+), slot: '([\d,]+€)', backups: '([\d,]+€)' \}/g, + ), + ].map((m) => m.slice(1).join('|')); + expect(rows).toEqual(expectedRows); + }); + + test('links to the guide in both locales', () => { + expect(pricing).toContain("lang === 'es' ? '/es/docs/workspaces' : '/docs/workspaces'"); + }); +}); + +describe('retired v1 agents docs now point at the workspaces guide', () => { + test('/docs/agents redirects to workspaces', () => { + expect(REMOVED_DOCS.agents).toBe('workspaces'); + }); + + test.each(Object.entries(REMOVED_DOCS))('redirect target of %s is a live page in both locales', (_slug, target) => { + for (const dir of ['content/docs', 'content/docs-es']) { + const live = ['md', 'mdx'].some((ext) => existsSync(resolve(src, dir, `${target}.${ext}`))); + expect(live, `${dir}/${target}`).toBe(true); + } + }); +}); + +describe('workspaces screenshots', () => { + test('the two locales show the same pictures with different, translated alt text', () => { + const [en, es] = (['en', 'es'] as const).map((l) => screenshots(read(pages[l].file))); + expect(es.map((s) => s.src)).toEqual(en.map((s) => s.src)); + en.forEach((shot, i) => expect(es[i].alt).not.toBe(shot.alt)); + }); + + test('only the published screenshots live in the folder (no raw captures)', () => { + expect(readdirSync(resolve(pub, 'docs/workspaces')).sort()).toEqual(SHOTS.map((n) => `${n}.webp`).sort()); + }); +}); diff --git a/src/tests/legal/workspaceAcceptableUse.test.ts b/src/tests/legal/workspaceAcceptableUse.test.ts new file mode 100644 index 0000000..15a9f4e --- /dev/null +++ b/src/tests/legal/workspaceAcceptableUse.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +/** + * Workspace acceptable use policy (Workspaces general launch, 2026-10-06). + * + * Every paying member can now run a machine with outbound internet access, so + * the terms have to say what that machine may not be used for, what happens on + * abuse, and that deletion takes the backups too. The docs and the pricing page + * link to `#workspace-acceptable-use`, so the anchor is part of the contract. + */ +const here = dirname(fileURLToPath(import.meta.url)); +const pages = resolve(here, '../../pages'); +const flat = (p: string) => readFileSync(resolve(pages, p), 'utf-8').replace(/\s+/g, ' '); + +/** The section's text: from its heading to the next h2, tags stripped. */ +function section(src: string): string { + const start = src.indexOf('

'); + expect(start, 'workspace-acceptable-use heading present').toBeGreaterThan(-1); + const end = src.indexOf(']+>/g, '').replace(/\s+/g, ' '); +} + +const cases = { + en: { + file: 'terms.astro', + heading: 'Workspace acceptable use', + since: 'This section applies from October 6, 2026', + prohibited: [ + 'mine cryptocurrency', + "scan, probe or attack third-party systems, other members' workspaces or our own infrastructure", + 'denial-of-service (DDoS)', + 'send spam, run phishing, or host or distribute malware', + 'run open proxies, offer VPN services to others, or run Tor exit or relay nodes', + 'credential stuffing', + 'illegal content', + "resell, sublet or share the workspace's compute", + ], + tunnels: 'Reverse tunnels are allowed only to give you access to your own workspace and for your own development use, and never to expose any of the services or activities listed above', + smtp: 'Outgoing email (SMTP) is blocked', + abuse: 'we may suspend or delete the workspace, its backups and the slots involved without prior notice', + refund: 'A suspension or deletion for abuse gives no right to a refund', + reports: 'We cooperate with abuse reports', + deletion: 'its disk and all its backups are deleted permanently and cannot be recovered', + }, + es: { + file: 'es/terms.astro', + heading: 'Uso aceptable de los workspaces', + since: 'Este apartado se aplica desde el 6 de octubre de 2026', + prohibited: [ + 'minar criptomonedas', + 'escanear, sondear o atacar sistemas de terceros, workspaces de otros miembros o nuestra propia infraestructura', + 'denegación de servicio (DDoS)', + 'enviar spam, hacer phishing, ni alojar o distribuir malware', + 'ofrecer proxies abiertos o servicios de VPN a terceros, ni ejecutar nodos de salida o relés de Tor', + 'credential stuffing', + 'contenido ilegal', + 'revender, subarrendar o compartir con terceros la capacidad de cómputo', + ], + tunnels: 'Los túneles inversos solo están permitidos para darte acceso a tu propio workspace y para tu propio uso de desarrollo, y nunca para exponer ninguno de los servicios o actividades anteriores', + smtp: 'El correo saliente (SMTP) está bloqueado', + abuse: 'podemos suspender o eliminar el workspace, sus copias de seguridad y los slots implicados sin aviso previo', + refund: 'La suspensión o eliminación por abuso no da derecho a reembolso', + reports: 'Colaboramos con los avisos de abuso', + deletion: 'su disco y todas sus copias de seguridad se borran de forma permanente y no se pueden recuperar', + }, +} as const; + +describe.each(Object.entries(cases))('terms: workspace acceptable use (%s)', (_locale, c) => { + const src = flat(c.file); + const s = section(src); + + test('has its own section, after membership and payments', () => { + expect(s.startsWith(c.heading)).toBe(true); + expect(src.indexOf('id="workspace-acceptable-use"')).toBeGreaterThan(src.indexOf('id="workspace-backups"')); + }); + + test('marks the date it applies from, and the page date moved with it', () => { + expect(s).toContain(c.since); + expect(src).toContain('updated="October 6, 2026"'); + }); + + test.each(c.prohibited)('prohibits: %s', (item) => { + expect(s).toContain(item); + }); + + test('allows reverse tunnels only for own access and development', () => { + expect(s).toContain(c.tunnels); + expect(s).toContain(c.smtp); + }); + + test('suspension or deletion without notice and without refund on abuse', () => { + expect(s).toContain(c.abuse); + expect(s).toContain(c.refund); + expect(s).toContain(c.reports); + }); + + test('deletion takes the backups with it', () => { + expect(s).toContain(c.deletion); + }); + + test('no em-dashes', () => { + expect(s).not.toContain('—'); + }); +}); diff --git a/src/tests/legal/workspaceBackupTerms.test.ts b/src/tests/legal/workspaceBackupTerms.test.ts index e29d09c..737d4a4 100644 --- a/src/tests/legal/workspaceBackupTerms.test.ts +++ b/src/tests/legal/workspaceBackupTerms.test.ts @@ -41,8 +41,7 @@ describe('terms: workspace backups (EN)', () => { expect(p).toContain('no refund for the current month'); expect(p).toContain('Turning the add-on off or deleting the workspace deletes all its snapshots permanently'); expect(p).toContain('the add-on stops being billed'); - expect(p).toContain('including any access you configured yourself'); - expect(p).toContain('only your current NaN SSH key is re-applied'); + expect(p).toContain('including any access you configured yourself inside the machine'); }); test('restore limits and grace rules (product decision 2026-10-02)', () => { @@ -89,8 +88,7 @@ describe('terms: workspace backups (ES)', () => { expect(p).toContain('sin reembolso del mes en curso'); expect(p).toContain('Desactivar el complemento o eliminar el workspace borra todas sus instantáneas de forma permanente'); expect(p).toContain('el complemento deja de cobrarse'); - expect(p).toContain('incluido cualquier acceso que hayas configurado tú'); - expect(p).toContain('solo vuelve a aplicar tu clave SSH actual de NaN'); + expect(p).toContain('incluido cualquier acceso que hayas configurado tú dentro de la máquina'); }); test('límites de restauración y reglas de gracia', () => { @@ -108,3 +106,36 @@ describe('terms: workspace backups (ES)', () => { expect(p).not.toContain('restaura antes la que necesites'); }); }); + +/** + * Since cloud-api migration 097 the member's SSH keys live in a per-workspace + * list managed in the portal (SSH keys tab), not on the workspace disk, and the + * SSH gateway authenticates against that list. A restore rewrites the disk only, + * so it cannot bring back or drop a managed key. The old sentence ("only your + * current NaN SSH key is re-applied") described the single create-time key. + */ +describe('terms: restore and managed SSH keys', () => { + const cases = [ + { + file: 'terms.astro', + stale: 'only your current NaN SSH key is re-applied', + keeps: 'A restore does not change the SSH keys you manage for the workspace in the member platform', + notOnDisk: 'they are not stored on the workspace disk, so after a restore the keys that can log in are the ones in that list at that moment', + gateway: 'Keys added inside the machine have no effect on logins through the NaN SSH gateway', + }, + { + file: 'es/terms.astro', + stale: 'solo vuelve a aplicar tu clave SSH actual de NaN', + keeps: 'Restaurar no cambia las claves SSH que gestionas para el workspace en la plataforma de miembros', + notOnDisk: 'no se guardan en el disco del workspace, así que tras una restauración pueden entrar las claves que haya en esa lista en ese momento', + gateway: 'Las claves añadidas dentro de la máquina no tienen efecto en el acceso a través de la pasarela SSH de NaN', + }, + ]; + test.each(cases)('$file', (c) => { + const p = section(flat(c.file)); + expect(p).not.toContain(c.stale); + expect(p).toContain(c.keeps); + expect(p).toContain(c.notOnDisk); + expect(p).toContain(c.gateway); + }); +}); diff --git a/src/tests/lib/middleware.test.ts b/src/tests/lib/middleware.test.ts index 43f49bb..cf022f9 100644 --- a/src/tests/lib/middleware.test.ts +++ b/src/tests/lib/middleware.test.ts @@ -127,23 +127,23 @@ describe('middleware — páginas de docs retiradas', () => { expect(r.passedThrough).toBe(true); }); - test('/docs/agents (agentes v1, retirados) lleva a /docs con 301', async () => { + test('/docs/agents (agentes v1, retirados) lleva a /docs/workspaces con 301', async () => { const r = await run('https://nan.builders/docs/agents'); expect(r.status).toBe(301); - expect(r.location).toBe('/docs'); + expect(r.location).toBe('/docs/workspaces'); expect(r.passedThrough).toBe(false); }); test('/docs/agents con barra final y en español', async () => { - expect((await run('https://nan.builders/docs/agents/')).location).toBe('/docs'); - expect((await run('https://nan.builders/es/docs/agents')).location).toBe('/es/docs'); + expect((await run('https://nan.builders/docs/agents/')).location).toBe('/docs/workspaces'); + expect((await run('https://nan.builders/es/docs/agents')).location).toBe('/es/docs/workspaces'); }); test('las subrutas de /docs/agents (capturas enlazadas) van al mismo destino', async () => { const r = await run('https://nan.builders/docs/agents/create-agent-form.png'); expect(r.status).toBe(301); - expect(r.location).toBe('/docs'); - expect((await run('https://nan.builders/es/docs/agents/x/y')).location).toBe('/es/docs'); + expect(r.location).toBe('/docs/workspaces'); + expect((await run('https://nan.builders/es/docs/agents/x/y')).location).toBe('/es/docs/workspaces'); }); test('no confunde páginas cuyo slug empieza por "agent"', async () => {