diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index ad6b725..e42bb76 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -18,6 +18,7 @@ Estado al cierre: - `arts/stor/engine-storage.ts` delega el registro de defaults conflictivos en `defaults-registry.ts`. - `arts/sess/engine-session.ts` delega la clasificacion `none/anonymous/identified` en `session-identity.ts`. - `arts/sess/engine-session.ts` delega snapshot, generation, dispatch, persistencia y commits en `session-state.ts`. + - `arts/sess/engine-session.ts` delega la resolucion de revoke local/global/degradado en `session-revoke.ts`. - Integracion total ampliada: `Auth.signOut()` valida anonimizacion, invalidacion de `Permissions` y evento `Cache.invalidate`. - Tanda focalizada verde: `npx vitest run src/arts/conn src/libs/cach src/arts/cach src/svrs/cach src/arts/auth src/svrs/auth src/libs/auth src/arts/aapp/test/ecosystem.integration.test.ts` -> 19 archivos, 82 tests. - `/test/ecosystem` revisado en navegador: carga sin errores de consola, `ar` cambia a `rtl`, Formats se actualiza por locale, Perm cambia con rol `viewer`, Cach re-scopea por locale y Conn loopback publica/recibe. diff --git a/src/arts/sess/engine-session.ts b/src/arts/sess/engine-session.ts index d2006fc..118add0 100644 --- a/src/arts/sess/engine-session.ts +++ b/src/arts/sess/engine-session.ts @@ -21,11 +21,7 @@ import { REFRESH_STATUS_FAILED, REFRESH_STATUS_REFRESHED, REFRESH_STATUS_SKIPPED, - REVOKE_REASON_MISSING_REVOKE_URL, - REVOKE_REASON_NETWORK_ERROR, REVOKE_REASON_NO_SESSION, - REVOKE_REASON_SERVER_REJECTED, - REVOKE_SCOPE_GLOBAL, REVOKE_SCOPE_LOCAL, SESSION_DIAGNOSTIC_EVENTS, SKIP_REASON_NO_REFRESH_FN, @@ -41,6 +37,7 @@ import { readField, toError } from './session-helpers.ts'; +import { resolveSessionRevoke } from './session-revoke.ts'; import { createSessionBroadcastSync, hydrateSessionStorage, @@ -312,78 +309,14 @@ export function createEngineSession { + readonly current: Session | null; + readonly options: RevokeOptions; + readonly onRevoke?: RevokeFn; + readonly logger?: Logger; + readonly diagnostics: SessionDiagnostics; +} + +/** + * Resolve whether revoke is local, global, or degraded. The caller owns the + * actual state mutation so this helper remains pure from the engine runtime. + */ +export async function resolveSessionRevoke({ + current, + options, + onRevoke, + logger, + diagnostics +}: ResolveSessionRevokeInput): Promise { + if (current === null) { + return { + localRevoked: true, + globalRevoked: false, + scope: REVOKE_SCOPE_LOCAL, + reason: REVOKE_REASON_NO_SESSION + }; + } + + const requestedScope = + options.scope ?? (onRevoke !== undefined ? REVOKE_SCOPE_GLOBAL : REVOKE_SCOPE_LOCAL); + + if (requestedScope !== REVOKE_SCOPE_GLOBAL) { + return { + localRevoked: true, + globalRevoked: false, + scope: REVOKE_SCOPE_LOCAL + }; + } + + if (onRevoke === undefined) { + emitSessionDiagnostic(diagnostics, SESSION_DIAGNOSTIC_EVENTS.REVOKE_GLOBAL_NO_HANDLER, { + error: REVOKE_REASON_MISSING_REVOKE_URL + }); + return { + localRevoked: true, + globalRevoked: false, + scope: REVOKE_SCOPE_LOCAL, + reason: REVOKE_REASON_MISSING_REVOKE_URL + }; + } + + try { + const ok = await onRevoke(current, { logger }); + if (ok) { + return { + localRevoked: true, + globalRevoked: true, + scope: REVOKE_SCOPE_GLOBAL + }; + } + + emitSessionDiagnostic( + diagnostics, + SESSION_DIAGNOSTIC_EVENTS.REVOKE_GLOBAL_SERVER_REJECTED, + { error: REVOKE_REASON_SERVER_REJECTED } + ); + return { + localRevoked: true, + globalRevoked: false, + scope: REVOKE_SCOPE_LOCAL, + reason: REVOKE_REASON_SERVER_REJECTED + }; + } catch (err) { + emitSessionDiagnostic(diagnostics, SESSION_DIAGNOSTIC_EVENTS.REVOKE_GLOBAL_HANDLER_FAILED, { + error: toError(err) + }); + return { + localRevoked: true, + globalRevoked: false, + scope: REVOKE_SCOPE_LOCAL, + reason: REVOKE_REASON_NETWORK_ERROR + }; + } +}