Integra tus notificaciones con tu IA

Vistas: 1
0:00 / 0:00
Integra tus notificaciones con tu IA

Durante estos últimos años te he ido contando, episodio a episodio, cómo gestionaba las notificaciones. Tenía el correo, mis páginas web, los feeds RSS y un montón de servicios, y todo eso me avisaba por algún sitio. Durante mucho tiempo ese sitio fue Matrix, que es lo que más se parece a un Telegram con temas y canales, y allí tenía un grupo para los correos, otro para las caídas, otro para los intentos de acceso raros. Funcionaba, no te lo voy a negar. Hasta que un día llegaron dieciocho avisos seguidos y me di cuenta de que tenía un sistema de notificaciones impecable y una capacidad de leerlo más bien escasa. De eso va el episodio de hoy, de cómo he integrado todas esas notificaciones, correos y servicios con mi modelo de lenguaje para que sea él quien resuma, agrupe y discrimine lo que merece mi atención.

De Matrix a un asistente que entiende lo que le llega

Empecemos por el principio, porque el problema no era Matrix, ni el correo, ni las webs. El problema era el ruido.

Yo tengo varias webs levantadas, un servidor de correo, un lector de RSS y un montón de contenedores que se actualizan solos. Cada una de esas piezas puede fallar, y cuando falla, avisa. Un vigilante de uptime te manda un mensaje cada vez que una web deja de responder y otro cuando vuelve. El lector de RSS te dice que hay entradas nuevas. El actualizador de contenedores te cuenta qué imágenes ha refrescado. Y así, sin querer, acabas con un flujo continuo de mensajitos que nadie lee.

Lo que hacía antes era repartirlo por grupos en Matrix. Los correos en un grupo, las caídas en otro, las cosas raras del servidor en un tercero. Y oye, tenerlo separado estaba bien. Pero tenía dos problemas gordos.

El primero es que era completamente impersonal. Un grupo de Matrix no sabe si el correo que acaba de entrar es una factura, una newsletter o un intento de phishing. Solo lo pone ahí, y ya te apañarás tú. El segundo, y este es el que de verdad duele, es la escala. Con dos avisos al día vas sobrado. Con trescientos, leerte trescientos avisos termina siendo un trabajo a jornada completa, y como trabajo no te lo van a pagar, lo que haces es dejar de mirarlos.

Y cuando dejas de mirarlos, pasa lo de siempre. Se te cae una web, se levanta, vuelve a caerse, se vuelve a levantar. Cien notificaciones que al final cuentan una sola cosa: que hubo un rato movidito y que ahora todo está bien. O peor, que una de tus webs se cayó y no se ha levantado, y esa notificación, la importante, se ha perdido entre las otras noventa y nueve.

Un asistente que recuerda, actúa y se integra

Todo esto viene de una serie que llevo unos cuantos episodios construyendo, así que déjame que te sitúe, porque si no, esto se entiende a medias.

En el episodio 843 te conté cómo le di memoria a mi asistente. Cuatro capas distintas, los mensajes, unas fichas de recuerdos episódicos, un estado persistente y una línea temporal, todo escrito en una sola transacción dentro de un único fichero SQLite. El resumen de aquello fue: un asistente que recuerda.

En el 845 te conté la otra mitad, cómo le di manos. Treinta y tres herramientas repartidas en fichas, un enrutador que decide qué anunciarle al modelo en cada turno, y tres niveles de permiso para que no se ponga a borrar cosas por su cuenta. El resumen de aquello fue: un asistente que actúa.

Y hoy cierro el arco con la tercera pata, que es la de la que vengo hablando desde el principio: cómo ese asistente habla con el resto de mi mundo. Un asistente que recibe avisos de mis scripts, que mira mi correo, que lee mis RSS y que sabe de mis entrenamientos. El resumen de hoy es: un asistente que se integra.

Las tres cosas juntas son la frase que me llevo de toda la trilogía. Un asistente que recuerda, actúa y habla con tu mundo es un asistente. Uno que solo charla es un juguete caro.

Dos formas de integrar: preguntar e inyectar

Cuando me puse a montar esto, lo primero que hice fue separar el problema en dos piezas, porque son dos flujos distintos y conviene no mezclarlos.

