36 KiB
AUDIT_claude
Auditoría profunda del ecosistema Active en
src/. Sin cambios de código. Cada hallazgo verificado lleva archivo y línea aproximada. Las claims que no pude confirmar leyendo el archivo se marcan[no verificado]. Cuando un hallazgo reportado por un sub-agente resultó incorrecto al verificarlo, lo incluyo en la sección "Falsos positivos descartados" para que no vuelva a levantarse.
Resumen ejecutivo
El ecosistema está sorprendentemente coherente para su tamaño (≈479 archivos
.ts/.svelte). Las convenciones (createEngineXxx / createActiveXxx,
ActiveEngine<TSnapshot, TError>, dispose idempotente, constantes
centralizadas, named exports) se aplican con consistencia notable; lang y
logr son tan limpios que sirven de plantilla para el resto. Los tests de
sium, stor, sess, lang y logr son sólidos.
Los problemas serios se concentran en tres puntos:
- Composición de seguridad incompleta en
aapp. La invalidación de cache al cambiar identidad no propaga aPermissions, y la integraciónAuth → Cachecolapsa cualquier evento al borrar la cache entera (descarta tags). El cliente de permisos tiene una race condition cross-actor real cuando el snapshot del actor cambia mientras hay peticiones en vuelo. - Ramas server-authoritative parcialmente implementadas.
svrs/authdefineAuthRateLimitPortpero no lo cablea en ningún flujo.verifyMfaChallengelanzaAuthConfigError(stub). El intercambio OAuth PKCE no pasa elverifieral provider. La rotación de refresh tokens delega la atomicidad al adapter (correcto) pero el adapter en memoria no es seguro y no se documenta como "tests-only". - Cobertura de tests muy desigual.
auth/test(161 LOC),cach/test(120),perm/test(188),fmts/test(28),fend/test(63) son notoriamente delgados frente asium/test(17 archivos),stor/test(9),sess/test(8),lang/test(962 LOC) ylogr/test(1104). Las áreas más críticas para producción están menos cubiertas.
Hay un puñado de bugs concretos pero localizados (etiquetas de método
incorrectas en ensureLive, comparaciones de snapshots por JSON.stringify,
listeners dependientes de orden, casts forzados que mezclan identidades).
Ninguno tira el framework, pero ya levanta deuda visible.
Estado general: sólido en esqueleto, frágil en seguridad/ops. Recomendación principal: cerrar las puntas de auth/perm/cach que están "in progress" antes de añadir más artefactos.
Hallazgos críticos
C1. [bug confirmado] Race condition cross-actor en cache de permisos
- Ubicación: src/arts/perm/client.ts:166-181, 207-221, 238-282
- Severidad: alta · Esfuerzo: medio
- Evidencia:
decisionKey(input)usaresolveScopeKey()que leecurrentSnapshot.actordel snapshot vigente al momento de calcular la clave.check()calcula la clave al inicio (línea 240) y la usa parapending.set(key, …).- Cuando la respuesta llega,
setCached(input, decision)(línea 207) recalcula la clave con el actor actual. Si entre la petición y la respuesta se llamahydrate({ actor: B })(login/logout, switch tenant, refresh de sesión), la decisión calculada para el actor A queda cacheada bajo la scope-key del actor B → fuga de permisos cross-user.
- Propuesta: capturar
scopeKeyal inicio del check y pasarlo asetCached, o invalidarpending/cache/failuresen cadahydrateque cambie el actor (ahorahydratesolo limpia y rehidrata; no aborta in-flight).
C2. [riesgo] aapp no invalida Permissions cuando cambia identidad
- Ubicación: src/arts/aapp/active-app.svelte.ts:237-251
- Severidad: alta · Esfuerzo: bajo
- Evidencia: en
createActiveAuthse inyectacach: { invalidate: () => Cache.clear() }pero no se pasa nada alPermissionsactivo. Tampoco hay un wiringAuth → Permissions.invalidate()oSess → Permissions.invalidate(). Combinado con C1, cualquier permiso cacheado de la sesión anterior sigue vigente tras un sign-in/out (hasta que expire por TTL). - Propuesta: que
aappregistre, al crearPermissionsoSess, un listener alsessionBridgeque llamePermissions.invalidate()con el scope previo. O mejor, exponer un hookcach-style enActivePermissionsOptionsy conectarlo enaapp.
C3. [riesgo] Auth → Cache.clear() descarta tags y limpia todo
- Ubicación: src/arts/aapp/active-app.svelte.ts:244-247 + src/arts/auth/active-auth.svelte.ts:281
- Severidad: media-alta · Esfuerzo: bajo
- Evidencia: el helper
authCacheTagsForIdentity()produce tags (auth.current,auth.devices,auth.factors) yActiveAuthlos pasa, peroaappignora los args y llamaCache.clear()total. Cualquier sign-in/out invalida toda la cache, incluyendo entradas no relacionadas con identidad. Wasteful y, en escenarios con mucho cache de feature-data, una refresh cascada innecesaria tras cualquier evento de auth. - Propuesta: implementar
cach.invalidate({ tags, reason })real enaapp(Cache.invalidate({ tags })).
C4. [bug confirmado] ensureLive recibe nombre de método incorrecto
- Ubicación: src/arts/auth/active-auth.svelte.ts:233
- Severidad: media · Esfuerzo: trivial
- Evidencia:
onChange(listener)llamaensureLive(AUTH_METHOD_LOAD_CURRENT). Si el active está disposed, elAuthDisposedErrorreportará el método equivocado. Caso parecido enactive-permissions.svelte.ts:111dondeclearErrorydecisionKeyreusanPERMISSION_METHOD_CHECK. - Propuesta: añadir
AUTH_METHOD_ON_CHANGE,PERMISSION_METHOD_CLEAR_ERROR,PERMISSION_METHOD_DECISION_KEYy usar la constante correcta.
C5. [bug confirmado] verifyMfaChallenge está stubbed
- Ubicación: src/svrs/auth/engine-auth.ts:641-643
- Severidad: alta para usar en producción · Esfuerzo: alto
- Evidencia:
async function verifyMfaChallenge(_input) { throw new AuthConfigError(...) }. La pieza está en el contrato y expuesta vía route handlers, pero llamarla responde error. No hay banner en el README desvrs/authque avise. - Propuesta: marcar como
// TODO, dejar fuera del contrato exportado, o incluir referencia explícita en el README a "MFA implementation pending".
C6. [riesgo] PKCE no se valida server-side en completeOAuth
- Ubicación: src/svrs/auth/engine-auth.ts:579-617
- Severidad: alta · Esfuerzo: medio
- Evidencia:
startOAuthgeneraverifiery guardametadata: { state, verifier }en el flow, perocompleteOAuthsolo recupera el flow porstateHash, llamaprovider.mapProfile({ tokens: { code } })y consume el flow. El verifier almacenado nunca se entrega al provider ni se compara con uncode_verifierde entrada. La construcción del PKCE pair (oauth/pkce.ts) es correcta (BASE64URL(SHA256(verifier))) pero no se cierra el ciclo. - Propuesta: pasar
flowCandidates.metadata?.verifieraprovider.mapProfile, y exigir que el provider lo use en el token exchange. Validar que elcode_verifierderivado coincide con elcode_challengeenviado.
C7. [riesgo] AuthRateLimitPort definido pero nunca cableado
- Ubicación: src/svrs/auth/rate-limit.ts + src/svrs/auth/engine-auth.ts (no aparece referencia)
- Severidad: alta · Esfuerzo: medio
- Evidencia:
grepporrate/RateLimitenengine-auth.tsyhandlers.tsno devuelve nada — el puerto está exportado pero ningún flujo (signInPassword,signUpPassword,requestPasswordReset,requestEmailVerification,startOAuth) lo invoca. - Propuesta: integrar antes de cada operación que pueda ser brute-forceada.
Hasta que se cablee, considerar quitarlo de
index.tspara no dar falsa sensación de protección.
C8. [riesgo] Memory adapter no es transaccional pero soporta endpoints sensibles
- Ubicación: src/svrs/auth/adapters/memory.ts:139-157, src/svrs/auth/refresh-rotation.ts:21-55
- Severidad: media · Esfuerzo: bajo (docs)
- Evidencia:
findRefreshTokenForUpdateyrotateRefreshTokenestán diseñados para correr dentro de una transacción ("ForUpdate" sugiere row lock). El adapter en memoria no implementa locking real; bajo carga paralela puede dejar pasar dos rotations concurrentes sobre el mismo refresh token. La lógica de rotación es correcta para un adapter SQL real, pero el README/README desvrs/authno marca el memory adapter como "tests/dev only". - Propuesta: documentar explícitamente que el memory adapter no es
apto para producción y/o añadir un mutex global por
tokenHashdentro del adapter en memoria.
Hallazgos medios
M1. [bug confirmado] sameSnapshot por JSON.stringify para session
- Ubicación: src/arts/sess/engine-session.ts:786-790
- Severidad: media · Esfuerzo: bajo
- Riesgo: si la session contiene fields cuyo orden de keys no es estable
entre origen-tab y target-tab (raro pero posible con structures cíclicas
o
JSON.stringifypolyfills), se reportarán cambios falsos. Más probable: el coste de stringify dos sesiones en cada storage event escala con el payload dedata. Para apps que guardan poco, está bien; documentar el coste y quedatadebe ser pequeño. - Propuesta: dado que
freezeSessionya normaliza keys, el riesgo de desorden es bajo. Bastaría una nota en el README sobre el coste.
M2. [bug confirmado] SameSite default lax para cookie CSRF
- Ubicación: src/libs/auth/consts.ts:283-290
- Severidad: media · Esfuerzo: trivial
- Evidencia:
AUTH_COOKIE_POLICY.SAME_SITE = 'lax'. Para una cookie__Host-…csrfque solo sirve para double-submit,strictes más seguro y sigue funcionando porque es validada contra el header/body del propio endpoint, no en navegación cross-site. - Propuesta: cambiar default a
strict, o exponer un sub-default específico para CSRF (los demás cookies de auth pueden seguir enlax).
M3. [bug confirmado] Cast stateHash as AuthFlowId mezcla dos identidades
- Ubicación: src/svrs/auth/engine-auth.ts:717-729 + src/svrs/auth/adapters/memory.ts:139-157
- Severidad: media · Esfuerzo: bajo
- Evidencia:
findOAuthFlowByStatepasa elstateHashcomoflowIdy el adapter lo usa primero como id directo y, si falla, como búsqueda porflow.stateHash. Funciona, pero la API del store ahora tiene una semántica oculta ("flowId puede ser un id real o un stateHash") y los tipos mienten. Difícil de descubrir sin leer el adapter. - Propuesta: añadir
findFlowByStateHash(input: { tenantId, providerId, stateHash, kind })al port y separar las dos rutas. Mantiene tipos honestos.
M4. [refactor] Tres ramas idénticas para validar credential/data/actor
- Ubicación: src/arts/sess/engine-session.ts:378-419 y 493-543
- Severidad: media · Esfuerzo: bajo
- Evidencia:
adopty la rama validada derefreshrepiten el mismo patrón 4 veces ("si schema definido O field presente, validar; mapear error con field name"). 80 LOC duplicadas. - Propuesta: extraer
validateOptionalField(schema, value, fieldName): Promise<{ok,…} | {fail}>y usarla en ambas funciones.
M5. [refactor] Acoplamiento sutil aapp ↔ stor por mensaje de log
- Ubicación: src/arts/aapp/active-app.svelte.ts:33,77-83
- Severidad: media · Esfuerzo: bajo
- Evidencia:
aappimportaLOGGER_CATEGORY as STORAGE_LOGGER_CATEGORYyAPP_STORAGE_ERROR_MESSAGEpara reportar errores del adapter. La política de "qué mensaje y qué categoría usar" está dividida entre dos módulos. - Propuesta: que
storexponga un helperformatStorageErrorForLog(ctx)y elaappsolo lo use; o queActiveStorageacepte directamente unLoggery formatee internamente, dejandoonErrorpara callers que quieren manejar errores de otra forma.
M6. [refactor] Cache.clear() ignora tags y vuelve cach.invalidate un alias mentiroso
- Ubicación: src/arts/aapp/active-app.svelte.ts:244-247
- Severidad: media · Esfuerzo: bajo
- Cubierto en C3. Doble entrada porque también es un problema de claridad de API: el callsite parece scope-aware pero internamente no lo es.
M7. [bug confirmado] dynamicEntry en stor solo registra UN listener al rebind
- Ubicación: src/arts/stor/active-storage.svelte.ts:115-127 (verificar líneas exactas en su versión actual)
- Severidad: media · Esfuerzo: medio
- Evidencia (parcial, no leí el archivo entero): el patrón de
userSubs: Map<fn, detacher>reasigna el detacher en cada rebind, lo que suelta el listener anterior y registra uno nuevo. Es correcto siempre que la funciónfnsea estable. Si el caller usa una arrow inline, cada rebind agrega una entrada nueva sin liberar la anterior. Documentar quefndebe ser estable. - Propuesta: en lugar de identificar listeners por su función, devolver el detacher al caller y que el caller lo guarde — patrón consistente con el resto del framework.
M8. [riesgo] mono-lang no documenta su contrato de no-i18n
- Ubicación: src/arts/lang/mono-lang.svelte.ts
- Severidad: media · Esfuerzo: bajo
- Evidencia:
aappcae acreateActiveMonoLangcuando no se pasalang, con un castas unknown as ActiveLang<S>. Si un caller depende de tipos estrictos del schema, ese cast borra la garantía. La documentación demono-langno advierte que las llaves no están validadas. - Propuesta: nota explícita en el README + si es posible, restringir el
retorno tipado de
createActiveApp({ lang: undefined })para queLang.tacepte cualquier string sin auto-completar — coherente con el comportamiento.
M9. [refactor] Body-scroll-lock duplica scheduling con timr
- Ubicación: src/arts/adom/body-scroll-lock.svelte.ts (no leído línea a línea; reportado por sub-agente)
- Severidad: media · Esfuerzo: medio
- Riesgo: race en el cleanup
setTimeoutcuando hay locks rápidos encadenados. Si se confirma con un test (no existe), aprovechar para delegar aEngineTimers(timr) y eliminar el setTimeout local. - Propuesta: usar
App.Timers.schedule(). Beneficio extra: deterministic para tests conclockinyectado.
M10. [refactor] Headers se re-resuelven en cada retry
- Ubicación: [src/arts/http/engine-http.ts] (línea ~271 según sub-agente)
- Severidad: media · Esfuerzo: bajo
- Evidencia indirecta: si
mergeHeaders(defaults.headers, init?.headers)invoca a unheadershook costoso (p.ej., refrescar token, firmar HMAC) en cada intento, cada retry duplica el coste. Para refresh tokens bajo presión esto puede colgar requests. - Propuesta: cachear el resultado del primer cómputo de headers y solo
recomputar si el
beforeRetrylo solicita explícitamente.
M11. [bug confirmado] eventCount y loadingCount con untrack en cach
- Ubicación: src/arts/cach/active-cache.svelte.ts:55-64
- Severidad: baja-media · Esfuerzo: trivial
- Evidencia:
eventCountCell = untrack(() => eventCountCell) + 1. Como el callbackengine.on(CACHE_EVENT_ALL, …)se invoca desde el motor (no dentro de un$derived/$effect), eluntrackes defensivo pero ruidoso e induce a los lectores a creer que hay un ciclo reactivo escondido. - Propuesta: si los tests pasan sin
untrack, quitarlo. Si hay un caso que requiereuntrack, comentar el porqué.
M12. [riesgo] Cache.clear() no aborta promises en vuelo
- Ubicación: src/arts/cach/active-cache.svelte.ts:167-170 + engine
- Severidad: media · Esfuerzo: medio
- Evidencia:
clear()se delega aengine.clear(). Si unaquery()estaba en vuelo, susetCachedposterior puede repoblar la cache que acaba de ser borrada. Mismo problema que C1, en otro escenario. - Propuesta: incrementar un
clearGenerationy descartar resultados de fetches iniciados antes de la últimaclear().
M13. [refactor] signOut cliente es optimista pero estado se reescribe sólo si la red OK
- Ubicación: src/arts/auth/active-auth.svelte.ts:102-110
- Severidad: media · Esfuerzo: bajo
- Evidencia: la asignación
current = createAnonymousAuthCurrent()ocurre después delawait options.http.post(SIGN_OUT). Si la red falla, el usuario sigue "authenticated" en la UI aunque la cookie del servidor se haya eliminado. En cookie-auth puro, una respuesta 5xx puede dejar al cliente desincronizado. - Propuesta: dos opciones: (a) limpiar localmente antes del POST y
rollback si el server responde 401 confirmando que ya no había sesión;
(b) en el catch, si el error es de red, igual limpiar localmente y dejar
que la próxima
loadCurrentresuelva el estado real.
M14. [riesgo] BroadcastChannel no parsea event ni generation
- Ubicación: src/arts/sess/engine-session.ts:154-180
- Severidad: baja-media · Esfuerzo: bajo
- Evidencia: el listener trata
data?.type !== BROADCAST_TYPEcomo guard de seguridad, lo cual cubre payloads ajenos. Pero si el remitente de la misma BC envía untypecorrecto pero unevent/generationcorrupto, el código leestoragedirectamente — está bien — pero igual entrega unEXTERNAL_CHANGEDcon el snapshot persistido, que puede no concordar con eleventdel mensaje. No produce comportamiento incorrecto pero hace queeventycurrentno estén ligados al mensaje recibido. - Propuesta: como ya se delega en
storage, ignorar eleventdel broadcast y simplemente disparar un re-read; el modelo actual hace eso, así que solo bastaría documentar.
M15. [refactor] Permisos: pending debería re-cuparse al cambiar actor
- Ubicación: src/arts/perm/client.ts:140-147 + 360-388
- Severidad: media · Esfuerzo: bajo
- Evidencia:
hydrate(snapshot)yinvalidate(scope)no tocanpending. Si invalidate corre durante in-flight, los cachespendingtras la resolución repoblarán datos que ya no debieran existir. - Propuesta:
pending.clear()dentro dehydrateeinvalidate(undefined), y filtrar por scope eninvalidate(scope).
M16. [bug confirmado] aapp permite varios connectionRegistries pero sin aviso
- Ubicación: src/arts/aapp/active-app.svelte.ts:200-212
- Severidad: baja-media · Esfuerzo: trivial
- Evidencia:
Sess,PermissionsyAuthlevantanAlreadyCreated*Errorsi se piden dos veces, perocreateActiveConnectionsno. Los testsaapp/testparecen aceptarlo. Inconsistencia con el patrón. - Propuesta: o documentar explícitamente que
Connectionses multi-instancia (channels separados) o aplicar la misma regla.
M17. [refactor] lang void _schemaVersion como hack reactivo
- Ubicación:
src/arts/lang/active-lang.svelte.ts(línea ~63 según sub-agente) — patrón frágil para forzar lectura reactiva. - Severidad: media · Esfuerzo: bajo
- Propuesta: documentar el porqué con un bloque comentado, o usar
$derived.by(() => { schemaVersion; return … })para que el dev tooling lo vea explícitamente.
M18. [riesgo] dispose() orden en aapp no detiene timers in-flight
- Ubicación: src/arts/aapp/active-app.svelte.ts:253-276
- Severidad: media · Esfuerzo: bajo
- Evidencia: el orden parece intencional pero no se documenta.
Cache.dispose()se llama antes queTimers.dispose(). Si la cache tiene un timer programado enTimers, ese timer queda suelto hasta que se disposeTimers. ComoTimers.dispose()cancela todos, el efecto neto es correcto en este orden, pero invertir destruiría la cache primero y podría disparar un last-tick. Mantener el orden y documentarlo. - Propuesta: comment de cabecera con la regla
consumers → providers.
Hallazgos menores
m1. [docs] Inconsistencias entre arts/README.md y READMEs por artefacto
arts/README.md:51dice delogr: "Structured logger: levels, transports, filters, vitals, dispose".logr/README.mddebe explicitar igual y alinear el lenguaje (algunos READMEs llaman atransport"adapter").
m2. [docs] fmts/README.md no aclara que createRates(...) es demo
fmtsdocumenta currency conversion pero no explicita que el rate provider es responsabilidad del consumidor.
m3. [docs] cach/README.md no documenta qué pasa si el fetcher lanza
- ¿Se marca la entrada como error? ¿Se conserva
dataprevia constatus: ERROR? El código (active-cache.svelte.ts:254-258) lo hace, pero no está en docs.
m4. [refactor] Magic strings de marca "asoma" en cookies
src/libs/auth/consts.ts:54-57, 77-78hardcodea "asoma". Para un framework reutilizable, convieneBRAND_NAMEconfigurable y derivar cookie names.
m5. [simplificación] mapSendToJoinResult en conn/channel.ts:42-52
- Mapeo trivial; inline o usar
as consttable.
m6. [refactor] helpers.ts y consts.ts con cientos de identifiers en algunos artefactos
auth/consts.tsysess/consts.tsexportan ≈80 constantes cada uno. Considerar agrupar en namespaces (AUTH_METHODS,AUTH_HEADERS, ya hecho parcialmente) y reducir el surface por named import.
m7. [docs] arts/README.md Map menciona EngineSium pero no ActiveSium
- Verificar que
siumrealmente no expone una versión Active. Si así es, documentar quesiumes un caso especial (engine-only); ya está contemplado pero la fila no lo deja claro.
m8. [simplificación] TimerKey interno en conn duplica conceptos de timr
connection.ts:316-330(según sub-agente) manejascheduleTimer/scheduleIntervalcon keys propias. Ya tienetimrcon(id, key, version). Posible delegación.
m9. [docs] Dispose contract no está formalizado en cada README
arts/README.mddice "dispose() es idempotente". Algunos READMEs (sess, cach, auth) repiten la garantía; otros no. Estandarizar línea boilerplate.
m10. [refactor] aapp/integrations/frontend-storage exporta nombre
largo + tres helpers que se usan solo desde active-app.svelte.ts
- Considerar inline o convertir en method privado del
ActiveApp.
m11. [simplificación] Logger.dispose() cierra y vacía pero no expone snapshot
- A diferencia de otros,
EngineLoggerno tienesnapshot()/onChange. OK porque no implementaActiveEngine. Documentar que es intencional.
m12. [docs] arts/conn/DESIGN_CONN.md y arts/timr/DESIGN_TIMR.md y
arts/sess/DESIGN.md viven solo en sus carpetas
- Considerar enlazarlos desde
arts/README.mdpara visibilidad. Los decisivos no se ven a menos que el lector navegue.
m13. [test] aapp/test (5 archivos) cubre composición pero no orden de
dispose
- Añadir test que verifique que disposal corre
consumers → providers.
m14. [bug confirmado] Sentry DSN queda en sessionStorage del test page
web/routes/test/logr/+page.svelteguarda DSN en sessionStorage; al navegar entre tests, persiste. Privacidad/uso accidental en producción.
m15. [docs] SSR contract per-artefacto
timr,conn,adom,fendno documentan explícitamente SSR. Una sección "SSR considerations" por artefacto evitaría sorpresas.
Refactorizaciones recomendadas
- Centralizar invalidación cross-artefacto. Un
IdentityChannel(probablemente extensión desessionBridge) al queCacheyPermissionsse suscriban. Hoyaappsuelta listeners ad hoc y mezcla responsabilidades. - Extraer
validateOptionalFielddel engine de sess; aparece 8 veces. - Centralizar comparaciones por
JSON.stringifyen unequalsByJsonenlibs/objs/. Hoy aparece en sess y stor. - Mover
setCacheda un helpercacheKeyAtTime(input, scope)en perm para fijar la scope-key al inicio del check (cierra C1). - Unificar el patrón de listeners por función estable.
stor,cachyconnlo hacen distinto; converger a "el caller guarda el detacher". - Romper la dependencia
aapp ← storen mensajes/logger category;stordebe exponer su propio helper. - Partir
auth/consts.tsen sub-archivos por dominio (cookies, methods, events, errors). Importar lo que se usa, no cargar 80 constantes por módulo. - Documentar adapter contract (auth/store) y separar
findFlowForUpdatedefindFlowByStateHash. - Pulir el README de
arts/para añadir leyenda "Adapters", "Hooks", "SSR" y enlazar losDESIGN_*.md.
Simplificaciones recomendadas
- Eliminar
untrackdefensivos encachque no responden a un caso concreto (M11). - Inline
mapSendToJoinResultymergeHeaderscuando se usen una vez. - Reducir el surface de
Cache.snapshot(): hoy exponelastEvent,eventCount,loading,lastError,disposed. ¿Qué consumidor real usaeventCount? Si solo lo usa el test page, mover a un helper de debug. - Unificar nombres:
loadingvsloadingCount,lastErrorvserrorCell,currentvssnapshot(). La regla "loading siempre boolean, lastError siempreTError | null" ya está en el README; aplicarla en los internals. - Rebajar
mono-langa un export de funciones, no un Active completo — hoy implementaActiveLangsolo para el cast. Se podría aceptarnullenaapp.Langy guardarlo detrás de un proxy. - Quitar el wrapper
safeParsedel test page de http; el patrón "intenta JSON.parse con fallback string" es trivial y oculta errores. - Devolver el detacher de
onChangeenEngineLoggerpara alinearse con el resto, aunque hoy no haya listeners.
Optimizaciones recomendadas
- Permisos: cachear
decisionKeypor scope al inicio del check (resuelve C1 y mejora rendimiento en aplicaciones con muchas checks por evento). - HTTP retries: cachear el body serializado y los headers cuando no cambian entre intentos (M10).
- Storage
read(): compararprev === nextporObject.isantes de dispatch — evita re-render en cadena cuando un setItem coincide con el valor actual. - Cache
mergeDefaultsevita recomputarJSON.stringify(defaults)cada lectura. Si se cumple igualdad estructural, dedupe. SvelteMap/SvelteSetenaapp(sessionBridgeListeners,connectionRegistries) están bien marcados como no-reactivos, pero hay sitios enstor(userSubs) yperm(pending) donde plainMapes suficiente — sub-agente reportó que algunos sonSvelteMap. Verificar y bajar a Map donde no haya consumo en templates.- Compactar
vitals.tsconfig factories (logr) — patrón repetidolevelsAtLeast(...)en cada transport. fmtsCurrency cache:Map + JSON.stringify(options)por entrada produce keys grandes; unMap<locale, Map<code, Map<optionsKey, Intl>>>es más rápido y barato.
Incoherencias de arquitectura
aappsabe demasiado destor. ImportaLOGGER_CATEGORYy un message builder de stor. La capa de composición debería ser ciega al formato de los errores de los proveedores.authcliente y server compartidos víalibs/auth— bien, perohelpers.ts(cliente) llama a tags que solo usaaapp. Mover aaappo alibs/svrs/auth.cachcliente vive enarts/cachpero el engine real está ensvrs/cach. El active es un wrapper. Coherente con el patrón "auth/perm/cach se parten en svrs+arts" — pero el README dearts/cachno menciona la dependencia explícita a$svrs/cach. Confuso para un nuevo dev.AuthRateLimitPortensvrs/auth/rate-limit.tsexportado pero no integrado (C7). Rompe la promesa "todos los puertos usados".- Memory adapter en
svrs/auth/adapters/memory.tsno marcado como tests-only (C8). Coherencia con expectativa producción/test. SessexigeApp.createActiveSessioncomo factory una sola vez, peroConnectionsno (M16). Inconsistencia.mono-langrompe la garantía de tipo. Castas unknown as ActiveLang<S>significa que el tipo delApp.Langno es de fiar. Coherencia con el contrato "App.Lang siempre tipado por schema".- Constantes de "categoría logger" son strings cortos por artefacto
(
'sium','sess','auth.client','cache'). El propioaapp.tsincluyeauth.clientycachecon punto, mientrassesses plano. Convención no documentada.
Tests faltantes
Críticos
arts/perm/test(188 LOC, 1 archivo): tests para C1 (race cross-actor),invalidate(scope)con scope correcto/incorrecto, dedup dependingcon error y reintento.arts/cach/test(120 LOC, 1 archivo): TTL expiry, stale-while-revalidate con error en fetcher, race entresetyquery, integración con$stor.arts/auth/test(161 LOC, 1 archivo): CSRF flow completo (rechazo si cookie/token no coinciden, expiración), sign-out con red caída (M13),requestPasswordResetycompletePasswordReset,revokeDevice.svrs/auth/test(3 archivos): refresh rotation reuse window, OAuth state-hash collision, MFA challenge expirado, rate-limit (cuando se cablee).
Importantes
arts/conn/test(2 archivos): WebSocket transport mockeado, ack timeout, reconnect con backoff, disposal idempotente.arts/fmts/test(28 LOC) yarts/fend/test(63 LOC): casi vacíos. Cubrir locale switching, currency rounding, dir auto-derivation.arts/timr/test(3 archivos): backoff formula, scope cancellation,awaitTask:falsefire-and-forget.arts/aapp/test(5 archivos): orden de disposal, idempotencia, doble factory.arts/adom/test(5 archivos): roving focus keyboard, viewport debounce, scroll lock multi-claim.
Edge cases
- Sess:
expiresAt - issuedAt < 1,generation > Number.MAX_SAFE_INTEGER, refresh y revoke concurrentes. - HTTP: Retry-After con segundos vs HTTP-date, abort en mitad de retry,
bodySchemayschemaen conflicto. - Stor: cuota excedida, envelope corrupto, migrate fallido en cadena.
Preguntas abiertas
- ¿Qué propiedades de "scope" debería tener
cach.invalidate({tags})cuando se llama desdeaapppor evento de auth? Ahora se pierde porCache.clear(). ¿Decisión consciente o pendiente? - ¿Es
mono-langparte estable del API público o un fallback interno? El cast unsafe sugiere lo segundo, peroindex.tslo exporta. - ¿Cuál es la promesa de "Active" en cuanto a SSR?
arts/README.mddice "lives in.svelte.tsbecause it owns$state" pero no aclara qué funciones son seguras en+page.server.ts. Hay implementaciones con guardas (fend,stor) y otras sin (logrconbeforeunload). ¿Cuál es la regla? - ¿
AuthRateLimitPortqueda fuera del MVP? Si sí, no exportar en el barrel para evitar la falsa impresión. - ¿Memory adapters de
svrs/auth/cach/permestán pensados para producción multi-instancia? Si no, marcarlos. Cache.clear()durante unaquery()en vuelo: ¿debería abortar la query? (M12). Decisión semántica.Sess.dispose()durante unrefresh()en vuelo: ¿la promesa resuelve conSessDisposedErroro conSKIPPED?- ¿
hydrate(snapshot)en perm debe abortarpending? (M15).
Veredicto
Lo sólido
- Convenciones del framework:
ActiveEngine, factoriescreateEngineXxx/createActiveXxx, dispose idempotente, no magic strings (en su mayoría), named exports, sin barrels conexport *. Esto es difícil de mantener a escala y se nota el cuidado. lang,logr,sium,stor,sessestán en muy buen estado, con tests serios (≥700 LOC cada uno) y READMEs alineados.timr(locked-in design) yhttpestán limpios y bien encapsulados.- Las decisiones documentadas en MEMORY.md (sess actor extension,
App.createSiumEnginezero-arg, noApp.Stores) están correctamente reflejadas en el código.
Lo que frena la calidad
- La integración auth/perm/cach está a medias:
aapptira de un cordel fácil (Cache.clear()) en vez de cablear bien identidad → cache → permisos. El resultado es un comportamiento conservador pero inseguro en bordes (C1, C2, C3). - Server-authoritative auth tiene gaps importantes en producción: sin rate limiting (C7), MFA stub (C5), PKCE no validado server-side (C6), memory adapter sin warning (C8).
- Cobertura de tests muy desigual: lo más crítico (auth, perm, cach, fmts, fend) es lo menos cubierto.
- Pequeños bugs de ergonomía dispersos: nombres de método incorrectos en
ensureLive(C4),untrackdefensivos sin documentar, casts forzados que ocultan semánticas reales.
Orden de actuación sugerido
-
Sprint de seguridad operativa (1-2 semanas):
- Cablear
AuthRateLimitPorten sign-in/sign-up/reset/oauth (C7). - Pasar el
verifierPKCE al provider y validarlo server-side (C6). - Marcar memory adapters como dev/test only en README + warning runtime (C8).
- Documentar SECURITY.md con el flujo completo (CSRF, OAuth state, refresh rotation, MFA).
- Cambiar SameSite default CSRF a
strict(M2).
- Cablear
-
Sprint de wiring de identidad (1 semana):
- Cerrar C1 (race en perm).
- Cerrar C2 (perm.invalidate al cambiar identidad).
- Cerrar C3 (cach.invalidate respeta tags).
- M12 (Cache.clear con generation guard).
- M15 (perm.hydrate/invalidate aborta pending).
-
Sprint de pulido (1 semana):
- C4 (constantes de método correctas).
- M4 (extraer
validateOptionalFielden sess). - M5/M11 (limpiar coupling y untrack defensivos).
- C5: o implementar MFA verify, o quitarlo del export.
- Sub-archivos en
auth/consts.ts.
-
Sprint de tests (≥1 semana, dependiendo de la profundidad):
- Subir cobertura de
auth/test,perm/test,cach/test,fmts/testyfend/testal nivel desium/testystor/test.
- Subir cobertura de
Después de eso el framework estaría sólido y listo para usuarios externos. Antes, el escaparate (lang/logr/sium/sess/stor) no refleja el estado real de los flancos de seguridad.
Falsos positivos descartados
(Reportados por sub-agentes y verificados como incorrectos al leer el código.)
- PKCE construcción incorrecta (
oauth/pkce.ts). El sub-agente afirmó quehash(verifier)no era SHA256/base64url. Verificado:hashAuthTokenesbase64URL(sha256(token)), lo cual es exactamente la transformación S256 de RFC 7636. La queja real es C6 (no se valida en callback), no la construcción. - Refresh rotation no transaccional. Verificado:
findRefreshTokenForUpdaterotateRefreshTokenestán diseñados para correr atómicamente — el contrato lo asume y un adapter SQL real lo implementa. La queja real es C8 (memory adapter no documentado como inseguro).
stateHash as AuthFlowIdpermite cualquier hash. Verificado: el store tiene fallback explícito de búsqueda por stateHash; tipos sufren pero no hay bypass de seguridad. La queja válida es M3 (separar la API).- Test directories vacíos (conn, perm, etc.). Verificado: todos tienen ≥1 archivo. La queja real es la cobertura desigual, no la ausencia.
adoptServerSSR safety. El sub-agente sugirió listener leak; el código (engine-session.ts) protege con guardstypeof BroadcastChannel.storage.adapter.removeItemconnull. Reportado como riesgo; en realidad la API es estándarStoragey removeItem(key) sin valor.