@ -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 ;
}