Saltar a contenido

28 de junio de 2026

Cerré la lógica de acceso del milestone con la pieza que faltaba: la que reconoce a cada persona y le ata su identidad de Microsoft la primera vez que entra.

El acceso queda cerrado: reconocer a la persona y atarle su identidad (Task 3c)

Con la decisión de acceso (3a) y la normalización del correo (3b) ya andando, hoy escribí la pieza que faltaba para terminar la lógica de acceso del milestone: la que empareja a quien inicia sesión con su fila en la whitelist. Suena a perogrullada —"buscá a la persona y listo"—, pero tiene una vuelta que vale la pena contar, porque es justo la que justifica todo el diseño.

El asunto es así. El admin carga a la gente en la whitelist por correo, antes de que entren por primera vez. O sea que de alguien que todavía no inició sesión lo único que tengo es su correo; el identificador estable que le asigna Microsoft —el oid— recién aparece la primera vez que entra de verdad. Por eso ese campo arranca vacío. La pregunta, entonces, es con qué emparejo: ¿por correo, por oid?

La respuesta que implementé tiene dos tiempos. La primera vez que la persona entra, como su oid todavía no está guardado, la reconozco por correo —y aprovecho ese mismo momento para anotarle el oid en su fila—. De ahí en adelante ya lo tengo pegado, así que paso a emparejar por él. ¿Y por qué cambiar de criterio a mitad de camino? Porque el oid es lo único realmente estable. El correo de una persona puede cambiar —se casa y cambia de apellido, la empresa migra de dominio, lo que sea—; el oid no se mueve. Atándome a él una vez que lo tengo, sigo reconociendo a la misma persona aunque su correo cambie de forma, o simplemente de mayúsculas. Eso fue exactamente lo que probé: que un segundo login matchee por oid aunque el correo venga escrito distinto.

Tres cosas del cómo que quiero dejar anotadas. Le di su propio archivo, separado de la decisión de acceso: una cosa es reconocer quién es la persona y otra distinta es decidir si puede entrar, y mezclarlas en un mismo módulo era juntar dos responsabilidades que no tienen por qué viajar pegadas. Acá también se terminó de pagar el haber sacado la normalización del correo a su propio lugar en la 3b: la reuso tal cual, para que el emparejamiento por correo no se caiga por una mayúscula de más. Y la dejé como lógica pura, sin tocar la base: recibe la whitelist y la identidad entrante, devuelve qué hacer —match por oid, primer login con su oid para vincular, o nadie—. Quién carga la lista y quién graba el oid es trabajo del borde, del callback real de inicio de sesión, que entra más adelante en el plan (la tarea 5d). Por ahora esto solo decide.

El ciclo fue el de siempre: los casos primero —el ya vinculado, el oid que manda por sobre el correo, el primer login, el primer login con el correo gritado y con espacios, y el que no figura en la lista—, después el rojo de rigor con el módulo todavía sin existir, y recién ahí la implementación, que son dos búsquedas encadenadas y nada más. Las cinco pruebas en verde, cobertura completa sobre el módulo, tipos y linter sin una sola queja. Con esto la Task 3 queda entera: las tres piezas del acceso —quién entra, cómo se normaliza el correo y cómo se reconoce a cada persona— están escritas y probadas, puro dominio aislado. Lo que sigue es cablearlas en el flujo real de Auth.js, pero ese ya es otro tramo del camino.

¿Cuán fino conviene cortar un commit? Repasé el plan entero

Antes de meterme con la capa de validación me frené a discutir algo que venía arrastrando: con qué grano commiteo de acá en adelante. Hace unos días había aflojado mi propia regla de cadencia —el piso de un commit dejó de ser de tamaño y pasó a ser funcional: cualquier porción probada, verde y que deje el repo andando ya vale como commit, por chiquita que sea—. Pero el plan todavía estaba escrito con la vara vieja, así que lo volví a pasar tarea por tarea con la nueva.

