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.

710 lines
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](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<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](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](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<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.