Skip to content

Identity Domain (identity_v1)

The identity_v1 domain manages all aspects of user and organization identity in the CO2 target asset management system. It handles user registration, profile management, authentication integration, organization creation, estate collaboration invitations, and site-level access control with role-based permissions.

The Identity domain is the central hub for identity and access management across the entire system. It maintains user identity state, public profiles, organizational information, invitation workflows, and fine-grained access controls for sites, layers, and features.

  • User Identity Management: Registration, profile updates, and account lifecycle
  • Organization Identity: Creation and management of organizational entities
  • Public Profiles: Public-facing user profiles with email indexing for fast lookups
  • Estate Invitations: Workflow for inviting users to collaborate on estates
  • Site Access Control: Role-based access management at the site level
  • Layer & Feature Permissions: Fine-grained permissions for map layers and features
  • Notifications: User notification inbox for system events
dart_packages/co2/domains/identity_v1/
├── lib/
│ ├── identity_v1.dart # Main export file
│ └── src/
│ ├── aggregates/ # Domain aggregates
│ │ ├── user_aggregate.dart
│ │ ├── public_user_aggregate.dart
│ │ ├── site_user_aggregate.dart
│ │ ├── organization_aggregate.dart
│ │ ├── user_invitation_aggregate.dart
│ │ ├── notification_inbox_aggregate.dart
│ │ └── public_user_email_index_aggregate.dart
│ ├── events/ # Domain events
│ │ ├── identity_events.dart
│ │ ├── public_user_events.dart
│ │ ├── site_user_events.dart
│ │ ├── notification_events.dart
│ │ └── public_user_email_index_events.dart
│ ├── directives/ # Command handlers
│ │ ├── identity_directives.dart
│ │ ├── public_user_directives.dart
│ │ ├── site_user_directives.dart
│ │ ├── notification_directives.dart
│ │ └── public_user_email_index_directives.dart
│ ├── projectors/ # Read model projections
│ │ └── public_user_email_index_projector.dart
│ ├── selectors/ # Data query utilities
│ │ └── email_lookup_selectors.dart
│ └── utils/ # Utility functions
│ └── email_normalization.dart
├── pubspec.yaml
└── test/

The Identity domain must be registered before use. This registers all aggregates, events, directives, and read models:

import 'package:identity_v1/identity_v1.dart';
void main() {
// Register all identity domain types
registerIdentityV1();
// Or use the domain module wrapper
final identityModule = IdentityV1();
identityModule.registerDomainTypes();
}

Manages user identity, profile information, and account lifecycle. This is the primary user aggregate used for authentication and profile management.

import 'package:identity_v1/identity_v1.dart';
import 'package:contracts_v1/contracts_v1.dart';
// Access a user aggregate from the repository
final user = userRepository.getById(userId);
// Check user status and properties
if (user.isActive) {
print('User can access the system');
}
print('Display name: ${user.displayName}');
print('Email: ${user.email.value}');
print('Registered: ${user.registeredAt}');
// Check profile completeness
if (user.hasCompleteProfile) {
print('User profile is ready for use');
}
// Check activity
if (user.isRecentlyActive) {
print('User was active within the last 30 days');
}

Fields:

  • userId (UserId): Unique user identifier
  • email (EmailAddress): User’s email address
  • status (UserStatus): User account status (active, deactivated, inactive)
  • registeredAt (DateTime): Registration timestamp
  • updatedAt (DateTime): Last profile update timestamp
  • profile (UserProfile): Typed user profile containing name, picture, and custom fields

Business Logic Methods:

  • isActive, isDeactivated, isInactive: Status checks
  • displayName: Full name or email fallback
  • hasCompleteProfile: Profile validation
  • isRecentlyActive: Activity check (within 30 days)
  • canBeDeactivated(), canBeReactivated(): Lifecycle transitions
  • hasProfilePicture: Profile picture existence check

Validation Rules:

  • User ID cannot be unresolved
  • Email cannot be unresolved for active users
  • First and last name cannot be empty for active users
  • Email format must be valid (basic validation)
  • Status must be a valid UserStatus value

