You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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:

  1. Composición de seguridad incompleta en aapp. La invalidación de cache al cambiar identidad no propaga a Permissions, y la integración Auth → Cache colapsa 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.
  2. Ramas server-authoritative parcialmente implementadas. svrs/auth define AuthRateLimitPort pero no lo cablea en ningún flujo. verifyMfaChallenge lanza AuthConfigError (stub). El intercambio OAuth PKCE no pasa el verifier al 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".
  3. 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 a sium/test (17 archivos), stor/test (9), sess/test (8), lang/test (962 LOC) y logr/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) usa resolveScopeKey() que lee currentSnapshot.actor del snapshot vigente al momento de calcular la clave.
    • check() calcula la clave al inicio (línea 240) y la usa para pending.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 llama hydrate({ 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 scopeKey al inicio del check y pasarlo a setCached, o invalidar pending/cache/failures en cada hydrate que cambie el actor (ahora hydrate solo 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 createActiveAuth se inyecta cach: { invalidate: () => Cache.clear() } pero no se pasa nada al Permissions activo. Tampoco hay un wiring Auth → Permissions.invalidate() o Sess → 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 aapp registre, al crear Permissions o Sess, un listener al sessionBridge que llame Permissions.invalidate() con el scope previo. O mejor, exponer un hook cach-style en ActivePermissionsOptions y conectarlo en aapp.

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) y ActiveAuth los pasa, pero aapp ignora los args y llama Cache.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 en aapp (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) llama ensureLive(AUTH_METHOD_LOAD_CURRENT). Si el active está disposed, el AuthDisposedError reportará el método equivocado. Caso parecido en active-permissions.svelte.ts:111 donde clearError y decisionKey reusan PERMISSION_METHOD_CHECK.
  • Propuesta: añadir AUTH_METHOD_ON_CHANGE, PERMISSION_METHOD_CLEAR_ERROR, PERMISSION_METHOD_DECISION_KEY y 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 de svrs/auth que 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: startOAuth genera verifier y guarda metadata: { state, verifier } en el flow, pero completeOAuth solo recupera el flow por stateHash, llama provider.mapProfile({ tokens: { code } }) y consume el flow. El verifier almacenado nunca se entrega al provider ni se compara con un code_verifier de 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?.verifier a provider.mapProfile, y exigir que el provider lo use en el token exchange. Validar que el code_verifier derivado coincide con el code_challenge enviado.

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: grep por rate / RateLimit en engine-auth.ts y handlers.ts no 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.ts para 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: findRefreshTokenForUpdate y rotateRefreshToken está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 de svrs/auth no 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 tokenHash dentro 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.stringify polyfills), se reportarán cambios falsos. Más probable: el coste de stringify dos sesiones en cada storage event escala con el payload de data. Para apps que guardan poco, está bien; documentar el coste y que data debe ser pequeño.
  • Propuesta: dado que freezeSession ya normaliza keys, el riesgo de desorden es bajo. Bastaría una nota en el README sobre el coste.
  • Ubicación: src/libs/auth/consts.ts:283-290
  • Severidad: media · Esfuerzo: trivial
  • Evidencia: AUTH_COOKIE_POLICY.SAME_SITE = 'lax'. Para una cookie __Host-…csrf que solo sirve para double-submit, strict es 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 en lax).

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: findOAuthFlowByState pasa el stateHash como flowId y el adapter lo usa primero como id directo y, si falla, como búsqueda por flow.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: adopt y la rama validada de refresh repiten 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: aapp importa LOGGER_CATEGORY as STORAGE_LOGGER_CATEGORY y APP_STORAGE_ERROR_MESSAGE para reportar errores del adapter. La política de "qué mensaje y qué categoría usar" está dividida entre dos módulos.
  • Propuesta: que stor exponga un helper formatStorageErrorForLog(ctx) y el aapp solo lo use; o que ActiveStorage acepte directamente un Logger y formatee internamente, dejando onError para 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ón fn sea estable. Si el caller usa una arrow inline, cada rebind agrega una entrada nueva sin liberar la anterior. Documentar que fn debe 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: aapp cae a createActiveMonoLang cuando no se pasa lang, con un cast as unknown as ActiveLang<S>. Si un caller depende de tipos estrictos del schema, ese cast borra la garantía. La documentación de mono-lang no 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 que Lang.t acepte 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 setTimeout cuando hay locks rápidos encadenados. Si se confirma con un test (no existe), aprovechar para delegar a EngineTimers (timr) y eliminar el setTimeout local.
  • Propuesta: usar App.Timers.schedule(). Beneficio extra: deterministic para tests con clock inyectado.

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 un headers hook 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 beforeRetry lo 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 callback engine.on(CACHE_EVENT_ALL, …) se invoca desde el motor (no dentro de un $derived/$effect), el untrack es 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 requiere untrack, 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 a engine.clear(). Si una query() estaba en vuelo, su setCached posterior puede repoblar la cache que acaba de ser borrada. Mismo problema que C1, en otro escenario.
  • Propuesta: incrementar un clearGeneration y descartar resultados de fetches iniciados antes de la última clear().

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 del await 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 loadCurrent resuelva 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_TYPE como guard de seguridad, lo cual cubre payloads ajenos. Pero si el remitente de la misma BC envía un type correcto pero un event/generation corrupto, el código lee storage directamente — está bien — pero igual entrega un EXTERNAL_CHANGED con el snapshot persistido, que puede no concordar con el event del mensaje. No produce comportamiento incorrecto pero hace que event y current no estén ligados al mensaje recibido.
  • Propuesta: como ya se delega en storage, ignorar el event del 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) y invalidate(scope) no tocan pending. Si invalidate corre durante in-flight, los caches pending tras la resolución repoblarán datos que ya no debieran existir.
  • Propuesta: pending.clear() dentro de hydrate e invalidate(undefined), y filtrar por scope en invalidate(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, Permissions y Auth levantan AlreadyCreated*Error si se piden dos veces, pero createActiveConnections no. Los tests aapp/test parecen aceptarlo. Inconsistencia con el patrón.
  • Propuesta: o documentar explícitamente que Connections es 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 que Timers.dispose(). Si la cache tiene un timer programado en Timers, ese timer queda suelto hasta que se dispose Timers. Como Timers.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:51 dice de logr: "Structured logger: levels, transports, filters, vitals, dispose". logr/README.md debe explicitar igual y alinear el lenguaje (algunos READMEs llaman a transport "adapter").

m2. [docs] fmts/README.md no aclara que createRates(...) es demo

  • fmts documenta 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 data previa con status: 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-78 hardcodea "asoma". Para un framework reutilizable, conviene BRAND_NAME configurable y derivar cookie names.

m5. [simplificación] mapSendToJoinResult en conn/channel.ts:42-52

  • Mapeo trivial; inline o usar as const table.

m6. [refactor] helpers.ts y consts.ts con cientos de identifiers en algunos artefactos

  • auth/consts.ts y sess/consts.ts exportan ≈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 sium realmente no expone una versión Active. Si así es, documentar que sium es 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) maneja scheduleTimer/scheduleInterval con keys propias. Ya tiene timr con (id, key, version). Posible delegación.