La primera pieza es la de consulta. Aquí el que pregunta soy yo. Tengo el correo integrado, pero mi asistente no se pasa el día mirándolo. Cuando quiero saber qué hay, se lo pido, y él lo mira en ese momento. Es un flujo de fuera hacia dentro que arranco yo.

La segunda pieza es la de ingesta, y es la que me interesa hoy. Aquí el que habla es el mundo. Cada vez que una de mis webs se cae, el vigilante de uptime le manda un mensaje a mi asistente. Antes ese mensaje lo mandaba a Matrix; ahora lo manda al asistente. El asistente lo guarda y, cuando yo le pregunto, me lo cuenta resumido. Es un flujo de dentro hacia fuera que arrancan las máquinas.

Fíjate en la diferencia, porque es la clave de todo el episodio. En la consulta, el que decide cuándo mirar soy yo. En la ingesta, el que decide cuándo hablar es la máquina, pero el que decide cuándo escuchar sigo siendo yo.

El resumen que separa el ruido de lo importante

Vamos al momento en el que todo esto cobra sentido. Hace unos días le pregunté a mi asistente qué tenía pendiente, y me contestó algo así.

Me dijo que habían entrado dieciocho avisos, y que él me los había dejado en tres. El primero era crítico, y decía que se habían caído todos los sitios y que ya se habían recuperado. Ahí está la gracia. En lugar de contarme que la web A se cayó y volvió, que la web B se cayó y volvió, que la web A se volvió a caer y volvió otra vez, me cuenta la historia completa de una vez: hubo una caída general y todo está de vuelta. Los dieciocho avisos eran un solo suceso, y él lo entendió.

Los otros dos eran del actualizador de contenedores, que me avisaba de que había refrescado un par de servicios. Y ya está. De dieciocho avisos, tres. Y ninguno de los quince que se quedaron fuera era importante, porque eran repeticiones del mismo hecho.

Con el correo pasa igual. Le preguntas y te dice que de doce correos te los ha agrupado en cinco o seis mensajes. Que de tal remitente tienes dos, que de cual tienes uno, que este hilo ya lo habías visto. Y no se queda en agrupar, que es lo fácil, sino que te dice lo que merece tu atención. Si ha habido caídas simultáneas, te avisa de que eso no es casualidad y que conviene mirar el proxy. Y le pone una prioridad, que interpreta a partir de lo que le has contado.

Es exactamente lo que le pedirías a un buen ayudante. No que te lea el correo en voz alta, sino que te diga qué hay y qué importa.

La API Notification: un endpoint para todo

¿Y cómo le llega todo esto al asistente? Pues con una pieza muy sencilla de describir y muy bien pensada por dentro. Un único endpoint, que llamo API Notification, donde cualquier servicio que yo quiera integrar deja su mensaje.

La idea es que el servicio manda un JSON de los de clave y valor. Le dice el origen, o sea, quién habla, que puede ser lo que quieras. Le pasa un mensaje, que es el texto de la alerta. Y además le pasa un objeto libre, un metadata, donde va lo que a ti te interese: el nombre del servicio, el estado en el que estaba antes, el estado en el que está ahora, el número de avisos, lo que sea.

Cuando lo monté, lo primero que pensé fue en lo de siempre: ¿y ahora cómo parseo yo esto? ¿Cómo saco los campos, cómo los normalizo, cómo los meto en una estructura? Y ahí me di cuenta de que estaba cometiendo el primer error, el mismo que había cometido en los primeros pasos del asistente. Estaba intentando parsear los datos antes de dárselos al modelo.

Y es un error porque el modelo no necesita que se los parsees. Al revés. Un modelo de lenguaje trabaja con JSON como pez en el agua. Le pasas el objeto tal cual, con sus claves y sus valores, y él lo interpreta solo. No tienes que decirle qué es cada campo ni en qué orden vienen. Él lee, entiende y te lo cuenta. Todo el trabajo de traducir de una estructura a una frase lo hace él, y lo hace mejor que un if tuyo.