Manages public-facing user profiles with email uniqueness guarantees. Used for public visibility and user discovery.

import 'package:identity_v1/identity_v1.dart';
// Get a public user profile
final publicUser = publicUserRepository.getById(userId);
// Public user properties
print('Display name: ${publicUser.displayName}');
print('Email: ${publicUser.normalizedEmail}');
print('Photo: ${publicUser.hasPhoto}');
// Check profile completeness
if (publicUser.hasCompleteProfile) {
print('Public profile is fully populated');
}
// Activity tracking
if (publicUser.isRecentlyActive) {
print('User was active recently');
}

Fields:

  • userId (UserId): Unique user identifier
  • email (EmailAddress): User’s email (enforced lowercase for uniqueness)
  • createdAt (DateTime): Profile creation timestamp
  • updatedAt (DateTime): Last update timestamp
  • profile (UserProfile): Typed user profile containing name, picture, and custom fields

Business Logic Methods:

  • normalizedEmail: Lowercase, trimmed email for consistent lookups
  • hasCompleteProfile: Validation check
  • hasPhoto: Profile picture check
  • isRecentlyActive: Activity tracking (within 30 days)

Validation Rules:

  • User ID and email cannot be unresolved
  • Profile must have a name (firstName or lastName)
  • Email format must be valid
  • Email must be lowercase (business rule for consistency)

Manages user access to specific sites with role-based permissions and fine-grained layer/feature permissions.

import 'package:identity_v1/identity_v1.dart';
import 'package:contracts_v1/contracts_v1.dart';
// Get site access record
final siteUser = siteUserRepository.getById(aggregateId);
// Check access
if (siteUser.isActive) {
// User has active access to this site
// Check layer access
if (siteUser.hasLayerAccess(layerId)) {
print('User can access layer');
}
// Check feature access with minimum role
if (siteUser.hasFeatureAccess(featureId, Role.featureWrite)) {
print('User can write to feature');
}
}
// Get effective roles
final layerRole = siteUser.getEffectiveLayerRole(layerId);
final featureRole = siteUser.getEffectiveFeatureRole(featureId);
// List all permissions
for (final perm in siteUser.layerPermissions) {
print('Layer ${perm.layerId}: ${perm.role}');
}
for (final perm in siteUser.featurePermissions) {
print('Feature ${perm.featureId}: ${perm.role}');
}

Fields:

  • siteId (SiteId): Site identifier
  • userId (UserId): User identifier
  • siteRole (Role): User’s site-level role (siteAdmin, siteWrite, siteRead)
  • layerPermissions (List<LayerPermission>): Per-layer permission records
  • featurePermissions (List<FeaturePermission>): Per-feature permission records
  • grantedAt (DateTime): When access was granted
  • grantedBy (UserId): User who granted access
  • lastAccessedAt (DateTime?): Last access timestamp
  • isActive (bool): Whether access is currently active

Business Logic Methods:

  • hasLayerAccess(LayerId, [Role?]): Check layer access with optional minimum role
  • hasFeatureAccess(ResourceId, [Role?]): Check feature access
  • getEffectiveLayerRole(LayerId): Get role considering site admin status
  • getEffectiveFeatureRole(ResourceId): Get role considering site admin status
  • canGrantLayerPermission(LayerId, Role): Validate permission grant eligibility
  • canGrantFeaturePermission(ResourceId, Role): Validate permission grant eligibility

Permission Hierarchy:

  • Site admins have implicit access to all layers and features
  • Layer/feature permissions only apply when user is not a site admin
  • Inactive users cannot have explicit layer or feature permissions

Manages the lifecycle of estate collaboration invitations with role assignment and metadata.

import 'package:identity_v1/identity_v1.dart';
import 'package:contracts_v1/contracts_v1.dart';
// Get invitation
final invitation = invitationRepository.getById(invitationId);
// Check invitation status
if (invitation.canAccept) {
print('Invitation is pending and can be accepted');
}
if (invitation.isExpired) {
print('Invitation has expired');
}
// Get invitation details
print('Estate: ${invitation.estateId}');
print('Role: ${invitation.role}');
print('From: ${invitation.fullName}'); // title + department
print('Message: ${invitation.message.value}');
// Check permissions
if (invitation.hasSpecialPermissions) {
print('This invitation grants special write permissions');
}

