Spaces:
Running
Running
Commit History
-
Sesiones revocables: logout real y cierre en todos los dispositivos
Jose Salazar +1: Claude Opus 5ARCHITECTURE_REVIEW §2.3. Las sesiones son cookies firmadas sin estado y el logout se limitaba a borrar la cookie del navegador: el token seguía siendo VÁLIDO allá donde se hubiera copiado, hasta `session_max_age_s` (8h). No había forma de invalidar una sesión filtrada ni de echar a nadie tras un incidente, salvo rotar MORPHOS_SESSION_SECRET, que tira a todos los usuarios a la vez. Dos mecanismos, porque resuelven cosas distintas (migración 4): - `sesiones_revocadas`: una sesión concreta, por `jti`. Se recuerda sólo hasta que su firma caduca por sí sola; a partir de ahí la fila sobra y se poda. - `usuarios.sesiones_validas_desde`: TODAS las de una cuenta a la vez, con el nuevo POST /api/auth/logout-todas. Un sello temporal en vez de un contador de versión porque la pregunta es «¿se emitió antes del corte?». `jti` y `emitida_en` los añade `firmar_sesion`, no quien llama: si se generaran fuera, una ruta podría emitir por descuido una sesión irrevocable. TIMESTAMPS EN PYTHON, NO …2f5c422 -
Aislar por clínica los resultados de analizador
Jose Salazar +1: Claude Opus 5ARCHITECTURE_REVIEW §2.1 (+ la parte de identidad de §2.9). Era el hallazgo más grave de la revisión: `lab/almacen.py` era un dict global sin dueño y `routers/lab.py` no filtraba nada, así que CUALQUIER sesión autenticada podía leer la muestra de cualquier clínica, y `/api/lab/pendientes` las enumeraba todas —sin necesidad siquiera de adivinar IDs—. Cada `muestra_id` abre el panel completo de analitos más las pistas de paciente (nombre de la mascota, raza, sexo). IDENTIDAD DE DISPOSITIVO. `lab_api_keys` pasa de lista de secretos anónimos a `clinica:clave`. `verificar_dispositivo` devuelve el tenant dueño en vez de None, y el recorrido sigue siendo en tiempo constante sobre TODAS las claves (un lookup por diccionario sobre un secreto filtra por tiempo). Revocar = quitar la clave y reiniciar. DE DÓNDE SALE EL TENANT. Del servidor, siempre: de la API key en la ingesta y de la cookie FIRMADA en la lectura. Nunca del cuerpo de la petición —si el puente pudiera declarar su clínica, mentir …1339cdc -
IP real detrás del proxy y caché de papers atómica y acotada
Jose Salazar +1: Claude Opus 5ARCHITECTURE_REVIEW §2.6 y §2.5. PROXY (§2.6). El limitador usaba `request.client.host`, que detrás de un proxy —HF Spaces tiene uno— es la dirección del PROXY. `limite_login` (5/minute) y `limite_papers` no eran por IP sino GLOBALES: a la vez un bypass (fuerza bruta desde muchas IPs contra un único contador) y una auto-denegación de servicio (un cliente ruidoso agota el login de todos). El throttle por email+IP de `auth.py` tenía el mismo problema. `X-Forwarded-For` NO se lee a ciegas: la manda el cliente, y creérsela permite falsificar la IP en cada petición. El operador declara cuántos proxies hay delante (`MORPHOS_PROXY_SALTOS_CONFIABLES`, 0 por defecto) y se toma el elemento -N, que es el último escrito por infraestructura de confianza; lo que hay a la izquierda lo controla el cliente y se descarta. Aviso al arrancar si en prod sigue a 0, porque el fallo es silencioso: los límites "funcionan", sólo que compartidos. CACHÉ DE PAPERS (§2.5). Tres fallos: - `write_text` no es atómi…5118fc3
-
No reintentar el 429 y limitar /api/interpret también por usuario
Jose Salazar +1: Claude Opus 5Dos cambios que atacan lo mismo: la cuota de GPU compartida es el recurso escaso de una instancia pública gratuita, y hoy se malgasta. 1) El reintento no distinguía qué fallo estaba tratando. service.py reintentaba ante cualquier ErrorModelo. Para la salida malformada de medGemma (razonamiento filtrado, bucle de repetición) eso es correcto y es su razón de ser. Para un 429 es contraproducente: gasta otra reserva de GPU del mismo pozo agotado, justo cuando el Space pide parar. Para un rechazo por seguridad o una clave ausente, no puede cambiar nada. ErrorModelo pasa a llevar `reintentable` y `saturado`; cada punto de fallo se clasifica (429 y cuota → no reintentable; 5xx → sí; config y rechazos → no). Un 429 ahora cuesta UNA llamada en vez de dos. /api/interpret responde 503 + Retry-After ante saturación, no un 502 genérico: el 502 invita a recargar en bucle contra una cuota ya agotada. 2) El rate limit era sólo por IP. ZeroGPU es por cuenta, no por …a5d73ff -
No tragar errores de programación al leer la sesión
Jose Salazar +1: Claude Opus 5leer_sesion capturaba (BadSignature, Exception). El `Exception` hacía redundante el `BadSignature` y, sobre todo, convertía cualquier error interno en «no hay sesión»: un bug en la capa de sesión salía como un 401 silencioso en vez de propagarse, que es justo lo que no se quiere en el camino de autenticación. Se captura BadData, el padre común de: - BadSignature firma inválida o falsificada - SignatureExpired sesión caducada (subclase de BadSignature) - BadPayload token corrupto Las tres son «cookie no válida» → sin sesión. Un TypeError o un fallo de configuración ahora se propagan. Nota: (BadSignature, SignatureExpired) —la corrección que se propuso primero— habría repetido el mismo error en pequeño, porque SignatureExpired ya es subclase de BadSignature. Se añaden tests de sesión (no había ninguno), incluido el caso negativo que motiva el cambio: un error interno debe propagarse, no devolver None. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>5af9605 -
Añadir el stack nuevo: Vite/TypeScript + FastAPI
Jose Salazar +1: Claude Opus 5frontend/ Motor de patrones portado a TS (analisis.ts) con 27 tests dorados que fijan el comportamiento validado por veterinarios, más el resto de módulos (ui, pdf-parser, auth, papers, lab-import, ia). backend/ Servicio FastAPI: salida estructurada con Pydantic, clientes medGemma/Claude, recuperación RAG y toda la seguridad (sesión, CSRF, rate limit, CORS cerrado, BD fuera del webroot). bridge/ Puente local ASTM/HL7 para analizadores de laboratorio. evals/ Arnés de evaluación clínica + puerta de CI. index.html pasa a cargar el bundle TS en lugar de js/main.js. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>70e641d