Así que la regla que me llevo de esto es sencilla: no parsees lo que le vas a dar a un modelo. Dáselo crudo y deja que lo lea. Menos código tuyo, menos sitios donde equivocarte, y encima funciona mejor.

Para que te hagas una idea, un aviso del vigilante de uptime viaja así:

{
  "message": "La web atareao.es no responde (timeout)",
  "origin": "uptime",
  "author": "watchbit",
  "level": "critical",
  "metadata": {
    "service": "atareao.es",
    "state_before": "up",
    "state_now": "down",
    "checks_failed": 3
  }
}

Y el actualizador de contenedores manda otro, con su origen y su metadata, sin que yo tenga que tocar nada del asistente. Cada servicio habla su idioma dentro del objeto, y el modelo hace de traductor.

Las claves de ingesta: dar voz a una máquina sin darle las llaves de casa

Y aquí llegamos al bloque que más me ha gustado montar, porque tiene miga y porque es donde más fácil es meter la pata.

Fíjate en el problema. Yo quiero que un script mío, que corre a las tres de la mañana, pueda dejar un aviso en el buzón de mi asistente. Ese script es una máquina, no un humano, así que no puede autenticarse con una sesión. Y no quiero darle mi contraseña ni mi cookie, porque si un día ese script se filtra, me quedo con la casa abierta.

La solución es una credencial de aplicación, un token de larga vida que le doy a la máquina y que puedo revocar cuando quiera. Eso es una clave de ingesta.

Lo primero que llama la atención es su forma. Cuando creas una clave, el servidor genera un token que empieza por el literal val_ y detrás lleva treinta y dos bytes aleatorios sacados de un generador criptográfico de verdad, no de un random() de juguete, codificados en base64url sin relleno. En total te sale algo de cuarenta y siete caracteres. Sin identificadores predecibles, sin contadores, sin nada que se pueda adivinar.

Y ahora la parte que importa, la que separa esto de un tutorial de manual. El servidor no guarda el token. Guarda dos cosas. El SHA-256 del token, en hexadecimal, y los doce primeros caracteres, que llama prefix. Nada más.

Puede parecer raro que guarde el prefijo si ya tiene el hash, pero tiene todo el sentido. El prefijo sirve para identificar la clave en el listado sin guardar el token. Tú ves val_83xGOtBB y sabes de cuál hablas, sin que ese trozo sirva para autenticarse. Es la diferencia entre saber que tienes una llave y tener la llave.

De aquí sale la primera regla dura: el token se enseña una sola vez. Una. En la respuesta que crea la clave, y después no hay ningún endpoint que te lo devuelva. No hay un «recupéralo», no hay un «mándamelo por correo». Es el patrón de guárdalo ahora o piérdelo. Y como el servidor solo tiene hashes, aunque alguien haga un volcado entero de la base de datos, lo que va a encontrar son hashes. Y de un SHA-256 no se vuelve atrás.

Hay un detalle de oficio que me encanta. El tipo que representa una clave en el código tiene su Debug escrito a mano, no derivado. ¿Y para qué, pudiendo derivarlo? Pues precisamente para dejar escrito en el código que un Debug nunca incluye un token. El comentario dice, literal, que esa representación se mantiene igual aunque el tipo gane campos. Alguien pensó «esto un día, con prisa, se rompe al añadir un campo y filtra una credencial», y lo blindó. Eso es tener oficio.

La revocación también tiene su carácter. Para revocar una clave haces un DELETE, y aquí vienen dos decisiones que se ven poco. La primera es que la clave no se borra. Se le pone una marca, revoked_at, y la fila se queda. Conservas la historia: sabes qué claves tuviste, cuándo las creaste, cuándo se usaron por última vez y cuándo las revocaste. La segunda es que la respuesta es siempre un 204, exista o no ese identificador, y la operación es idempotente. Si la revocas dos veces, la marca ya puesta no cambia. La revocación no falla, no discute y no te dice si la clave existía.

Y ahora la joya del bloque, la parte que me parece mejor hecha. Los errores son indistinguibles.