Fields:

  • invitationId (InvitationId): Unique invitation identifier
  • estateId (EstateId): Target estate for collaboration
  • inviteeUserId (UserId): User being invited
  • invitedBy (UserId): User who sent the invitation
  • role (Role): Role to be assigned on acceptance
  • department (DomainText): Inviter’s department
  • title (DomainText): Inviter’s title/position
  • message (DomainText): Custom invitation message
  • status (InvitationStatus): Invitation status (pending, accepted, declined, expired)
  • createdAt (DateTime): Invitation creation time
  • updatedAt (DateTime): Last status update
  • respondedAt (DateTime?): When user responded
  • metadata (InvitationMetadata): Typed metadata including expiry
  • permissions (AccessPermissions): Invitation-specific access grants

Status Lifecycle:

  • pending: Initial state, awaiting user response
  • accepted: User accepted, should trigger access grant
  • declined: User rejected, invitation closed
  • expired: Invitation window closed without response

Business Logic Methods:

  • canAccept: Validation for acceptance
  • canDecline: Validation for decline
  • isExpired: Check expiry based on metadata
  • isResponded: Check if invitation has terminal status
  • fullName: Convenience getter (title + department)
  • hasSpecialPermissions: Check for write-level permissions

Validation Rules:

  • IDs cannot be unresolved
  • Role cannot be empty
  • Title cannot be empty
  • Status must be valid
  • Responded invitations must have respondedAt timestamp
  • Pending invitations should not have respondedAt

Manages organizational entity information and metadata.

import 'package:identity_v1/identity_v1.dart';
import 'package:contracts_v1/contracts_v1.dart';
final org = organizationRepository.getById(organizationId);
print('Name: ${org.name.value}');
print('Type: ${org.type.value}');
print('Status: ${org.status.value}');
print('Contact: ${org.primaryContactEmail?.value}');
print('Address: ${org.organizationAddress.street}');

Fields:

  • organizationId (OrganizationId): Unique organization identifier
  • name (OrganizationName): Organization name
  • type (OrganizationType): Type of organization (corporation, nonprofit, etc.)
  • status (OrganizationStatus): Current status (active, inactive, etc.)
  • description (OrganizationDescription?): Optional description
  • website (DomainText?): Organization website URL
  • primaryContactEmail (EmailAddress?): Primary contact email
  • primaryContactPhone (DomainText?): Primary contact phone
  • registrationNumber (DomainText?): Business registration number
  • taxId (DomainText?): Tax ID or VAT number
  • organizationAddress (OrganizationAddress): Typed address object
  • typedOrganizationData (OrganizationData): Extended organization metadata
  • createdBy (UserId): User who created the organization
  • createdAt (DateTime): Creation timestamp
  • updatedAt (DateTime): Last update timestamp

Manages user notifications and their read/unread status.

import 'package:identity_v1/identity_v1.dart';
final inbox = notificationRepository.getById(userId);
// Get notifications (sorted by creation date, newest first)
for (final notification in inbox.notifications) {
print('${notification.title}: ${notification.message}');
if (notification.readAt != null) {
print('Read at: ${notification.readAt}');
}
}
// Find unread count
final unreadCount = inbox.notifications
.where((n) => n.readAt == null)
.length;
print('Unread: $unreadCount');

Fields:

  • userId (UserId): Owner of the inbox
  • notifications (List<UserNotificationEntry>): List of notifications (sorted newest first)
  • createdAt (DateTime): Inbox creation time
  • updatedAt (DateTime): Last modification time

Validation:

  • User ID cannot be unresolved or empty

Maintains a denormalized index of email addresses to user IDs for efficient lookup of public users by email.

