Skip to main content
Version: v0.13

Events System

The IDK provides a core event system for broadcasting and subscribing to events across the application. Events are used for command lifecycle tracking, audit logging, cross-component communication, and integration with external systems.

Modules

ModuleDescription
lib-core-events-publicEvent interfaces, types, and filtering
lib-core-events-implDefault implementations

Core Concepts

Event

Events are immutable records of something that happened in the system:

interface Event {
val id: Uuid // Unique event identifier
val type: EventType // Event type (extensible)
val origin: String // Command/service that emitted the event
val timestamp: Instant // When the event occurred
val context: EventContext // Session, tenant, principal info
val subsystem: EventSubsystem // Which subsystem emitted the event
val category: EventCategory // Event category for filtering
val payload: JsonObject // Event-specific data
val signature: EventSignature? // Optional cryptographic signature
val encryption: EventEncryption? // Optional encryption
}

EventHub

The central hub for event broadcasting and subscription. It's an AppScope singleton that serves as the distribution point for all events:

import com.sphereon.core.events.EventHub
import com.sphereon.core.events.EventTypes
import kotlinx.coroutines.launch

// Get the EventHub from your DI component
val eventHub: EventHub = appComponent.eventHub

// Collect all events
launch {
eventHub.events.collect { event ->
when (event.type) {
EventTypes.COMMAND_COMPLETED -> {
log.info("Command completed: ${event.origin}")
}
EventTypes.COMMAND_FAILED -> {
log.error("Command failed: ${event.origin}")
}
}
}
}

EventService

Scoped services for emitting events at different levels:

ServiceScopeDescription
AppEventServiceAppScopeApplication-level events
UserEventServiceUserScopePer-tenant/principal events
SessionEventServiceSessionScopePer-session events

Event Types

Built-in event types for common scenarios:

TypeDescription
COMMAND_STARTEDCommand execution began
COMMAND_COMPLETEDCommand completed successfully
COMMAND_FAILEDCommand execution failed
STATE_CHANGEDState machine transition
CUSTOMApplication-defined events

Defining Custom Event Types

import com.sphereon.core.events.EventType

// Create custom event types as extensions
object MyEventTypes {
val USER_LOGGED_IN = EventType("user.logged_in")
val CREDENTIAL_PRESENTED = EventType("credential.presented")
val VERIFICATION_COMPLETED = EventType("verification.completed")
}

Subsystems

Events are categorized by subsystem for filtering:

SubsystemDescription
CORECore IDK operations
CRYPTOCryptographic operations
KMSKey management operations
MDOCMobile credential operations
OID4VPOpenID4VP operations
OAUTH2OAuth 2.0 operations

Event Filtering

The EventFilter allows selective subscription to events:

import com.sphereon.core.events.EventFilter
import com.sphereon.core.events.EventSubsystems
import com.sphereon.core.events.EventCategories

// Build a filter
val filter = EventFilter {
// Filter by subsystems
subsystems(EventSubsystems.CRYPTO, EventSubsystems.KMS)

// Filter by categories
categories(EventCategories.ERROR, EventCategories.WARNING)

// Filter by event type pattern (glob-style)
typePatterns("command.*", "kms.key.*")

// Filter by origin
origins("SignDocumentCommand", "KeyManagerService")

// Filter by context
tenantIds("tenant-123")
principalIds("user@example.com")
}

// Use the filter with subscription
val job = eventHub.subscribe(scope, filter) { event ->
alertService.notify("Issue in ${event.subsystem}: ${event.payload}")
}

Subscription DSL

Subscribe to events using a builder DSL:

val job = eventHub.subscribe(scope) {
filter {
subsystems(EventSubsystems.MDOC)
categories(EventCategories.INFO, EventCategories.SUCCESS)
}
onEvent { event ->
when (event.type) {
EventTypes.COMMAND_COMPLETED -> {
analytics.track("mdoc_operation", event.payload)
}
}
}
}

// Cancel subscription when done
job.cancel()

Flow-Based API

For integration with Kotlin Flow operators:

import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map

// Get filtered flow
val errorFlow = eventHub.filteredEvents(
EventFilter { categories(EventCategories.ERROR) }
)

// Use Flow operators
errorFlow
.filter { it.subsystem == EventSubsystems.KMS }
.map { it.payload }
.collect { payload ->
errorReporter.report(payload)
}

// Convenience methods
eventHub.eventsBySubsystem(EventSubsystems.CRYPTO).collect { ... }
eventHub.eventsByCategory(EventCategories.ERROR).collect { ... }
eventHub.eventsByTypePattern("command.*").collect { ... }

Event Storage

The EventStore interface provides persistent event storage:

interface EventStore {
suspend fun store(event: Event)
suspend fun query(filter: EventFilter, limit: Int = 100): List<Event>
suspend fun getById(id: Uuid): Event?
suspend fun count(filter: EventFilter): Long
}

RingBufferEventStore

The default in-memory implementation with configurable capacity:

val eventStore = RingBufferEventStore(
capacity = 10000, // Keep last 10,000 events
scope = applicationScope
)

// Query recent events
val recentErrors = eventStore.query(
EventFilter { categories(EventCategories.ERROR) },
limit = 50
)

Event Signing

Events can be cryptographically signed for authenticity:

interface EventSigningService {
suspend fun sign(event: Event): Event
suspend fun verify(event: Event): Boolean
}

// Signed events include signature metadata
data class EventSignature(
val keyAlias: String, // Key used for signing
val providerId: String, // KMS provider
val algorithm: String, // e.g., "ES256"
val jws: String // JWS signature
)

Event Encryption

Events can be encrypted for confidentiality:

interface EventEncryptionService {
suspend fun encrypt(event: Event, parts: Set<EncryptedPart>): Event
suspend fun decrypt(event: Event): Event
}

enum class EncryptedPart {
PAYLOAD, // Encrypt event data
CONTEXT // Encrypt session/tenant info
}

Command Event Integration

Commands automatically emit lifecycle events when configured:

// Configure command to emit events
val commandConfig = CommandEventConfig(
emitStarted = true,
emitCompleted = true,
emitFailed = true,
includeArgsInPayload = false, // Privacy control
includeResultInPayload = true
)

// Or use the @SilentCommand annotation to disable events
@SilentCommand
class InternalHelperCommand : ICommand<Unit, Unit> { ... }

Best Practices

Use subsystem filtering. Subscribe only to relevant subsystems to minimize processing overhead.

Handle events asynchronously. Event handlers should not block. Use coroutines for I/O operations.

Consider event signing for audit trails. Sign events that may be used for compliance or legal purposes.

Use the ring buffer store for debugging. Keep recent events in memory for troubleshooting.

Don't include sensitive data in payloads. Or use encryption for events containing PII.

// Good: Include only necessary metadata
val payload = buildJsonObject {
put("documentId", documentId)
put("signatureType", "PAdES")
put("success", true)
}

// Avoid: Including sensitive document content
val payload = buildJsonObject {
put("documentContent", Base64.encode(sensitiveDocument)) // Don't do this
}