Refactor auth cach conn integration paths

master
dev 5 months ago
parent 7b54bdac4c
commit 67b6b0f23a

@ -6,6 +6,12 @@ Estado al cierre:
- `fmts` verde: `npx vitest run src/arts/fmts` -> 14 archivos, 39 tests.
- `conn` verde: `npx vitest run src/arts/conn` -> 5 archivos, 28 tests.
- `auth` verde: `npx vitest run src/arts/auth src/svrs/auth src/libs/auth` -> 4 archivos, 14 tests.
- Refactor tecnico posterior:
- `svrs/auth/engine-auth.ts` ya delega CSRF en `csrf-flow.ts`, coherente con password/session/recovery/device flows.
- `libs/cach/engine.ts` centraliza eventos de lectura con `emitForContext(...)`.
- `arts/conn/connection.ts` usa `createConnectionIdFactory(...)` desde helpers.
- 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.
- `src/arts/aapp/test/ecosystem.integration.test.ts` ampliado para cubrir rol `viewer` no-allow y cache re-scoped por locale.
- Referencias residuales de marca anterior eliminadas de `src/` fuera de rutas temporales: docs, páginas de test y constantes de cookies/headers auth usan ahora `Active/active`.
@ -17,7 +23,7 @@ Estado al cierre:
Pendiente para manana:
- Revisar documentacion restante de `cach`, `perm`, `auth` y `fmts` con ojo de consumidor externo, no solo tecnico.
- Continuar la reduccion de archivos grandes: prioridad `conn/connection.ts`, `libs/cach/engine.ts`, `svrs/auth/engine-auth.ts` y `svrs/auth/adapters/memory-store.ts`.
- Continuar la reduccion de archivos grandes: prioridad `svrs/auth/adapters/memory-store.ts`, `conn/connection.ts` y extraccion progresiva de runtime read/write en `libs/cach`.
- Ampliar tests de integracion cruzada: `auth + sess + perm + cach + http + stor + fmts + conn + timr + logr`.
- Revisar la adopcion final del contrato comun `Logger` / diagnostics en todos los modulos, sin acoplar artefactos a `arts/logr`; primera pasada limpia salvo `aapp` como composition root, `arts/logr` y tests.
- Decidir que hacer con la pagina temporal que bloquea `npm run check`; mientras tanto, validar con `npm test` y tests focalizados.