Vamos a verlo despacio. Si mandas una notificación y no pones la cabecera Authorization, recibes un 401. Si la pones pero no empieza por Bearer , con esa ortografía exacta y un solo espacio, recibes un 401. Si el token está mal formado, 401. Si el token es de una clave que no existe, 401. Y si es de una clave revocada, 401. Todos. El mismo cuerpo, {"error":"unauthorized"}, clavadito.

¿Y entonces cómo se diagnostica? Pues con el log del servidor. Ahí sí aparece el motivo real, missing_authorization, malformed_authorization o unknown_or_revoked_key. Pero eso se queda dentro. Al cliente le llega lo mismo, siempre. ¿Por qué? Porque si el servidor te dijera «esta clave existe pero está revocada», le estaría confirmando a quien pregunta que esa clave fue real. Y eso es información que no se regala. La regla es: el rechazo no te dice nada.

Y un último detalle que, si vienes del SQL de toda la vida, te va a parecer raro. El servidor no compara el token en claro contra nada, porque no lo tiene. Lo que hace es calcular el SHA-256 del token que le llega y buscarlo por una columna que es única. La autenticación es una búsqueda por hash, no una comparación de secretos byte a byte. Menos superficie, menos maneras de equivocarse.

Con esto ya tienes la respuesta a la pregunta que da título a todo esto. ¿Cómo le das a una máquina permiso para hablarte sin darle las llaves de tu casa? Le das un token que solo se enseña una vez, que en el servidor es solo un hash, que se revoca sin borrar, cuyos rechazos no revelan nada y que se autentica por búsqueda de hash. Eso es una credencial de aplicación bien hecha.

El buzón y el WebPush

Una vez que las máquinas pueden dejar recados, hay que poder leerlos. Y aquí hay una asimetría que me parece muy sana.

El buzón se lee con sesión, no con token de ingesta. Es decir, la ruta que autentica por token escribe, y la ruta con sesión lee. Una máquina puede dejarte un recado; leerlo es cosa tuya. Y esto está grabado en el código con el par método y ruta, no con un prefijo. La misma familia de rutas tiene tres puertas, y solo una está abierta a las máquinas.

Cuando le pides el buzón, te llega una lista con las no leídas, las más recientes primero, con el origen, el autor, el mensaje, el nivel y el metadata ya interpretado. Y puedes marcar cosas como leídas, que también es idempotente: si repites la llamada, el contador baja a cero porque ya no había nada pendiente.

Y aquí me encontré una trampa que vale para cualquiera que integre sistemas. La documentación pública de la API decía dos cosas que ya no eran verdad. Decía que el buzón no pagina y que el campo del cursor es siempre nulo, cuando la especificación y el código ya hacen paginación por cursor. Y decía que si marcas como leídas sin pasar la lista de identificadores recibes un error, cuando el código hace justo lo contrario: sin lista, marca todas. La lección es que el código es el que manda y el documento va un paso por detrás. Casi una ley de la gravedad: la documentación se queda atrás antes de que te des cuenta. Y en un episodio sobre integración esto importa mucho, porque la interfaz que prometes también es parte del contrato. Si integras contra un servicio, no te fíes solo de lo que dice su documentación. Pruébalo. Lo que responde el servidor es la verdad.

Y ahora la parte del aviso que salta en el navegador, el WebPush. Aquí hay una decisión que me gusta. La fila se guarda antes de responder. Cuando la ingesta te devuelve el 201, la notificación ya está en la base de datos. Punto. No hay una ventana en la que el aviso esté en el aire. Y el envío por push ocurre después, en segundo plano, y es best-effort. ¿Qué significa eso? Que si el push falla, se registra en el log y no cambia la respuesta que ya recibiste. Tú ya tienes tu 201 y tu fila guardada; el push es un extra. Porque lo importante es que el aviso no se pierda, no que te haga saltar el móvil.

El push se firma con un par de claves VAPID, que en Valet es único por instalación y se genera en la primera petición, no antes. La migración siembra las claves vacías, y la primera vez que alguien pide la clave pública, el servidor genera el par y lo guarda. Y si dos peticiones lo piden a la vez, no se generan dos pares. La escritura va dentro de una sola transacción, con un INSERT ... ON CONFLICT ... DO UPDATE que solo escribe si el valor estaba vacío. La primera que escribe gana y la segunda lee lo que la primera dejó. Dos peticiones simultáneas convergen en el mismo par.

