Split memory auth store by domain

master
dev 5 months ago
parent 67b6b0f23a
commit ab1c581aa3

@ -10,6 +10,7 @@ Estado al cierre:
- `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.
- `svrs/auth/adapters/memory-store.ts` queda como composition root de 39 lineas; la logica se reparte en stores internos de credentials, flows, linked accounts, sessions/devices, refresh y state/snapshot.
- 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.
@ -23,7 +24,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 `svrs/auth/adapters/memory-store.ts`, `conn/connection.ts` y extraccion progresiva de runtime read/write en `libs/cach`.
- Continuar la reduccion de archivos grandes: prioridad `conn/connection.ts`, extraccion progresiva de runtime read/write en `libs/cach` y separar helpers de test/server auth cuando crezcan.
- 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.

@ -0,0 +1,43 @@
import type {
AuthFindLinkedAccountInput,
AuthLinkAccountInput,
AuthStoreAdapter
} from '$libs/auth/contracts';
import type { AuthLinkedAccountRecord } from '$libs/auth/types';
import { clone, type MemoryAuthStoreState } from './memory-store-state';
export function createMemoryLinkedAccountStore(
state: MemoryAuthStoreState
): Pick<AuthStoreAdapter, 'findLinkedAccount' | 'linkAccount'> {
return {
async findLinkedAccount(input: AuthFindLinkedAccountInput) {
for (const record of state.linkedAccounts.values()) {
if (
record.actorRef.tenantId === input.tenantId &&
record.providerId === input.providerId &&
record.providerSubject === input.providerSubject &&
!record.revokedAt
) {
return clone(record);
}
}
return null;
},
async linkAccount(input: AuthLinkAccountInput) {
const record: AuthLinkedAccountRecord = {
id: input.id,
actorRef: input.actorRef,
providerId: input.providerId,
providerKind: input.providerKind,
providerSubject: input.providerSubject,
email: input.email,
emailVerified: input.emailVerified,
createdAt: input.nowMs,
updatedAt: input.nowMs
};
state.linkedAccounts.set(record.id, record);
return clone(record);
}
};
}

@ -0,0 +1,70 @@
import { AUTH_CREDENTIAL_KINDS } from '$libs/auth/consts';
import type {
AuthCreatePasswordCredentialInput,
AuthFindCredentialInput,
AuthMarkCredentialVerifiedInput,
AuthStoreAdapter,
AuthUpdatePasswordCredentialInput
} from '$libs/auth/contracts';
import type { AuthCredentialRecord } from '$libs/auth/types';
import { clone, type MemoryAuthStoreState } from './memory-store-state';
export function createMemoryCredentialStore(
state: MemoryAuthStoreState
): Pick<
AuthStoreAdapter,
| 'findCredentialByIdentifier'
| 'createPasswordCredential'
| 'updatePasswordCredential'
| 'markCredentialVerified'
> {
return {
async findCredentialByIdentifier(input: AuthFindCredentialInput) {
for (const credential of state.credentials.values()) {
if (
credential.actorRef.tenantId === input.tenantId &&
credential.kind === input.kind &&
credential.identifierHash === input.identifierHash
) {
return clone(credential);
}
}
return null;
},
async createPasswordCredential(input: AuthCreatePasswordCredentialInput) {
const record: AuthCredentialRecord = {
id: input.id,
actorRef: input.actorRef,
kind: AUTH_CREDENTIAL_KINDS.PASSWORD,
identifierHash: input.identifierHash,
identifierDisplay: input.identifierDisplay,
passwordHash: input.passwordHash,
createdAt: input.nowMs,
updatedAt: input.nowMs
};
state.credentials.set(record.id, record);
return clone(record);
},
async updatePasswordCredential(input: AuthUpdatePasswordCredentialInput) {
const record = state.credentials.get(input.credentialId);
if (!record || record.actorRef.tenantId !== input.tenantId) return;
state.credentials.set(record.id, {
...record,
passwordHash: input.passwordHash,
updatedAt: input.nowMs
});
},
async markCredentialVerified(input: AuthMarkCredentialVerifiedInput) {
const record = state.credentials.get(input.credentialId);
if (!record || record.actorRef.tenantId !== input.tenantId) return;
state.credentials.set(record.id, {
...record,
verifiedAt: input.nowMs,
updatedAt: input.nowMs
});
}
};
}

@ -0,0 +1,58 @@
import type {
AuthCreateFlowInput,
AuthFindFlowInput,
AuthRevokeFlowsInput,
AuthStoreAdapter
} from '$libs/auth/contracts';
import type { AuthFlowRecord } from '$libs/auth/types';
import { clone, type MemoryAuthStoreState } from './memory-store-state';
export function createMemoryFlowStore(
state: MemoryAuthStoreState
): Pick<AuthStoreAdapter, 'createFlow' | 'findFlowForUpdate' | 'consumeFlow' | 'revokeFlows'> {
return {
async createFlow(input: AuthCreateFlowInput) {
const record: AuthFlowRecord = { ...input };
state.flows.set(record.id, record);
return clone(record);
},
async findFlowForUpdate(input: AuthFindFlowInput) {
const direct = state.flows.get(input.flowId);
if (
direct &&
direct.tenantId === input.tenantId &&
(!input.kind || direct.kind === input.kind)
) {
return clone(direct);
}
for (const flow of state.flows.values()) {
if (
flow.tenantId === input.tenantId &&
(!input.kind || flow.kind === input.kind) &&
flow.stateHash === input.flowId
) {
return clone(flow);
}
}
return null;
},
async consumeFlow(input) {
const record = state.flows.get(input.flowId);
if (!record || record.tenantId !== input.tenantId) return;
state.flows.set(record.id, { ...record, consumedAt: input.nowMs });
},
async revokeFlows(input: AuthRevokeFlowsInput) {
for (const flow of state.flows.values()) {
const sameTenant = flow.tenantId === input.tenantId;
const sameActor = !input.actorRef || flow.actorRef?.actorId === input.actorRef.actorId;
const sameKind = !input.kind || flow.kind === input.kind;
if (sameTenant && sameActor && sameKind && !flow.consumedAt) {
state.flows.set(flow.id, { ...flow, consumedAt: input.nowMs });
}
}
}
};
}

@ -0,0 +1,77 @@
import { AUTH_INTERNAL_ERROR_MESSAGES } from '$libs/auth/consts';
import { AuthAdapterError } from '$libs/auth/errors';
import type {
AuthCreateRefreshFamilyInput,
AuthCreateRefreshTokenInput,
AuthFindRefreshTokenInput,
AuthRevokeRefreshFamilyInput,
AuthRotateRefreshTokenInput,
AuthRotateRefreshTokenResult,
AuthStoreAdapter
} from '$libs/auth/contracts';
import type { AuthRefreshFamilyRecord, AuthRefreshTokenRecord } from '$libs/auth/types';
import { clone, type MemoryAuthStoreState } from './memory-store-state';
export function createMemoryRefreshStore(
state: MemoryAuthStoreState
): Pick<
AuthStoreAdapter,
| 'createRefreshFamily'
| 'createRefreshToken'
| 'findRefreshTokenForUpdate'
| 'rotateRefreshToken'
| 'revokeRefreshFamily'
> {
return {
async createRefreshFamily(input: AuthCreateRefreshFamilyInput) {
const record: AuthRefreshFamilyRecord = { ...input };
state.refreshFamilies.set(record.id, record);
return clone(record);
},
async createRefreshToken(input: AuthCreateRefreshTokenInput) {
const record: AuthRefreshTokenRecord = { ...input };
state.refreshTokens.set(record.id, record);
return clone(record);
},
async findRefreshTokenForUpdate(input: AuthFindRefreshTokenInput) {
for (const record of state.refreshTokens.values()) {
if (record.tokenHash === input.tokenHash) return clone(record);
}
return null;
},
async rotateRefreshToken(
input: AuthRotateRefreshTokenInput
): Promise<AuthRotateRefreshTokenResult> {
const current = state.refreshTokens.get(input.currentTokenId);
if (!current) {
throw new AuthAdapterError(AUTH_INTERNAL_ERROR_MESSAGES.MEMORY_REFRESH_TOKEN_NOT_FOUND, {
currentTokenId: input.currentTokenId
});
}
const child: AuthRefreshTokenRecord = { ...input.childToken };
const updatedCurrent = { ...current, consumedAt: input.consumedAt, childTokenId: child.id };
state.refreshTokens.set(current.id, updatedCurrent);
state.refreshTokens.set(child.id, child);
return { current: clone(updatedCurrent), child: clone(child) };
},
async revokeRefreshFamily(input: AuthRevokeRefreshFamilyInput) {
const family = state.refreshFamilies.get(input.familyId);
if (family) {
state.refreshFamilies.set(family.id, {
...family,
revokedAt: input.nowMs,
revokeReason: input.reason
});
}
for (const token of state.refreshTokens.values()) {
if (token.familyId === input.familyId && !token.revokedAt) {
state.refreshTokens.set(token.id, { ...token, revokedAt: input.nowMs });
}
}
}
};
}

@ -0,0 +1,126 @@
import { AUTH_REVOKE_REASONS } from '$libs/auth/consts';
import type {
AuthBindSessionInput,
AuthListDevicesStoreInput,
AuthRevokeActorSessionsInput,
AuthRevokeDeviceStoreInput,
AuthStoreAdapter,
AuthUpsertDeviceInput
} from '$libs/auth/contracts';
import type {
AuthDeviceRecord,
AuthSessionBindingRecord,
AuthSessionId
} from '$libs/auth/types';
import { clone, cloneOrNull, type MemoryAuthStoreState } from './memory-store-state';
export function createMemorySessionStore(
state: MemoryAuthStoreState
): Pick<
AuthStoreAdapter,
| 'upsertDevice'
| 'listDevices'
| 'revokeDevice'
| 'bindSession'
| 'findSessionBinding'
| 'revokeSessionBinding'
| 'revokeActorSessions'
> {
return {
async upsertDevice(input: AuthUpsertDeviceInput) {
const existing = input.deviceId ? state.devices.get(input.deviceId) : undefined;
const record: AuthDeviceRecord =
existing && existing.actorRef.actorId === input.actorRef.actorId
? {
...existing,
displayName: input.displayName ?? existing.displayName,
userAgentDisplay: input.userAgentDisplay ?? existing.userAgentDisplay,
ipDisplayHint: input.ipDisplayHint ?? existing.ipDisplayHint,
lastSeenAt: input.nowMs
}
: {
id: input.deviceId!,
actorRef: input.actorRef,
displayName: input.displayName,
userAgentDisplay: input.userAgentDisplay,
ipDisplayHint: input.ipDisplayHint,
firstSeenAt: input.nowMs,
lastSeenAt: input.nowMs
};
state.devices.set(record.id, record);
return clone(record);
},
async listDevices(input: AuthListDevicesStoreInput) {
return [...state.devices.values()]
.filter(
(device) =>
device.actorRef.tenantId === input.actorRef.tenantId &&
device.actorRef.actorId === input.actorRef.actorId
)
.map(clone);
},
async revokeDevice(input: AuthRevokeDeviceStoreInput) {
const record = state.devices.get(input.deviceId);
if (
!record ||
record.actorRef.actorId !== input.actorRef.actorId ||
record.actorRef.tenantId !== input.actorRef.tenantId
) {
return [];
}
const revoked: AuthSessionId[] = [];
state.devices.set(record.id, { ...record, revokedAt: input.nowMs });
for (const binding of state.sessionBindings.values()) {
if (binding.deviceId === input.deviceId && !binding.revokedAt) {
state.sessionBindings.set(binding.sessSessionId, {
...binding,
revokedAt: input.nowMs,
revokeReason: AUTH_REVOKE_REASONS.DEVICE_REVOKED
});
revoked.push(binding.sessSessionId);
}
}
return revoked;
},
async bindSession(input: AuthBindSessionInput) {
const record: AuthSessionBindingRecord = { ...input };
state.sessionBindings.set(record.sessSessionId, record);
return clone(record);
},
async findSessionBinding(input) {
return cloneOrNull(state.sessionBindings.get(input.sessSessionId));
},
async revokeSessionBinding(input) {
const record = state.sessionBindings.get(input.sessSessionId);
if (!record) return;
state.sessionBindings.set(record.sessSessionId, {
...record,
revokedAt: input.nowMs,
revokeReason: input.reason
});
},
async revokeActorSessions(input: AuthRevokeActorSessionsInput) {
const revoked: AuthSessionId[] = [];
for (const binding of state.sessionBindings.values()) {
const sameActor =
binding.actorRef.tenantId === input.actorRef.tenantId &&
binding.actorRef.actorId === input.actorRef.actorId;
if (sameActor && !binding.revokedAt) {
state.sessionBindings.set(binding.sessSessionId, {
...binding,
revokedAt: input.nowMs,
revokeReason: input.reason
});
revoked.push(binding.sessSessionId);
}
}
return revoked;
}
};
}

@ -0,0 +1,70 @@
import type {
AuthCredentialRecord,
AuthDeviceRecord,
AuthFlowRecord,
AuthLinkedAccountRecord,
AuthPublicActor,
AuthRefreshFamilyRecord,
AuthRefreshTokenRecord,
AuthSessionBindingRecord
} from '$libs/auth/types';
export interface MemoryAuthSnapshot {
readonly credentials: readonly AuthCredentialRecord[];
readonly flows: readonly AuthFlowRecord[];
readonly linkedAccounts: readonly AuthLinkedAccountRecord[];
readonly devices: readonly AuthDeviceRecord[];
readonly sessionBindings: readonly AuthSessionBindingRecord[];
readonly actors: readonly AuthPublicActor[];
}
export interface MemoryAuthStoreState {
readonly credentials: Map<string, AuthCredentialRecord>;
readonly flows: Map<string, AuthFlowRecord>;
readonly linkedAccounts: Map<string, AuthLinkedAccountRecord>;
readonly devices: Map<string, AuthDeviceRecord>;
readonly sessionBindings: Map<string, AuthSessionBindingRecord>;
readonly refreshFamilies: Map<string, AuthRefreshFamilyRecord>;
readonly refreshTokens: Map<string, AuthRefreshTokenRecord>;
}
export function createMemoryAuthStoreState(): MemoryAuthStoreState {
return {
credentials: new Map(),
flows: new Map(),
linkedAccounts: new Map(),
devices: new Map(),
sessionBindings: new Map(),
refreshFamilies: new Map(),
refreshTokens: new Map()
};
}
export function resetMemoryAuthStoreState(state: MemoryAuthStoreState): void {
state.credentials.clear();
state.flows.clear();
state.linkedAccounts.clear();
state.devices.clear();
state.sessionBindings.clear();
state.refreshFamilies.clear();
state.refreshTokens.clear();
}
export function snapshotMemoryAuthStoreState(state: MemoryAuthStoreState): MemoryAuthSnapshot {
return {
credentials: [...state.credentials.values()].map(clone),
flows: [...state.flows.values()].map(clone),
linkedAccounts: [...state.linkedAccounts.values()].map(clone),
devices: [...state.devices.values()].map(clone),
sessionBindings: [...state.sessionBindings.values()].map(clone),
actors: []
};
}
export function clone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
export function cloneOrNull<T>(value: T | undefined): T | null {
return value ? clone(value) : null;
}

@ -1,50 +1,17 @@
import { AUTH_CREDENTIAL_KINDS, AUTH_INTERNAL_ERROR_MESSAGES, AUTH_REVOKE_REASONS } from '$libs/auth/consts';
import { AuthAdapterError } from '$libs/auth/errors';
import type {
AuthBindSessionInput,
AuthCreateFlowInput,
AuthCreatePasswordCredentialInput,
AuthCreateRefreshFamilyInput,
AuthCreateRefreshTokenInput,
AuthFindCredentialInput,
AuthFindFlowInput,
AuthFindLinkedAccountInput,
AuthFindRefreshTokenInput,
AuthFindSessionBindingInput,
AuthLinkAccountInput,
AuthListDevicesStoreInput,
AuthMarkCredentialVerifiedInput,
AuthRevokeActorSessionsInput,
AuthRevokeDeviceStoreInput,
AuthRevokeFlowsInput,
AuthRevokeRefreshFamilyInput,
AuthRevokeSessionBindingInput,
AuthRotateRefreshTokenInput,
AuthRotateRefreshTokenResult,
AuthStoreAdapter,
AuthUpdatePasswordCredentialInput,
AuthUpsertDeviceInput
} from '$libs/auth/contracts';
import type {
AuthCredentialRecord,
AuthDeviceRecord,
AuthFlowRecord,
AuthLinkedAccountRecord,
AuthPublicActor,
AuthRefreshFamilyRecord,
AuthRefreshTokenRecord,
AuthSessionBindingRecord,
AuthSessionId
} from '$libs/auth/types';
export interface MemoryAuthSnapshot {
readonly credentials: readonly AuthCredentialRecord[];
readonly flows: readonly AuthFlowRecord[];
readonly linkedAccounts: readonly AuthLinkedAccountRecord[];
readonly devices: readonly AuthDeviceRecord[];
readonly sessionBindings: readonly AuthSessionBindingRecord[];
readonly actors: readonly AuthPublicActor[];
}
import type { AuthStoreAdapter } from '$libs/auth/contracts';
import { createMemoryLinkedAccountStore } from './memory-store-accounts';
import { createMemoryCredentialStore } from './memory-store-credentials';
import { createMemoryFlowStore } from './memory-store-flows';
import { createMemoryRefreshStore } from './memory-store-refresh';
import { createMemorySessionStore } from './memory-store-sessions';
import {
createMemoryAuthStoreState,
resetMemoryAuthStoreState,
snapshotMemoryAuthStoreState,
type MemoryAuthSnapshot
} from './memory-store-state';
export type { MemoryAuthSnapshot } from './memory-store-state';
export interface MemoryAuthAdapter extends AuthStoreAdapter {
reset(): void;
@ -52,299 +19,21 @@ export interface MemoryAuthAdapter extends AuthStoreAdapter {
}
export function createMemoryAuthAdapter(): MemoryAuthAdapter {
const credentials = new Map<string, AuthCredentialRecord>();
const flows = new Map<string, AuthFlowRecord>();
const linkedAccounts = new Map<string, AuthLinkedAccountRecord>();
const devices = new Map<string, AuthDeviceRecord>();
const sessionBindings = new Map<string, AuthSessionBindingRecord>();
const refreshFamilies = new Map<string, AuthRefreshFamilyRecord>();
const refreshTokens = new Map<string, AuthRefreshTokenRecord>();
const state = createMemoryAuthStoreState();
return {
async findCredentialByIdentifier(input: AuthFindCredentialInput) {
for (const credential of credentials.values()) {
if (
credential.actorRef.tenantId === input.tenantId &&
credential.kind === input.kind &&
credential.identifierHash === input.identifierHash
)
return clone(credential);
}
return null;
},
async createPasswordCredential(input: AuthCreatePasswordCredentialInput) {
const record: AuthCredentialRecord = {
id: input.id,
actorRef: input.actorRef,
kind: AUTH_CREDENTIAL_KINDS.PASSWORD,
identifierHash: input.identifierHash,
identifierDisplay: input.identifierDisplay,
passwordHash: input.passwordHash,
createdAt: input.nowMs,
updatedAt: input.nowMs
};
credentials.set(record.id, record);
return clone(record);
},
async updatePasswordCredential(input: AuthUpdatePasswordCredentialInput) {
const record = credentials.get(input.credentialId);
if (!record || record.actorRef.tenantId !== input.tenantId) return;
credentials.set(record.id, {
...record,
passwordHash: input.passwordHash,
updatedAt: input.nowMs
});
},
async markCredentialVerified(input: AuthMarkCredentialVerifiedInput) {
const record = credentials.get(input.credentialId);
if (!record || record.actorRef.tenantId !== input.tenantId) return;
credentials.set(record.id, { ...record, verifiedAt: input.nowMs, updatedAt: input.nowMs });
},
async createFlow(input: AuthCreateFlowInput) {
const record: AuthFlowRecord = { ...input };
flows.set(record.id, record);
return clone(record);
},
async findFlowForUpdate(input: AuthFindFlowInput) {
const direct = flows.get(input.flowId);
if (
direct &&
direct.tenantId === input.tenantId &&
(!input.kind || direct.kind === input.kind)
)
return clone(direct);
for (const flow of flows.values()) {
if (
flow.tenantId === input.tenantId &&
(!input.kind || flow.kind === input.kind) &&
flow.stateHash === input.flowId
) {
return clone(flow);
}
}
return null;
},
async consumeFlow(input) {
const record = flows.get(input.flowId);
if (!record || record.tenantId !== input.tenantId) return;
flows.set(record.id, { ...record, consumedAt: input.nowMs });
},
async revokeFlows(input: AuthRevokeFlowsInput) {
for (const flow of flows.values()) {
const sameTenant = flow.tenantId === input.tenantId;
const sameActor = !input.actorRef || flow.actorRef?.actorId === input.actorRef.actorId;
const sameKind = !input.kind || flow.kind === input.kind;
if (sameTenant && sameActor && sameKind && !flow.consumedAt)
flows.set(flow.id, { ...flow, consumedAt: input.nowMs });
}
},
async findLinkedAccount(input: AuthFindLinkedAccountInput) {
for (const record of linkedAccounts.values()) {
if (
record.actorRef.tenantId === input.tenantId &&
record.providerId === input.providerId &&
record.providerSubject === input.providerSubject &&
!record.revokedAt
)
return clone(record);
}
return null;
},
async linkAccount(input: AuthLinkAccountInput) {
const record: AuthLinkedAccountRecord = {
id: input.id,
actorRef: input.actorRef,
providerId: input.providerId,
providerKind: input.providerKind,
providerSubject: input.providerSubject,
email: input.email,
emailVerified: input.emailVerified,
createdAt: input.nowMs,
updatedAt: input.nowMs
};
linkedAccounts.set(record.id, record);
return clone(record);
},
async upsertDevice(input: AuthUpsertDeviceInput) {
const existing = input.deviceId ? devices.get(input.deviceId) : undefined;
const record: AuthDeviceRecord =
existing && existing.actorRef.actorId === input.actorRef.actorId
? {
...existing,
displayName: input.displayName ?? existing.displayName,
userAgentDisplay: input.userAgentDisplay ?? existing.userAgentDisplay,
ipDisplayHint: input.ipDisplayHint ?? existing.ipDisplayHint,
lastSeenAt: input.nowMs
}
: {
id: input.deviceId!,
actorRef: input.actorRef,
displayName: input.displayName,
userAgentDisplay: input.userAgentDisplay,
ipDisplayHint: input.ipDisplayHint,
firstSeenAt: input.nowMs,
lastSeenAt: input.nowMs
};
devices.set(record.id, record);
return clone(record);
},
async listDevices(input: AuthListDevicesStoreInput) {
return [...devices.values()]
.filter(
(device) =>
device.actorRef.tenantId === input.actorRef.tenantId &&
device.actorRef.actorId === input.actorRef.actorId
)
.map(clone);
},
async revokeDevice(input: AuthRevokeDeviceStoreInput) {
const record = devices.get(input.deviceId);
if (
!record ||
record.actorRef.actorId !== input.actorRef.actorId ||
record.actorRef.tenantId !== input.actorRef.tenantId
)
return [];
const revoked: AuthSessionId[] = [];
devices.set(record.id, { ...record, revokedAt: input.nowMs });
for (const binding of sessionBindings.values()) {
if (binding.deviceId === input.deviceId && !binding.revokedAt) {
sessionBindings.set(binding.sessSessionId, {
...binding,
revokedAt: input.nowMs,
revokeReason: AUTH_REVOKE_REASONS.DEVICE_REVOKED
});
revoked.push(binding.sessSessionId);
}
}
return revoked;
},
async bindSession(input: AuthBindSessionInput) {
const record: AuthSessionBindingRecord = { ...input };
sessionBindings.set(record.sessSessionId, record);
return clone(record);
},
async findSessionBinding(input: AuthFindSessionBindingInput) {
return cloneOrNull(sessionBindings.get(input.sessSessionId));
},
async revokeSessionBinding(input: AuthRevokeSessionBindingInput) {
const record = sessionBindings.get(input.sessSessionId);
if (!record) return;
sessionBindings.set(record.sessSessionId, {
...record,
revokedAt: input.nowMs,
revokeReason: input.reason
});
},
async revokeActorSessions(input: AuthRevokeActorSessionsInput) {
const revoked: AuthSessionId[] = [];
for (const binding of sessionBindings.values()) {
const sameActor =
binding.actorRef.tenantId === input.actorRef.tenantId &&
binding.actorRef.actorId === input.actorRef.actorId;
if (sameActor && !binding.revokedAt) {
sessionBindings.set(binding.sessSessionId, {
...binding,
revokedAt: input.nowMs,
revokeReason: input.reason
});
revoked.push(binding.sessSessionId);
}
}
return revoked;
},
async createRefreshFamily(input: AuthCreateRefreshFamilyInput) {
const record: AuthRefreshFamilyRecord = { ...input };
refreshFamilies.set(record.id, record);
return clone(record);
},
async createRefreshToken(input: AuthCreateRefreshTokenInput) {
const record: AuthRefreshTokenRecord = { ...input };
refreshTokens.set(record.id, record);
return clone(record);
},
async findRefreshTokenForUpdate(input: AuthFindRefreshTokenInput) {
for (const record of refreshTokens.values()) {
if (record.tokenHash === input.tokenHash) return clone(record);
}
return null;
},
async rotateRefreshToken(
input: AuthRotateRefreshTokenInput
): Promise<AuthRotateRefreshTokenResult> {
const current = refreshTokens.get(input.currentTokenId);
if (!current)
throw new AuthAdapterError(AUTH_INTERNAL_ERROR_MESSAGES.MEMORY_REFRESH_TOKEN_NOT_FOUND, {
currentTokenId: input.currentTokenId
});
const child: AuthRefreshTokenRecord = { ...input.childToken };
const updatedCurrent = { ...current, consumedAt: input.consumedAt, childTokenId: child.id };
refreshTokens.set(current.id, updatedCurrent);
refreshTokens.set(child.id, child);
return { current: clone(updatedCurrent), child: clone(child) };
},
async revokeRefreshFamily(input: AuthRevokeRefreshFamilyInput) {
const family = refreshFamilies.get(input.familyId);
if (family)
refreshFamilies.set(family.id, {
...family,
revokedAt: input.nowMs,
revokeReason: input.reason
});
for (const token of refreshTokens.values()) {
if (token.familyId === input.familyId && !token.revokedAt)
refreshTokens.set(token.id, { ...token, revokedAt: input.nowMs });
}
},
...createMemoryCredentialStore(state),
...createMemoryFlowStore(state),
...createMemoryLinkedAccountStore(state),
...createMemorySessionStore(state),
...createMemoryRefreshStore(state),
reset() {
credentials.clear();
flows.clear();
linkedAccounts.clear();
devices.clear();
sessionBindings.clear();
refreshFamilies.clear();
refreshTokens.clear();
resetMemoryAuthStoreState(state);
},
snapshot() {
return {
credentials: [...credentials.values()].map(clone),
flows: [...flows.values()].map(clone),
linkedAccounts: [...linkedAccounts.values()].map(clone),
devices: [...devices.values()].map(clone),
sessionBindings: [...sessionBindings.values()].map(clone),
actors: []
};
return snapshotMemoryAuthStoreState(state);
}
};
}
function clone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
function cloneOrNull<T>(value: T | undefined): T | null {
return value ? clone(value) : null;
}

Loading…
Cancel
Save

Powered by TurnKey Linux.