@ -1,9 +1,12 @@
import { describe, expect, it } from 'vitest';
import { createActiveApp } from '../active-app.svelte';
import {
CACHE_EVENT_ALL,
CACHE_EVENT_INVALIDATE,
CACHE_POLICY_INTERACTIVE,
CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
CACHE_SCOPE_TENANT,
type CacheEvent,
type ResolvedScopeValues
} from '$cach';
import { createMockTransport } from '$conn';
@ -103,6 +106,7 @@ describe('ActiveApp — total ecosystem integration', () => {
const permissionEngine = createPermissionEngine();
const permissionHandlers = createPermissionHttpHandlers(permissionEngine, actorRef);
let projectFetches = 0;
const cacheEvents: CacheEvent[] = [];
const appFetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const path = requestPath(input);
@ -127,6 +131,7 @@ describe('ActiveApp — total ecosystem integration', () => {
}
if (path === AUTH_ROUTE_PATHS.CURRENT) return jsonResponse(authCurrent);
if (path === AUTH_ROUTE_PATHS.CSRF) return jsonResponse({ token: 'csrf-test-token' });
if (path === AUTH_ROUTE_PATHS.SIGN_OUT) return jsonResponse({ ok: true });
if (path === HTTP_PROJECT_PATH) {
projectFetches += 1;
return jsonResponse({ id: PROJECT_ID, tenantId: TENANT_ID, version: projectFetches });
@ -182,6 +187,9 @@ describe('ActiveApp — total ecosystem integration', () => {
}
}
});
const offCacheEvents = App.Cache.on(CACHE_EVENT_ALL, (event) => {
cacheEvents.push(event);
});
try {
App.Logger.info(LOG_CATEGORY, LOG_MESSAGE_BOOT);
@ -238,6 +246,7 @@ describe('ActiveApp — total ecosystem integration', () => {
context: { risk: { mfa: true } }
});
expect(viewerDecision.effect).toBe(PERMISSION_EFFECT_NOT_APPLICABLE);
expect(Permissions.size).toBeGreaterThan(0);
actorRole = ROLE_ADMIN;
Permissions.invalidate();
@ -306,7 +315,13 @@ describe('ActiveApp — total ecosystem integration', () => {
expect(connected.ok).toBe(true);
await Updates.send('project.updated', { id: PROJECT_ID });
expect(transport.sentMessages()).toHaveLength(1);
await Auth.signOut();
expect(Auth.authenticated).toBe(false);
expect(Permissions.size).toBe(0);
expect(cacheEvents.some((event) => event.type === CACHE_EVENT_INVALIDATE)).toBe(true);
} finally {
offCacheEvents();
App.dispose();
permissionEngine.dispose();
}

@ -23,8 +23,6 @@ import {
CONNECTION_CONNECT_REASON_DISPOSED,
CONNECTION_CONNECT_REASON_TRANSPORT_ERROR,
CONNECTION_FRAME_TYPE_PONG,
CONNECTION_ID_PREFIX,
CONNECTION_ID_SEPARATOR,
CONNECTION_DIAGNOSTIC_EVENTS,
CONNECTION_STATE_CLOSED,
CONNECTION_STATE_CLOSING,
@ -43,7 +41,7 @@ import {
emitScopedConnectionDiagnostic
} from './diagnostics.ts';
import { createConnectionFrameBuffer } from './frame-buffer.ts';
import { loggerScope } from './helpers.ts';
import { createConnectionIdFactory, loggerScope } from './helpers.ts';
import { createConnectionHeartbeat } from './heartbeat.ts';
import { createConnectionReconnectPolicy } from './reconnect.ts';
import { wireConnectionSession } from './session-wiring.ts';
@ -76,14 +74,6 @@ interface ConnectionRuntime {
readonly session?: ConnectionSessionSource;
}
function createIdFactory(connectionName: string): () => string {
let next = 0;
return () => {
next += 1;
return [CONNECTION_ID_PREFIX, connectionName, String(next)].join(CONNECTION_ID_SEPARATOR);
};
}
export function createConnection<TChannels extends ConnectionChannelMap = ConnectionChannelMap>(
name: string,
options: ConnectionOptions<TChannels>,
@ -94,7 +84,7 @@ export function createConnection<TChannels extends ConnectionChannelMap = Connec
const frameBuffer = createConnectionFrameBuffer(options.buffer);
const acks = createConnectionAckRegistry(name, runtime.timers);
const timers = createConnectionTimerControls(name, runtime.timers);
const nextId = createIdFactory(name);
const nextId = createConnectionIdFactory(name);
const scope = loggerScope(name, options.loggerScope);
const diagnostics = createConnectionDiagnostics({ logger: runtime.logger, scope });
const events = createConnectionEventBus({ diagnostics });

@ -6,6 +6,8 @@ import {
ERROR_MSG_INVALID_NAME_PREFIX,
ERROR_MSG_NOT_FOUND_PREFIX,
ERROR_PREFIX,
CONNECTION_ID_PREFIX,
CONNECTION_ID_SEPARATOR,
LOGGER_CATEGORY,
LOGGER_SCOPE_SEPARATOR,
LOG_MSG_LISTENER_THREW_PREFIX
@ -50,3 +52,11 @@ export function timerKey(connection: string, kind: string, id?: string): string
? `${LOGGER_CATEGORY}${LOGGER_SCOPE_SEPARATOR}${connection}${LOGGER_SCOPE_SEPARATOR}${kind}`
: `${LOGGER_CATEGORY}${LOGGER_SCOPE_SEPARATOR}${connection}${LOGGER_SCOPE_SEPARATOR}${kind}${LOGGER_SCOPE_SEPARATOR}${id}`;
}
export function createConnectionIdFactory(connectionName: string): () => string {
let next = 0;
return () => {
next += 1;
return [CONNECTION_ID_PREFIX, connectionName, String(next)].join(CONNECTION_ID_SEPARATOR);
};
}

@ -91,6 +91,20 @@ export function createCacheRuntime(config: CacheRuntimeConfig): CacheRuntime {
});
}
function emitForContext(
type: CacheEventType,
ctx: ResolvedCacheContext,
patch: Partial<CacheEvent> = {}
): void {
emit(type, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name,
...patch
});
}
async function readCurrentEpochs(
entryOrParts:
| CacheEnvelope<unknown>
@ -177,13 +191,7 @@ export function createCacheRuntime(config: CacheRuntimeConfig): CacheRuntime {
ctx.policy.mode === CACHE_READ_MODE_BYPASS_CACHE
? CACHE_DECISION_REASON_BYPASS_CACHE
: CACHE_DECISION_REASON_NO_STORE;
emit(CACHE_EVENT_MISS, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
reason,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name
});
emitForContext(CACHE_EVENT_MISS, ctx, { reason });
const value = await options.fetcher();
if (ctx.policy.mode === CACHE_READ_MODE_BYPASS_CACHE) {
const envelope = await createEnvelope(value, ctx, normalizeTags(options.tags));
@ -210,46 +218,22 @@ export function createCacheRuntime(config: CacheRuntimeConfig): CacheRuntime {
const decision = await evaluate(cached, ctx);
if (decision.action === CACHE_DECISION_ACTION_SERVE) {
emit(CACHE_EVENT_HIT, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
reason: decision.reason,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name
});
emitForContext(CACHE_EVENT_HIT, ctx, { reason: decision.reason });
return cached.value;
}
if (decision.action === CACHE_DECISION_ACTION_SERVE_AND_REFRESH) {
emit(CACHE_EVENT_STALE_HIT, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
reason: decision.reason,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name
});
emitForContext(CACHE_EVENT_STALE_HIT, ctx, { reason: decision.reason });
await backgroundRefresh(ctx, options);
return cached.value;
}
if (decision.action === CACHE_DECISION_ACTION_DELETE_AND_FETCH) {
emit(CACHE_EVENT_SCHEMA_MISMATCH, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
reason: decision.reason,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name
});
emitForContext(CACHE_EVENT_SCHEMA_MISMATCH, ctx, { reason: decision.reason });
await safeDelete(ctx.fullKey, ctx.keyHash);
}
} else {
emit(CACHE_EVENT_MISS, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
reason: CACHE_DECISION_REASON_CACHE_MISS,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name
});
emitForContext(CACHE_EVENT_MISS, ctx, { reason: CACHE_DECISION_REASON_CACHE_MISS });
}
return singleflight.run(ctx.fullKey, async () => {
@ -257,12 +241,8 @@ export function createCacheRuntime(config: CacheRuntimeConfig): CacheRuntime {
return await fetchAndStore(ctx, options);
} catch (error) {
if (cached && canServeCacheStaleIfError(cached, clock.now())) {
emit(CACHE_EVENT_STALE_IF_ERROR, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
emitForContext(CACHE_EVENT_STALE_IF_ERROR, ctx, {
reason: CACHE_CONTEXT_REASON_FETCH_ERROR,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name,
error
});
return cached.value;
@ -283,25 +263,13 @@ export function createCacheRuntime(config: CacheRuntimeConfig): CacheRuntime {
const cached = await adapter.get<T>(ctx.fullKey);
if (!cached) {
emit(CACHE_EVENT_MISS, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
reason: CACHE_DECISION_REASON_CACHE_MISS,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name
});
emitForContext(CACHE_EVENT_MISS, ctx, { reason: CACHE_DECISION_REASON_CACHE_MISS });
return undefined;
}
const decision = await evaluate(cached, ctx);
if (decision.action === CACHE_DECISION_ACTION_DELETE_AND_FETCH) {
emit(CACHE_EVENT_SCHEMA_MISMATCH, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
reason: decision.reason,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name
});
emitForContext(CACHE_EVENT_SCHEMA_MISMATCH, ctx, { reason: decision.reason });
await safeDelete(ctx.fullKey, ctx.keyHash);
return undefined;
}
@ -310,26 +278,15 @@ export function createCacheRuntime(config: CacheRuntimeConfig): CacheRuntime {
decision.action === CACHE_DECISION_ACTION_SERVE ||
decision.action === CACHE_DECISION_ACTION_SERVE_AND_REFRESH
) {
emit(
emitForContext(
decision.action === CACHE_DECISION_ACTION_SERVE ? CACHE_EVENT_HIT : CACHE_EVENT_STALE_HIT,
{
key: ctx.fullKey,
keyHash: ctx.keyHash,
reason: decision.reason,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name
}
ctx,
{ reason: decision.reason }
);
return cached.value;
}
emit(CACHE_EVENT_MISS, {
key: ctx.fullKey,
keyHash: ctx.keyHash,
reason: decision.reason,
scopeMode: ctx.scope.mode,
policy: ctx.policy.name
});
emitForContext(CACHE_EVENT_MISS, ctx, { reason: decision.reason });
return undefined;
}

@ -0,0 +1,51 @@
import { AUTH_EVENT_NAMES } from '$libs/auth/consts';
import { issueAuthCsrf, verifyAuthCsrf } from '$libs/auth/csrf';
import type {
AuthCsrfIssueInput,
AuthCsrfIssueResult,
AuthCsrfVerifyInput,
AuthCsrfVerifyResult
} from '$libs/auth/types';
import type { AuthEngineFlowContext } from './engine-internal';
export function createCsrfAuthFlow(context: AuthEngineFlowContext) {
const { options, emit } = context;
async function issueCsrf(input: AuthCsrfIssueInput): Promise<AuthCsrfIssueResult> {
const result = await issueAuthCsrf({
crypto: options.ports.crypto,
clock: options.ports.timr,
tenantId: input.tenantId,
config: options.security.csrf
});
await emit(AUTH_EVENT_NAMES.CSRF_ISSUED, {
name: AUTH_EVENT_NAMES.CSRF_ISSUED,
tenantId: input.tenantId,
meta: input.meta
});
return result;
}
async function verifyCsrf(input: AuthCsrfVerifyInput): Promise<AuthCsrfVerifyResult> {
try {
await verifyAuthCsrf({
crypto: options.ports.crypto,
clock: options.ports.timr,
tenantId: input.tenantId,
token: input.token,
cookie: input.cookie,
config: options.security.csrf
});
return { ok: true };
} catch (error) {
await emit(AUTH_EVENT_NAMES.CSRF_REJECTED, {
name: AUTH_EVENT_NAMES.CSRF_REJECTED,
tenantId: input.tenantId,
meta: input.meta
});
throw error;
}
}
return { issueCsrf, verifyCsrf };
}

@ -2,8 +2,11 @@ import { AUTH_ARTIFACT, AUTH_EVENT_NAMES } from '$libs/auth/consts';
import type { AuthCacheInvalidationInput, AuthEventHandler } from '$libs/auth/contracts';
import type { AuthEventPayloadMap } from '$libs/auth/events';
import { authCacheTagsForIdentity } from '$libs/auth/helpers';
import { issueAuthCsrf, verifyAuthCsrf } from '$libs/auth/csrf';
import type {
AuthCsrfIssueInput,
AuthCsrfIssueResult,
AuthCsrfVerifyInput,
AuthCsrfVerifyResult,
AuthCurrentInput,
AuthCurrentView,
AuthDevicePublicView,
@ -38,6 +41,7 @@ import { createAuthDiagnostics, emitAuthDiagnostic } from './diagnostics';
import { logEntryForEvent } from './engine-logging';
import { validateAuthOptions } from './engine-validation';
import { createDeviceAuthFlow } from './device-flow';
import { createCsrfAuthFlow } from './csrf-flow';
import { createMfaAuthFlow } from './mfa-flow';
import { createOAuthAuthFlow } from './oauth-flow';
import { createPasswordAuthFlow } from './password-flow';
@ -51,12 +55,8 @@ export interface EngineAuth {
signInPassword(input: AuthSignInPasswordInput): Promise<AuthSignInResult>;
signOut(input: AuthSignOutInput): Promise<AuthSignOutResult>;
signOutGlobal(input: AuthSignOutGlobalInput): Promise<AuthSignOutResult>;
issueCsrf(
input: import('$libs/auth/types').AuthCsrfIssueInput
): Promise<import('$libs/auth/types').AuthCsrfIssueResult>;
verifyCsrf(
input: import('$libs/auth/types').AuthCsrfVerifyInput
): Promise<import('$libs/auth/types').AuthCsrfVerifyResult>;
issueCsrf(input: AuthCsrfIssueInput): Promise<AuthCsrfIssueResult>;
verifyCsrf(input: AuthCsrfVerifyInput): Promise<AuthCsrfVerifyResult>;
requestEmailVerification(input: AuthEmailVerificationRequestInput): Promise<AuthFlowPublicResult>;
completeEmailVerification(
input: AuthEmailVerificationCompleteInput
@ -120,6 +120,7 @@ export function createEngineAuth(options: EngineAuthOptions): EngineAuth {
completePasswordReset
} = createRecoveryAuthFlow(flowContext);
const { listDevices, revokeDevice } = createDeviceAuthFlow(flowContext);
const { issueCsrf, verifyCsrf } = createCsrfAuthFlow(flowContext);
const { startOAuth, completeOAuth } = createOAuthAuthFlow(flowContext);
const { createMfaChallenge, verifyMfaChallenge } = createMfaAuthFlow(flowContext);
@ -130,40 +131,8 @@ export function createEngineAuth(options: EngineAuthOptions): EngineAuth {
signInPassword,
signOut,
signOutGlobal,
issueCsrf: async (input: import('$libs/auth/types').AuthCsrfIssueInput) => {
const result = await issueAuthCsrf({
crypto: options.ports.crypto,
clock: options.ports.timr,
tenantId: input.tenantId,
config: options.security.csrf
});
await emit(AUTH_EVENT_NAMES.CSRF_ISSUED, {
name: AUTH_EVENT_NAMES.CSRF_ISSUED,
tenantId: input.tenantId,
meta: input.meta
});
return result;
},
verifyCsrf: async (input: import('$libs/auth/types').AuthCsrfVerifyInput) => {
try {
await verifyAuthCsrf({
crypto: options.ports.crypto,
clock: options.ports.timr,
tenantId: input.tenantId,
token: input.token,
cookie: input.cookie,
config: options.security.csrf
});
return { ok: true as const };
} catch (error) {
await emit(AUTH_EVENT_NAMES.CSRF_REJECTED, {
name: AUTH_EVENT_NAMES.CSRF_REJECTED,
tenantId: input.tenantId,
meta: input.meta
});
throw error;
}
},
issueCsrf,
verifyCsrf,
requestEmailVerification,
completeEmailVerification,
requestPasswordReset,

Loading…
Cancel
Save

Powered by TurnKey Linux.