Y una regla de higiene que se repite: la clave privada VAPID nunca aparece, ni en un Debug, ni en un log, ni en un mensaje de error, ni en una respuesta HTTP. El endpoint público devuelve solo la pública. Y si por lo que sea las claves no se pueden cargar, responde que el push no está disponible en lugar de devolver un 200 con una clave vacía para salir del paso. Prefiere decirte que no hay push antes que fingir que sí.

La prueba real, en cinco pasos

Todo esto que te cuento lo he probado de verdad, contra una instancia local de Valet levantada con una base de datos SQLite de usar y tirar, sin tocar nada de producción. Y no te lo dejo a deber: los scripts de la demo están publicados en un gist, con la salida real de cada paso.

Para probarlo, la primera decisión es arrancar el backend con la autenticación desactivada y apuntando a una base efímera:

cd /data/rust/valet
mkdir -p /tmp/opencode/valet-847
DATABASE_URL="sqlite:/tmp/opencode/valet-847/valet-demo.db" \
  AUTH_ENABLED=false HOST=127.0.0.1 PORT=3458 ./target/debug/valet

Dos avisos que te ahorran el rato que perdí yo. Uno, usa el binario debug, no el release, que en mi copia estaba caducado y reventaba al arrancar por una extensión vectorial. Y dos, arranca el backend desde el directorio del repositorio, porque las migraciones se resuelven por ruta relativa y desde otro sitio fallan.

Paso uno: crear la clave. Esto lo haces con sesión, porque crear claves es una operación de humano.

curl -i -X POST http://127.0.0.1:3458/api/ingest-keys \
  --cookie "valet_session=<tu-sesion>" \
  -H "Content-Type: application/json" \
  -d '{"name": "backup-host"}'

Y te responde un 201 con cuatro campos, el identificador, el nombre, el prefijo y el token. Ese token es la única vez que lo vas a ver. Cópialo y mételo en el gestor de secretos de tu script, porque no hay segunda oportunidad.

Paso dos: mandar la notificación. Y aquí usas el token, no la sesión.

curl -i -X POST http://127.0.0.1:3458/api/notifications \
  -H "Authorization: Bearer val_<tu-token>" \
  -H "Content-Type: application/json" \
  -d '{"message": "Backup terminado con 3 avisos",
       "origin": "backup-host",
       "author": "nightly-job",
       "level": "warning",
       "metadata": {"warnings": 3}}'

Te devuelve un 201 con un identificador y nada más. La fila ya está guardada. Y cada campo tiene su límite, que conviene saber: el mensaje es obligatorio y no puede pasar de 4096 bytes, el origen es opcional con tope de 64 bytes y si no lo pones se rellena con el nombre de la clave, el autor es opcional con tope de 128 bytes, el nivel es uno de info, warning o critical, y el metadata es un objeto JSON que serializado no puede pasar de 8192 bytes.

Un detalle del nivel que me hizo gracia: se compara exacto contra la lista, no se recorta. O sea, " info " con espacios se rechaza. Es una tontería, pero es de esas tonterías que evitan que un día un script te cuele un nivel que no existe. Y los espacios que se te cuelan al copiar un curl o al editar un fichero de configuración son una fuente de fallos real, y aquí el servidor no los perdona.

Paso tres: leer el buzón. Y esto lo haces con sesión, porque el buzón es tuyo.

curl -i "http://127.0.0.1:3458/api/notifications/unread?limit=20" \
  --cookie "valet_session=<tu-sesion>"

Te llega la lista, más recientes primero, con el metadata ya interpretado. El límite por defecto es 50 y el máximo 200.

Paso cuatro: marcar como leídas. Un POST con la lista de identificadores, y te responde cuántas se marcaron.

curl -i -X POST http://127.0.0.1:3458/api/notifications/mark-read \
  -H "Content-Type: application/json" \
  -d '{"ids": ["<id-de-la-notificacion>"]}'

Es idempotente: si repites la llamada, el contador baja a cero. Y si le pasas un identificador que no existe, lo ignora sin error. Ahora bien, si le mandas una lista vacía o nula, te responde un 400, porque eso es pedir «marca nada», que no tiene sentido.