m9. [docs] Dispose contract no está formalizado en cada README

  • arts/README.md dice "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, EngineLogger no tiene snapshot()/onChange. OK porque no implementa ActiveEngine. 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.md para 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.svelte guarda DSN en sessionStorage; al navegar entre tests, persiste. Privacidad/uso accidental en producción.

m15. [docs] SSR contract per-artefacto

  • timr, conn, adom, fend no documentan explícitamente SSR. Una sección "SSR considerations" por artefacto evitaría sorpresas.

Refactorizaciones recomendadas

  1. Centralizar invalidación cross-artefacto. Un IdentityChannel (probablemente extensión de sessionBridge) al que Cache y Permissions se suscriban. Hoy aapp suelta listeners ad hoc y mezcla responsabilidades.
  2. Extraer validateOptionalField del engine de sess; aparece 8 veces.
  3. Centralizar comparaciones por JSON.stringify en un equalsByJson en libs/objs/. Hoy aparece en sess y stor.
  4. Mover setCached a un helper cacheKeyAtTime(input, scope) en perm para fijar la scope-key al inicio del check (cierra C1).
  5. Unificar el patrón de listeners por función estable. stor, cach y conn lo hacen distinto; converger a "el caller guarda el detacher".
  6. Romper la dependencia aapp ← stor en mensajes/logger category; stor debe exponer su propio helper.
  7. Partir auth/consts.ts en sub-archivos por dominio (cookies, methods, events, errors). Importar lo que se usa, no cargar 80 constantes por módulo.
  8. Documentar adapter contract (auth/store) y separar findFlowForUpdate de findFlowByStateHash.
  9. Pulir el README de arts/ para añadir leyenda "Adapters", "Hooks", "SSR" y enlazar los DESIGN_*.md.

