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
| Module | Description |
|---|---|
lib-core-events-public | Event interfaces, types, and filtering |
lib-core-events-impl | Default 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:
- Android/Kotlin
- iOS/Swift
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}")
}
}
}
}
import SphereonIDK
// Get the EventHub from your DI component
let eventHub = appComponent.eventHub
// Collect all events
Task {
for await event in eventHub.events {
switch event.type {
case EventTypes.commandCompleted:
print("Command completed: \(event.origin)")
case EventTypes.commandFailed:
print("Command failed: \(event.origin)")
default:
break
}
}
}
EventService
Scoped services for emitting events at different levels:
| Service | Scope | Description |
|---|---|---|
AppEventService | AppScope | Application-level events |
UserEventService | UserScope | Per-tenant/principal events |
SessionEventService | SessionScope | Per-session events |
Event Types
Built-in event types for common scenarios:
| Type | Description |
|---|---|
COMMAND_STARTED | Command execution began |
COMMAND_COMPLETED | Command completed successfully |
COMMAND_FAILED | Command execution failed |
STATE_CHANGED | State machine transition |
CUSTOM | Application-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:
| Subsystem | Description |
|---|---|
CORE | Core IDK operations |
CRYPTO | Cryptographic operations |
KMS | Key management operations |
MDOC | Mobile credential operations |
OID4VP | OpenID4VP operations |
OAUTH2 | OAuth 2.0 operations |
Event Filtering
The EventFilter allows selective subscription to events:
- Android/Kotlin
- iOS/Swift
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}")
}
import SphereonIDK
// Build a filter
let filter = EventFilter { builder in
builder.subsystems([.crypto, .kms])
builder.categories([.error, .warning])
builder.typePatterns(["command.*", "kms.key.*"])
}
// Use the filter with subscription
let job = eventHub.subscribe(scope: scope, filter: filter) { event in
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
}