Paso cinco: la prueba de fuego. Revocar la clave.

curl -i -X DELETE http://127.0.0.1:3458/api/ingest-keys/<id-de-la-clave>

Recibes un 204. Y ahora vuelves a mandar la misma notificación con el mismo token, y recibes un 401, igual que si el token fuera inventado. La clave dejó de funcionar en el instante en que le pusiste la marca. Revocas una vez y esa máquina ya no te habla.

Fíjate en lo que acabamos de hacer en cinco pasos. Le hemos dado voz a una máquina, hemos recogido su aviso, lo hemos leído, lo hemos archivado y le hemos quitado la voz. Todo con dos credenciales distintas y sin que la de la máquina valga para leer tu buzón. Eso es la integración.

Todos los scripts de esta demo, con sus casos límite y la salida real de cada paso, los he dejado en un gist para que puedas reproducirlos:

El correo, el RSS y Strava: un molde, tres servicios

Hasta aquí la parte de dentro hacia fuera, la de recibir avisos de mis scripts. Pero mi asistente no solo recibe cosas de fuera: también habla con servicios de fuera. Y ahí hay tres clientes integrados que siguen exactamente el mismo patrón.

El primero es ApiMail, correo por API. Con él, las herramientas del correo listan los no leídos, leen un cuerpo, marcan como leído y envían. Y aquí enlaza con lo que te conté en el episodio de las manos: enviar un correo es una de esas operaciones que piden mi aprobación expresa, así que aparece un cuadro de diálogo pidiéndome confirmación. No quiero que un modelo decida por su cuenta mandar un correo en mi nombre.

El segundo es Miniflux, el lector de RSS. Con él funcionan las herramientas del feed. Un detalle que confunde a mucha gente al integrar RSS: Miniflux no autentica con Bearer, sino con una cabecera X-Auth-Token. Confundirlo es el fallo clásico.

El tercero es Strava, y este es distinto porque es OAuth2, con un único atleta y un scope de solo lectura. Valet lee mis actividades de running y no puede escribir nada en mi Strava. Ni una actividad, ni un comentario. Solo mira. Ese scope mínimo es una decisión consciente: si el asistente no necesita escribir, no le doy permiso para escribir.

¿Y por qué no lo hice todo con MCP, el protocolo de contexto de modelos, que es lo que está de moda? Pues porque no me hacía falta. Con un cliente directo escojo solo las partes que me interesan. No tengo que suscribir todo el contrato que vendría de un MCP. De ApiMail tengo las operaciones concretas que necesito, ni una más. Menos superficie, menos cosas que se pueden romper.

Y lo bonito es el molde, porque los tres siguen el mismo patrón, palabra por palabra. En cada llamada, la credencial se resuelve igual: primero se mira en los ajustes de la aplicación, si no está se mira en una variable de entorno, y si tampoco, se cae a un valor por defecto, que solo existe para la URL porque para la clave no hay defecto posible. Esa cascada, ajustes, entorno y defecto, está escrita igual en los tres clientes, a propósito. Es un molde.

La segunda pata del molde es la traducción de errores. Los tres convierten las respuestas de fuera a sus propios tipos de error, con mensajes en español y accionables. Si falta la credencial, te dice dónde configurarla. Si el otro extremo dice que no autorizado, te dice que la credencial no vale. Nada de un error genérico y a callar.

Y la tercera pata, la que más me gusta porque es la que casi nadie hace, es que la credencial nunca aparece. Ni en un Debug, ni en un log, ni en un mensaje de error. Los tres clientes implementan a mano el Debug de su configuración para redactar la clave. La ves como asteriscos, nunca como ella misma. Es la misma higiene que veíamos en las claves de ingesta, ahora aplicada a tres servicios distintos. Un mismo autor, un mismo criterio.

Discriminar, no acumular

Y con todo esto montado, vuelvo al principio, al problema del ruido, porque es ahí donde esto se gana el sueldo.