Simplificaciones recomendadas

  1. Eliminar untrack defensivos en cach que no responden a un caso concreto (M11).
  2. Inline mapSendToJoinResult y mergeHeaders cuando se usen una vez.
  3. Reducir el surface de Cache.snapshot(): hoy expone lastEvent, eventCount, loading, lastError, disposed. ¿Qué consumidor real usa eventCount? Si solo lo usa el test page, mover a un helper de debug.
  4. Unificar nombres: loading vs loadingCount, lastError vs errorCell, current vs snapshot(). La regla "loading siempre boolean, lastError siempre TError | null" ya está en el README; aplicarla en los internals.
  5. Rebajar mono-lang a un export de funciones, no un Active completo — hoy implementa ActiveLang solo para el cast. Se podría aceptar null en aapp.Lang y guardarlo detrás de un proxy.
  6. Quitar el wrapper safeParse del test page de http; el patrón "intenta JSON.parse con fallback string" es trivial y oculta errores.
  7. Devolver el detacher de onChange en EngineLogger para alinearse con el resto, aunque hoy no haya listeners.

Optimizaciones recomendadas

  1. Permisos: cachear decisionKey por scope al inicio del check (resuelve C1 y mejora rendimiento en aplicaciones con muchas checks por evento).
  2. HTTP retries: cachear el body serializado y los headers cuando no cambian entre intentos (M10).
  3. Storage read(): comparar prev === next por Object.is antes de dispatch — evita re-render en cadena cuando un setItem coincide con el valor actual.
  4. Cache mergeDefaults evita recomputar JSON.stringify(defaults) cada lectura. Si se cumple igualdad estructural, dedupe.
  5. SvelteMap/SvelteSet en aapp (sessionBridgeListeners, connectionRegistries) están bien marcados como no-reactivos, pero hay sitios en stor (userSubs) y perm (pending) donde plain Map es suficiente — sub-agente reportó que algunos son SvelteMap. Verificar y bajar a Map donde no haya consumo en templates.
  6. Compactar vitals.ts config factories (logr) — patrón repetido levelsAtLeast(...) en cada transport.
  7. fmts Currency cache: Map + JSON.stringify(options) por entrada produce keys grandes; un Map<locale, Map<code, Map<optionsKey, Intl>>> es más rápido y barato.

Incoherencias de arquitectura

  1. aapp sabe demasiado de stor. Importa LOGGER_CATEGORY y un message builder de stor. La capa de composición debería ser ciega al formato de los errores de los proveedores.
  2. auth cliente y server compartidos vía libs/auth — bien, pero helpers.ts (cliente) llama a tags que solo usa aapp. Mover a aapp o a libs/svrs/auth.
  3. cach cliente vive en arts/cach pero el engine real está en svrs/cach. El active es un wrapper. Coherente con el patrón "auth/perm/cach se parten en svrs+arts" — pero el README de arts/cach no menciona la dependencia explícita a $svrs/cach. Confuso para un nuevo dev.
  4. AuthRateLimitPort en svrs/auth/rate-limit.ts exportado pero no integrado (C7). Rompe la promesa "todos los puertos usados".
  5. Memory adapter en svrs/auth/adapters/memory.ts no marcado como tests-only (C8). Coherencia con expectativa producción/test.
  6. Sess exige App.createActiveSession como factory una sola vez, pero Connections no (M16). Inconsistencia.
  7. mono-lang rompe la garantía de tipo. Cast as unknown as ActiveLang<S> significa que el tipo del App.Lang no es de fiar. Coherencia con el contrato "App.Lang siempre tipado por schema".
  8. Constantes de "categoría logger" son strings cortos por artefacto ('sium', 'sess', 'auth.client', 'cache'). El propio aapp.ts incluye auth.client y cache con punto, mientras sess es 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 de pending con error y reintento.
  • arts/cach/test (120 LOC, 1 archivo): TTL expiry, stale-while-revalidate con error en fetcher, race entre set y query, 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), requestPasswordReset y completePasswordReset, 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) y arts/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:false fire-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, bodySchema y schema en conflicto.
  • Stor: cuota excedida, envelope corrupto, migrate fallido en cadena.