import 'package:identity_v1/identity_v1.dart';
// This aggregate is used internally for email lookups
// typically through the email_lookup_selectors
final userIds = emailLookupSelectors.findUsersByEmail('user@example.com');

Events represent important changes in the Identity domain. These are immutable records of what happened.

Fired when a new user account is created.

final event = UserRegisteredEvent(
userId: UserId('user-123'),
email: EmailAddress.fromString('user@example.com'),
firstName: FirstName.fromString('John'),
lastName: LastName.fromString('Doe'),
registeredAt: DateTime.now(),
);

Payload:

  • userId: New user’s ID
  • email: Registration email address
  • firstName: User’s first name
  • lastName: User’s last name
  • registeredAt: Registration timestamp

Fired when user profile information is updated.

final event = UserProfileUpdatedEvent(
userId: userId,
updatedProfile: UserProfile(
firstName: FirstName.fromString('Jane'),
profilePictureUrl: FileUrl.fromString('https://example.com/pic.jpg'),
),
updatedAt: DateTime.now(),
);

Payload:

  • userId: User being updated
  • updatedProfile: Typed profile changes (only changed fields needed)
  • updatedAt: Update timestamp

Fired when a user account is deactivated.

final event = UserDeactivatedEvent(
userId: userId,
reason: DomainText('Account requested deletion'),
deactivatedBy: adminUserId,
deactivatedAt: DateTime.now(),
);

Payload:

  • userId: User being deactivated
  • reason: Optional deactivation reason
  • deactivatedBy: Admin who performed the action
  • deactivatedAt: Deactivation timestamp

Fired when a new organization is created.

final event = OrganizationCreatedEvent(
organizationId: OrganizationId('org-123'),
name: OrganizationName.fromString('Acme Corp'),
type: OrganizationType.corporation,
description: OrganizationDescription.fromString('A company'),
website: DomainText('https://acme.example.com'),
primaryContactEmail: EmailAddress.fromString('contact@acme.example.com'),
organizationAddress: OrganizationAddress(
street: 'Main Street',
city: 'Springfield',
postalCode: '12345',
),
createdBy: userId,
createdAt: DateTime.now(),
typedOrganizationData: const OrganizationData(),
);

Fired when organization information changes.

Fired when organization status changes (active, inactive, etc.).

Fired when a public user profile is created.

final event = PublicUserCreatedEvent(
userId: userId,
email: email,
displayName: DomainText('John Doe'),
photoUrl: FileUrl.fromString('https://example.com/photo.jpg'),
createdAt: DateTime.now(),
userProfile: UserProfile(
firstName: FirstName.fromString('John'),
lastName: LastName.fromString('Doe'),
),
);

Fired when public profile information changes.

Fired when a user is invited to collaborate on an estate.

final event = UserInvitedToEstateEvent(
invitationId: invitationId,
estateId: estateId,
inviteeUserId: inviteeUserId,
invitedBy: currentUserId,
role: Role.estateWrite,
department: DomainText('Operations'),
title: DomainText('Estate Manager'),
message: DomainText('Please join our estate'),
createdAt: DateTime.now(),
typedInvitationMetadata: const InvitationMetadata(),
typedAccessPermissions: const AccessPermissions(),
);

Fired when a user accepts an invitation.

final event = UserInvitationAcceptedEvent(
invitationId: invitationId,
inviteeUserId: userId,
acceptedAt: DateTime.now(),
);

Fired when a user declines an invitation.

final event = UserInvitationDeclinedEvent(
invitationId: invitationId,
inviteeUserId: userId,
declinedAt: DateTime.now(),
reason: DomainText('Not interested'),
);

Fired when a user is granted access to a site.

final event = SiteUserAccessGrantedEvent(
siteId: siteId,
userId: userId,
role: Role.siteWrite,
grantedBy: adminUserId,
grantedAt: DateTime.now(),
);

Fired when a user’s site role is changed.

Fired when a user’s site access is revoked.

Fired when a user is granted permission to access a specific layer.

final event = LayerPermissionGrantedEvent(
siteId: siteId,
userId: userId,
layerId: layerId,
role: Role.layerWrite,
grantedBy: adminUserId,
grantedAt: DateTime.now(),
);