La idea que más me ha cambiado la forma de trabajar es que no quiero acumular notificaciones, quiero discriminarlas. Y eso es exactamente lo que un modelo de lenguaje hace bien. Le das una pila de avisos y te devuelve lo que importa. Te dice que hubo una caída general y que ya está resuelta. Te dice que de doce correos hay cinco que cuentan algo. Te dice qué es urgente y qué puede esperar.

Piensa en el caso del correo, que es donde más partido se le saca. Un modelo de lenguaje es capaz de mirar un mensaje y decirte si es phishing sin que tú lo abras. Y no lo dice a ojo, sino fijándose en las señales que importan. Si el remitente real no coincide con el nombre que se muestra, si el enlace apunta a un dominio que no tiene nada que ver con el que dice ser, si el tono es de urgencia fingida, si te pide credenciales o datos de pago por un canal que no toca. Todo eso lo pesa y te da una respuesta razonada. Tú, mientras tanto, ni has abierto el mensaje. Eso es tiempo que te ahorras y, si me apuras, un disgusto que te quitas de encima.

Lo mismo con las caídas. Cuando varias webs se caen a la vez, un modelo atento no se limita a listártelas. Te dice que la simultaneidad no es casualidad y que mires el proxy, porque si todas comparten proxy y todas se han caído juntas, el sospechoso es el proxy, y no cada web por separado. Eso es lo que hace un buen diagnóstico, y es justo lo que se pierde cuando tienes cien notificaciones y las miras de reojo.

Y hay un cambio de enfoque que quiero subrayar, porque va más allá de la técnica. Antes, el flujo era de fuera hacia dentro: los servicios me empujaban notificaciones al móvil y yo vivía pendiente de ellas. Ahora el flujo es proactivo: cuando necesito saber qué hay, pregunto, y me llega agrupado y con prioridades. Ya no me llegan las setenta alertas al móvil. Me llega una respuesta.

De hecho, ahora mismo todas las notificaciones que entran las sigo viendo como WebPush, pero puedo volver a agruparlas para que solo me salten las más importantes. Eso es tener el control. Tú decides cuándo quieres ingestar y cuándo no, qué dejas abierto y qué cierras. Y no es que estés abriendo una puerta a tu casa, es que estás abriendo una ventana. Le das a la máquina un canal para dejarte un recado y un botón para cerrarlo cuando quieras. Esa diferencia, la de la ventana frente a la puerta, es toda la diferencia entre una integración que duermes tranquilo y una que te desvela.

Lo que me llevo

Y con esto cierro el arco. Recapitulando en treinta segundos. Un asistente que recuerda, actúa y habla con tu mundo es un asistente. Uno que solo charla es un juguete caro. Y la pata que faltaba era esta, la de la integración.

Lo que me llevo de hoy es una API con dos fronteras: al humano se le identifica con sesión, y a la máquina con una clave de ingesta. La única ruta abierta a las máquinas es la de ingesta, y la exención está puesta sobre el par método y ruta, no sobre el prefijo. La clave es un token con treinta y dos bytes aleatorios que se enseña una sola vez, del que el servidor guarda solo el hash y los doce primeros caracteres, que se revoca sin borrar, que es idempotente y cuyos rechazos no revelan nada. El buzón se lee con sesión, la fila se guarda antes de responder y el push va después, como un extra. Y hacia fuera, el asistente habla con el correo, con el RSS y con Strava siguiendo el mismo molde, con la credencial siempre redactada.

Si esto te ha picado la curiosidad, el asistente es público y con licencia MIT, y hay un fichero que se lee en cinco minutos donde está todo lo que hemos contado hoy: la generación del token, el hash, el prefijo, la revocación que no borra y ese Debug escrito a mano. Clónalo y échale un ojo.

Y te dejo una pregunta, porque me interesa de verdad tu opinión. ¿Tú guardarías el token en claro en algún sitio para poder volver a enseñarlo, o lo darías por perdido como hace Valet? Esa decisión tiene partidarios en los dos bandos, y no sé si hay una respuesta correcta. Lo que sí sé es que la ventana frente a la puerta marca la diferencia. Y como siempre, disfruta como si no hubiera un mañana, y si puede ser con Linux, mejor que mejor. La vida son dos días, y uno ya ha pasado.


Más información

Deja una respuesta