Preguntas abiertas

  1. ¿Qué propiedades de "scope" debería tener cach.invalidate({tags}) cuando se llama desde aapp por evento de auth? Ahora se pierde por Cache.clear(). ¿Decisión consciente o pendiente?
  2. ¿Es mono-lang parte estable del API público o un fallback interno? El cast unsafe sugiere lo segundo, pero index.ts lo exporta.
  3. ¿Cuál es la promesa de "Active" en cuanto a SSR? arts/README.md dice "lives in .svelte.ts because it owns $state" pero no aclara qué funciones son seguras en +page.server.ts. Hay implementaciones con guardas (fend, stor) y otras sin (logr con beforeunload). ¿Cuál es la regla?
  4. ¿AuthRateLimitPort queda fuera del MVP? Si sí, no exportar en el barrel para evitar la falsa impresión.
  5. ¿Memory adapters de svrs/auth/cach/perm están pensados para producción multi-instancia? Si no, marcarlos.
  6. Cache.clear() durante una query() en vuelo: ¿debería abortar la query? (M12). Decisión semántica.
  7. Sess.dispose() durante un refresh() en vuelo: ¿la promesa resuelve con SessDisposedError o con SKIPPED?
  8. ¿hydrate(snapshot) en perm debe abortar pending? (M15).

Veredicto

Lo sólido

  • Convenciones del framework: ActiveEngine, factories createEngineXxx / createActiveXxx, dispose idempotente, no magic strings (en su mayoría), named exports, sin barrels con export *. Esto es difícil de mantener a escala y se nota el cuidado.
  • lang, logr, sium, stor, sess están en muy buen estado, con tests serios (≥700 LOC cada uno) y READMEs alineados.
  • timr (locked-in design) y http están limpios y bien encapsulados.
  • Las decisiones documentadas en MEMORY.md (sess actor extension, App.createSiumEngine zero-arg, no App.Stores) están correctamente reflejadas en el código.

Lo que frena la calidad

  • La integración auth/perm/cach está a medias: aapp tira 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), untrack defensivos sin documentar, casts forzados que ocultan semánticas reales.

Orden de actuación sugerido

  1. Sprint de seguridad operativa (1-2 semanas):

    • Cablear AuthRateLimitPort en sign-in/sign-up/reset/oauth (C7).
    • Pasar el verifier PKCE 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).
  2. 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).
  3. Sprint de pulido (1 semana):

    • C4 (constantes de método correctas).
    • M4 (extraer validateOptionalField en sess).
    • M5/M11 (limpiar coupling y untrack defensivos).
    • C5: o implementar MFA verify, o quitarlo del export.
    • Sub-archivos en auth/consts.ts.
  4. Sprint de tests (≥1 semana, dependiendo de la profundidad):

    • Subir cobertura de auth/test, perm/test, cach/test, fmts/test y fend/test al nivel de sium/test y stor/test.

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ó que hash(verifier) no era SHA256/base64url. Verificado: hashAuthToken es base64URL(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: findRefreshTokenForUpdate
    • rotateRefreshToken está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 AuthFlowId permite 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.
  • adoptServer SSR safety. El sub-agente sugirió listener leak; el código (engine-session.ts) protege con guards typeof BroadcastChannel.
  • storage.adapter.removeItem con null. Reportado como riesgo; en realidad la API es estándar Storage y removeItem(key) sin valor.

Powered by TurnKey Linux.