Cookies con Astro.cookies: opciones y firma
El primitivo de bajo nivel bajo toda sesión. La API tipada Astro.cookies para leer con get y sus conversores, escribir con set y borrar con delete. Las opciones que deciden la seguridad de una cookie: httpOnly frente al robo por scripts, secure para exigir https, sameSite contra el CSRF y maxAge para acotar su vida. Y por qué una cookie que carga significado hay que firmarla con HMAC para detectar manipulación, con la frontera exacta entre integridad y secreto.
Leer: get, has y los conversores
Astro.cookies.get devuelve un objeto AstroCookie —o undefined si la cookie no existe—. Ese objeto no es una cadena pelada: expone el valor crudo en .value y una batería de conversores que interpretan el texto y devuelven el tipo que esperas. Como una cookie siempre viaja como cadena, esos conversores te ahorran el parseo manual y su manejo de errores.
---const tema = Astro.cookies.get('tema')?.value; // cadena | undefinedconst prefs = Astro.cookies.get('prefs')?.json(); // objeto parseado de JSONconst visitas = Astro.cookies.get('visitas')?.number(); // numeroconst activo = Astro.cookies.get('activo')?.boolean(); // booleanoconst existe = Astro.cookies.has('sesion'); // true o false---Astro.cookies.has responde a una pregunta más barata —¿está?— sin materializar el valor. Importa un matiz: get y has miran tanto las cookies entrantes de la petición como las que tú mismo hayas fijado durante el renderizado, así que leen un estado coherente aunque acabes de escribir una cookie en la misma petición.
Escribir y borrar: set y delete
Astro.cookies.set acepta una clave, un valor y un objeto de opciones. El valor puede ser una cadena o un objeto; si le pasas un objeto, Astro lo serializa a JSON por ti, y get(...).json() lo recupera. Astro.cookies.delete caduca la cookie pidiéndole al navegador que la olvide.
Astro.cookies.set('tema', 'oscuro', { path: '/', maxAge: 60 * 60 * 24 * 365 });Astro.cookies.set('prefs', { idioma: 'es', compacto: true }, { path: '/' });Astro.cookies.delete('tema', { path: '/' });Hay una trampa clásica con delete: para que el navegador borre la cookie correcta, las opciones de alcance —path y domain— deben coincidir con las que usaste al crearla. Una cookie fijada en path: '/app' no se borra con un delete sin ruta; quedaría viva, fantasma, reenviándose en cada petición bajo /app. La regla práctica es sencilla: escribe y borra siempre con el mismo path.
Conviene saber qué hace set por dentro: no habla con el navegador al instante, sino que registra la cookie para que Astro la emita como una cabecera Set-Cookie al construir la respuesta. Por eso puedes fijar una cookie en el frontmatter y leerla en esa misma petición —Astro la tiene en memoria— aunque el navegador todavía no la conozca. Si necesitas depurar, Astro.cookies.headers te deja inspeccionar las cabeceras Set-Cookie pendientes antes de que salgan.
Las opciones que deciden la seguridad
Las opciones de set no son adornos: son la diferencia entre una cookie defendida y una expuesta. Cuatro merecen memorizarse.
La combinación que querrás por defecto para cualquier cookie sensible es httpOnly: true, secure: true, sameSite: 'lax', con un maxAge acorde a cuánto debe durar. Hay opciones secundarias que conviene conocer sin obsesionarse. expires fija una fecha absoluta de caducidad y es la alternativa a maxAge —si das las dos, maxAge manda en los navegadores modernos—. domain ensancha el alcance de la cookie a los subdominios, un arma de doble filo: úsalo solo si de verdad compartes sesión entre app.midominio.com y api.midominio.com, porque ampliar el alcance amplía la superficie de exposición. Y partitioned aísla la cookie por sitio de origen en contextos de terceros, y exige secure: true para funcionar. Pero son las cuatro primeras las que fijan la postura de seguridad; las demás afinan alcance y vigencia.
Un endurecimiento poco conocido pero potente son los prefijos de nombre. Si bautizas una cookie con el prefijo __Host-, el navegador se niega a aceptarla salvo que venga con secure, sin domain y con path en la raíz —justo la configuración más restrictiva posible—. El prefijo __Secure- exige al menos secure. Como el prefijo forma parte del nombre, un atacante que intente sobrescribir la cookie desde un subdominio hermano o por http fracasa: es el propio navegador quien rechaza la versión mal configurada, sin que tu servidor tenga que hacer nada.
Firmar: integridad para una cookie con significado
Llega el punto delicado. Si guardas en una cookie un dato que significa algo —rol=admin, userId=42—, el servidor estará confiando en un valor que el cliente controla y puede reescribir. La defensa no es esconderlo, es firmarlo: adjuntar una firma criptográfica, calculada con un secreto que solo el servidor conoce, que el servidor recomputa al leer para verificar que nadie tocó el dato. Astro no firma por ti —deja la política en tus manos—, pero compones la firma con la Web Crypto API estándar y la enchufas en la cookie.
// firma.ts — HMAC-SHA256 con la Web Crypto APIconst clave = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(process.env.COOKIE_SECRET), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],);const hex = (b: ArrayBuffer) => [...new Uint8Array(b)].map((n) => n.toString(16).padStart(2, '0')).join('');export async function firmar(valor: string): Promise<string> { const f = await crypto.subtle.sign('HMAC', clave, new TextEncoder().encode(valor)); return `${valor}.${hex(f)}`; // el valor y su firma, unidos}export async function verificar(firmado: string): Promise<string | null> { const i = firmado.lastIndexOf('.'); const valor = firmado.slice(0, i); return (await firmar(valor)) === firmado ? valor : null; // firma no valida -> null}Con ese par, escribes Astro.cookies.set('rol', await firmar('admin')) y, al leer, pasas el contenido por verificar: si devuelve null, alguien manipuló la cookie y la descartas. La frontera conceptual que no debes cruzar es esta: firmar da integridad, no secreto. El valor sigue siendo legible por cualquiera que abra la cookie —la firma solo prueba que no ha cambiado—. Si además necesitas que el contenido sea ilegible, tienes que cifrarlo, o mejor, no ponerlo en la cookie en absoluto y usar una sesión, donde la cookie lleva un id opaco y el dato vive en el servidor.
Astro te ofrece un gancho para que firmar no sea un ritual que haya que recordar en cada llamada: la opción encode de set transforma el valor justo antes de serializarlo, y su pareja decode en get lo revierte al leer. Enchufando ahí tus funciones, cada Astro.cookies.set firma sola y cada Astro.cookies.get verifica sola, y el resto de tu código trata la cookie como si fuera de confianza porque la frontera se aplica en un único punto.