# 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`, 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](src/arts/perm/client.ts#L166-L282) - 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](src/arts/aapp/active-app.svelte.ts#L237-L251) - 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/aapp/active-app.svelte.ts#L244-L247) + [src/arts/auth/active-auth.svelte.ts:281](src/arts/auth/active-auth.svelte.ts#L281) - 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](src/arts/auth/active-auth.svelte.ts#L233) - 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](src/svrs/auth/engine-auth.ts#L641-L643) - 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](src/svrs/auth/engine-auth.ts#L579-L617) - 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/rate-limit.ts) + [src/svrs/auth/engine-auth.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/adapters/memory.ts#L139-L157), [src/svrs/auth/refresh-rotation.ts:21-55](src/svrs/auth/refresh-rotation.ts#L21-L55) - 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](src/arts/sess/engine-session.ts#L786-L790) - 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. ### M2. `[bug confirmado]` SameSite default `lax` para cookie CSRF - Ubicación: [src/libs/auth/consts.ts:283-290](src/libs/auth/consts.ts#L283-L290) - 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/engine-auth.ts#L717-L729) + [src/svrs/auth/adapters/memory.ts:139-157](src/svrs/auth/adapters/memory.ts#L139-L157) - 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](src/arts/sess/engine-session.ts#L378-L419) y [493-543](src/arts/sess/engine-session.ts#L493-L543) - 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](src/arts/aapp/active-app.svelte.ts#L33-L83) - 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](src/arts/aapp/active-app.svelte.ts#L244-L247) - 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](src/arts/stor/active-storage.svelte.ts#L115-L127) (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` 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](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`. 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](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](src/arts/cach/active-cache.svelte.ts#L55-L64) - 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](src/arts/cach/active-cache.svelte.ts#L167-L170) + 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](src/arts/auth/active-auth.svelte.ts#L102-L110) - 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](src/arts/sess/engine-session.ts#L154-L180) - 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](src/arts/perm/client.ts#L140-L388) - 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](src/arts/aapp/active-app.svelte.ts#L200-L212) - 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](src/arts/aapp/active-app.svelte.ts#L253-L276) - 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>>` 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` 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.