El repaso movió varias cosas. La Task 3, sin ir más lejos, ya había terminado yendo en tres commits, uno por función; el plan seguía diciendo "una, o dos si respira", así que lo puse al día con lo que de verdad pasó. La capa de validación se parte en dos: el schema de tienda por un lado y el de usuario por el otro, que además es la primera vez que la validación del borde agarra una pieza del dominio de acceso —la normalización del correo— y eso me gusta que quede solo en el historial. Y el bloque de Auth.js sube a cuatro commits: el arranque con la validación de los secretos ya es algo andando por sí mismo, sin un solo callback de negocio encima, así que no tiene por qué viajar pegado al primer callback.

Lo más interesante fue una pregunta que me hizo frenar en seco: ¿está mal hacer un commit por cada método de un CRUD? Me había escapado a escribir "vedado" en el plan, y al releerlo me di cuenta de que exageré. La regla no prohíbe eso. Un endpoint solo, con su prueba de integración, es una porción andando: cumple el piso al pie de la letra. Lo que la regla pelea es el otro extremo —el commit gigante con medio milestone adentro— y la granularidad de mentira, esa de partir una sola pieza en pedacitos para inflar el conteo. La línea fina, me quedó claro, no es "endpoint sí o no": es comportamiento contra archivo. Cortar por comportamiento, cada pedazo con su test y saliendo de un ciclo de TDD real, es avance honesto; cortar por archivo, o fingir pasos, no.

Con eso sobre la mesa, para los dos CRUD —tiendas y usuarios— elegí un punto medio que me convence: parto la API en lecturas y escrituras, y después la pantalla. Tres commits cada uno. El GET se prueba y queda firme antes de tocar nada que cambie estado; recién ahí entran las altas, ediciones y bajas con su validación. Lo que no voy a hacer es un commit por verbo —list, create, update, deactivate sueltos—, no porque rompa ninguna regla, sino porque cuatro entradas casi calcadas en este mismo cuaderno se leerían como una lista de supermercado y no como trabajo pensado. El cálculo final quedó en unos veinte commits para lo que resta del milestone, y el plan ahora cuenta esa misma historia de punta a punta. Listo: ahora sí, a la validación.

Primera compuerta de la validación: el schema de tiendas (Task 4a)

Arranqué la capa de validación por donde dije que iba a arrancar: el schema de tiendas. Es la primera vez en el proyecto que escribo algo en lib/validation/, así que antes que nada sumé Zod a las dependencias —la herramienta que ya tenía elegida para esto desde el RFC—. La idea de esta capa es simple de contar pero importante: es la compuerta que corre en el borde, antes de que un solo dato toque la base. Si llega un payload de tienda mal armado, lo frena acá y no más adelante.

Lo lindo del schema es que termina siendo el espejo, en la entrada, de los CHECK que el RFC §5 graba en Postgres. El puerto tiene que caer entre 1 y 65535 y ser entero —nada de un 8069.5 ni de un 70000—; el código de sucursal son exactamente dos dígitos, ni uno ni tres, y dígitos de verdad, no letras; el de empresa, entero; y los textos, nombre y hostname, con contenido real y no tres espacios en blanco. Esa última me gustó resolverla recortando los bordes antes de exigir que quede algo: así un " 10.0.0.12 " entra limpio como "10.0.0.12" y un " " se cae, que es justo lo que quiero.

Fui por el camino de siempre, los casos primero. Escribí once pruebas —la tienda bien formada que pasa, y cada límite roto por separado: puerto bajo, puerto alto, puerto con decimales, codsuc de un dígito, de tres, con letras, codemp no entero, hostname vacío, nombre vacío, y la del recorte de espacios—. Corrí, rojo de cajón porque el módulo todavía no existía, y recién ahí escribí el schema. Once en verde, la suite entera del proyecto en treinta pruebas sin una caída, cobertura al 100% sobre lib, tipos y linter sin una queja. Una compuerta cerrada; la que falta para terminar la Task 4 es la de usuarios, que además va a reusar la normalización de correo que dejé lista en la 3b.