Fired when layer permission is revoked.

Fired when a user is granted permission to use a feature.

Fired when feature permission is revoked.

Fired when a notification is added to a user’s inbox.

Fired when a user marks a notification as read.

Directives are commands that drive the domain, causing aggregates to apply events.

import 'package:identity_v1/identity_v1.dart';
import 'package:contracts_v1/contracts_v1.dart';
// Create a registration directive
final directive = RegisterUserDirective(
payload: RegisterUserPayload(
userId: UserId('user-123'),
email: EmailAddress.fromString('user@example.com'),
firstName: FirstName.fromString('John'),
lastName: LastName.fromString('Doe'),
),
);
// Execute the directive (framework handles execution)
// This will create a UserRegisteredEvent and update the UserAggregate
// Update user profile
final directive = UpdateUserProfileDirective(
payload: UpdateUserProfilePayload(
userId: userId,
updatedProfile: UserProfile(
firstName: FirstName.fromString('Jane'),
profilePictureUrl: FileUrl.fromString('https://example.com/pic.jpg'),
),
),
);
// Deactivate user account
final deactivateDirective = DeactivateUserDirective(
payload: DeactivateUserPayload(
userId: userId,
reason: DomainText('Account deletion requested'),
deactivatedBy: adminUserId,
),
);
final directive = CreateOrganizationDirective(
payload: CreateOrganizationPayload(
organizationId: OrganizationId('org-123'),
name: OrganizationName.fromString('Acme Corp'),
type: OrganizationType.corporation,
// ... other fields
),
);
// Accept an invitation
final acceptDirective = AcceptEstateInvitationDirective(
payload: AcceptEstateInvitationPayload(
invitationId: invitationId,
acceptedBy: userId,
),
);
// Decline an invitation
final declineDirective = DeclineEstateInvitationDirective(
payload: DeclineEstateInvitationPayload(
invitationId: invitationId,
declinedBy: userId,
reason: DomainText('Not interested'),
),
);
// Grant site access
final grantDirective = GrantSiteAccessDirective(
id: DirectiveId('directive-1'),
payload: JsonValue({'siteId': siteId.value, 'userId': userId.value, ...}),
);
// Change site user role
final changeRoleDirective = ChangeSiteUserRoleDirective(
id: DirectiveId('directive-2'),
payload: JsonValue({'siteId': siteId.value, 'userId': userId.value, 'newRole': ...}),
);
// Revoke site access
final revokeDirective = RevokeSiteAccessDirective(
id: DirectiveId('directive-3'),
payload: JsonValue({'siteId': siteId.value, 'userId': userId.value}),
);
// Grant layer permission
final grantLayerDirective = GrantLayerPermissionDirective(
id: DirectiveId('directive-4'),
payload: JsonValue({'siteId': siteId.value, 'userId': userId.value, 'layerId': layerId.value, ...}),
);
// Grant feature permission
final grantFeatureDirective = GrantFeaturePermissionDirective(
id: DirectiveId('directive-5'),
payload: JsonValue({'siteId': siteId.value, 'userId': userId.value, 'featureId': featureId.value, ...}),
);

The Identity domain provides the foundation for the Permissions domain, which enforces fine-grained access control throughout the system.

  1. User Registration: UserRegisteredEvent creates the initial user identity
  2. Site Access: SiteUserAccessGrantedEvent establishes site-level membership
  3. Role Assignment: SiteUserRoleChangedEvent updates permission level
  4. Layer Permissions: LayerPermissionGrantedEvent restricts access to specific map layers
  5. Feature Permissions: FeaturePermissionGrantedEvent controls feature-specific access
System Admin
Site Admin (implies all layer and feature access)
├─ Layer Admin (for specific layers)
│ └─ Layer Write
│ └─ Layer Read
├─ Feature Admin
│ └─ Feature Write
│ └─ Feature Read
└─ [Estate/Site level roles]
// Check if user can view a specific layer
bool canViewLayer(SiteUserAggregate access, LayerId layerId) {
return access.hasLayerAccess(layerId, Role.layerRead);
}
// Check if user can edit a feature
bool canEditFeature(SiteUserAggregate access, ResourceId featureId) {
return access.hasFeatureAccess(featureId, Role.featureWrite);
}
// Site admins get implicit access to everything
bool isSiteAdmin(SiteUserAggregate access) {
return access.siteRole.hasMinimumPermission(Role.siteAdmin);
}

The domain provides email normalization utilities for consistent handling:

import 'package:identity_v1/identity_v1.dart';
// Normalize email for comparison
final normalized = EmailNormalization.normalize('User@Example.COM');
// Result: 'user@example.com'
// Check email equivalence
if (EmailNormalization.areEquivalent(email1, email2)) {
print('Emails are the same');
}
// Validate email format
if (EmailNormalization.isValidEmail('test@example.com')) {
print('Email is valid');
}

The domain maintains a denormalized email index for fast public user lookups:

import 'package:identity_v1/identity_v1.dart';
// Find public users by email (returns UserId list)
final userIds = emailLookupSelectors.findUsersByEmail('user@example.com');

Public user profiles are separate from internal user accounts and serve the public-facing aspects of the system.

import 'package:identity_v1/identity_v1.dart';
// Create a public user profile
final directive = CreatePublicUserDirective(
payload: CreatePublicUserPayload(
userId: userId,
email: email,
displayName: DomainText('John Doe'),
photoUrl: FileUrl.fromString('https://example.com/photo.jpg'),
),
);
// Update public profile
final updateDirective = UpdatePublicUserProfileDirective(
payload: UpdatePublicUserProfilePayload(
userId: userId,
updatedProfile: UserProfile(
firstName: FirstName.fromString('Jane'),
profilePictureUrl: FileUrl.fromString('https://example.com/new-photo.jpg'),
),
),
);

The domain provides user notification inbox management:

import 'package:identity_v1/identity_v1.dart';
// Create a notification
final directive = CreateNotificationDirective(
payload: CreateNotificationPayload(
userId: userId,
title: DomainText('New collaboration invite'),
message: DomainText('You have been invited to Estate ABC'),
relatedResourceId: estateId.value,
relatedResourceType: 'Estate',
),
);
// Mark notification as read
final readDirective = MarkNotificationReadDirective(
payload: MarkNotificationReadPayload(
userId: userId,
notificationId: NotificationId('notif-123'),
),
);
// 1. Register new user (creates UserAggregate)
final registerDirective = RegisterUserDirective(
payload: RegisterUserPayload(
userId: UserId('user-123'),
email: EmailAddress.fromString('user@example.com'),
firstName: FirstName.fromString('John'),
lastName: LastName.fromString('Doe'),
),
);
// 2. Create public profile (creates PublicUserAggregate)
final publicDirective = CreatePublicUserDirective(
payload: CreatePublicUserPayload(
userId: userId,
email: email,
displayName: DomainText('John Doe'),
),
);
// 3. Add email to index (creates PublicUserEmailIndexAggregate entry)
final indexDirective = AddPublicUserEmailIndexEntryDirective(
payload: AddPublicUserEmailIndexEntryPayload(
email: email,
userId: userId,
),
);
// 1. Invite user to estate (creates UserInvitationAggregate)
final inviteEvent = UserInvitedToEstateEvent(
invitationId: invitationId,
estateId: estateId,
inviteeUserId: inviteeUserId,
invitedBy: currentUserId,
role: Role.estateWrite,
department: DomainText('Operations'),
title: DomainText('Estate Manager'),
message: DomainText('Please join our estate'),
createdAt: DateTime.now(),
);
// 2a. User accepts (creates SiteUserAccessGrantedEvent)
final acceptDirective = AcceptEstateInvitationDirective(
payload: AcceptEstateInvitationPayload(
invitationId: invitationId,
acceptedBy: userId,
),
);
// 2b. System grants site access (triggered by invitation acceptance)
final grantAccessEvent = SiteUserAccessGrantedEvent(
siteId: siteId,
userId: userId,
role: Role.estateWrite,
grantedBy: UserId.system,
grantedAt: DateTime.now(),
);
// 1. Grant layer permission
final grantDirective = GrantLayerPermissionDirective(
id: DirectiveId('directive-1'),
payload: JsonValue({
'siteId': siteId.value,
'userId': userId.value,
'layerId': layerId.value,
'role': {'type': 'layer_write'},
}),
);
// 2. Later, change the permission
final changeDirective = ChangeSiteUserRoleDirective(
id: DirectiveId('directive-2'),
payload: JsonValue({
'siteId': siteId.value,
'userId': userId.value,
'newRole': {'type': 'site_admin'},
}),
);
// 3. Eventually, revoke access
final revokeDirective = RevokeSiteAccessDirective(
id: DirectiveId('directive-3'),
payload: JsonValue({
'siteId': siteId.value,
'userId': userId.value,
}),
);

The Identity domain uses strongly-typed value objects from contracts_v1:

  • UserId, EstateId, SiteId: Entity identifiers
  • EmailAddress, FirstName, LastName: Named value objects
  • Role: Permission levels across the system
  • UserProfile, OrganizationAddress: Structured data
  • Various enumeration types: UserStatus, OrganizationStatus, etc.
  • Permissions Domain: Enforces fine-grained access control based on Identity aggregates
  • Policy Domain: Manages access policies and security rules
  • Estate/Site Domains: User access is defined in Identity domain, estate structures manage physical hierarchy
  • Intent Domain: User-initiated commands flow through directives registered in Identity domain

Some fields in aggregates have been deprecated in favor of more strongly-typed alternatives:

// Deprecated: Use profile.firstName instead
@Deprecated('Use profile.firstName instead')
String get firstName => profile.firstName?.value ?? '';
// Deprecated: Use profile instead for type-safe access
@Deprecated('Use profile instead for type-safe access')
Map<String, dynamic> get profileData => profile.toJson();

Always use the typed profile field and its properties (firstName, lastName, profilePictureUrl) instead of accessing the untyped profileData map.

The Identity domain includes comprehensive test coverage:

test/
├── user_aggregate_test.dart
├── public_user_aggregate_test.dart
├── site_user_aggregate_test.dart
├── user_invitation_aggregate_test.dart
├── notification_inbox_aggregate_test.dart
├── identity_directives_test.dart
├── public_user_directives_test.dart
├── site_user_directives_test.dart
├── notification_directives_test.dart
├── identity_v1_test.dart
└── ...

Test examples:

test('UserAggregate can be registered and created', () {
registerIdentityV1();
final event = UserRegisteredEvent(
userId: UserId('user-123'),
email: EmailAddress.fromString('user@example.com'),
firstName: FirstName.fromString('John'),
lastName: LastName.fromString('Doe'),
registeredAt: DateTime.now(),
);
final user = UserAggregate.empty().apply(event);
expect(user.isActive, true);
expect(user.email.value, 'user@example.com');
expect(user.displayName, 'John Doe');
});
  1. Always use typed value objects: Never pass raw strings for IDs or structured data
  2. Validate on aggregate creation: Use aggregate validate() method to enforce business rules
  3. Normalize emails: Use EmailNormalization for consistent email handling
  4. Check status before operations: Verify aggregate status before attempting state transitions
  5. Use unresolved values: For optional IDs, use the unresolved constants rather than null
  6. Profile management: Always update the strongly-typed profile field, not deprecated profileData
  7. Permission checks: Use hasLayerAccess() and hasFeatureAccess() with proper role checks
  8. Invitation lifecycle: Respect invitation status transitions (pending → accepted/declined)
dependencies:
nomos_core: ^1.0.0
nomos_persistence_memory: ^1.0.0
contracts_v1: ^1.0.0
intents_v1: ^1.0.0
policy_v1: ^1.0.0
  • Package: identity_v1
  • Version: 1.0.0
  • Dart SDK: >=3.3.0 <4.0.0