@@ -51,28 +51,19 @@ pub struct User {
5151 /// Display name (1-200 characters)
5252 #[ prost( string, tag = "2" ) ]
5353 pub name : :: prost:: alloc:: string:: String ,
54- /// Argon2id PHC string format (empty for passkey-only users)
55- #[ prost( string, tag = "3" ) ]
56- pub password_hash : :: prost:: alloc:: string:: String ,
57- #[ prost( message, optional, tag = "4" ) ]
58- pub created_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
54+ /// References UserEmail.id
55+ #[ prost( int64, tag = "3" ) ]
56+ pub primary_email_id : i64 ,
57+ /// Lifecycle state
58+ #[ prost( enumeration = "UserStatus" , tag = "4" ) ]
59+ pub status : i32 ,
5960 #[ prost( message, optional, tag = "5" ) ]
60- pub updated_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
61- /// When Terms of Service was accepted
61+ pub created_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
6262 #[ prost( message, optional, tag = "6" ) ]
63- pub tos_accepted_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
64- /// Soft-delete timestamp (null if active)
65- #[ prost( message, optional, tag = "7" ) ]
66- pub deleted_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
67- /// Admin-disabled flag
68- #[ prost( bool , tag = "8" ) ]
69- pub disabled : bool ,
70- /// References UserEmail.id (single source of truth)
71- #[ prost( int64, tag = "9" ) ]
72- pub primary_email_id : i64 ,
63+ pub updated_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
7364}
7465/// User email address (stored in \_system namespace as Entity with key "user_email:{id}")
75- /// Each user can have multiple emails; primary is referenced by User.primary_email_id .
66+ /// Each user can have multiple emails; primary is tracked by UserEmail.primary field .
7667/// Constraint: Primary email cannot be deleted (must reassign primary first).
7768/// Global email uniqueness is enforced via index: "\_idx:email:{email}" → email_id
7869/// ID assigned by Ledger leader from sequence counter "\_meta:seq:user_email"
@@ -87,11 +78,17 @@ pub struct UserEmail {
8778 /// Normalized to lowercase (max 320 chars per RFC 5321)
8879 #[ prost( string, tag = "3" ) ]
8980 pub email : :: prost:: alloc:: string:: String ,
81+ /// Whether email has been verified
82+ #[ prost( bool , tag = "4" ) ]
83+ pub verified : bool ,
84+ /// Whether this is the user's primary email
85+ #[ prost( bool , tag = "5" ) ]
86+ pub primary : bool ,
87+ #[ prost( message, optional, tag = "6" ) ]
88+ pub created_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
9089 /// When verified (null if unverified)
91- #[ prost( message, optional, tag = "4 " ) ]
90+ #[ prost( message, optional, tag = "7 " ) ]
9291 pub verified_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
93- #[ prost( message, optional, tag = "5" ) ]
94- pub created_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
9592}
9693/// Email verification token (stored in \_system namespace with TTL)
9794/// Key: "email_verify:{id}", Index: "\_idx:email_verify:token:{token}" → token_id
@@ -149,6 +146,10 @@ pub struct BlockHeader {
149146 pub state_root : :: core:: option:: Option < Hash > ,
150147 #[ prost( message, optional, tag = "7" ) ]
151148 pub timestamp : :: core:: option:: Option < :: prost_types:: Timestamp > ,
149+ /// Note: leader_id is included in proto for API completeness but excluded from
150+ /// the 148-byte deterministic block hash computation. This ensures blocks hash
151+ /// identically regardless of which leader committed them. The leader_id is stored
152+ /// in ShardBlock and populated when extracting VaultBlock for client responses.
152153 #[ prost( message, optional, tag = "8" ) ]
153154 pub leader_id : :: core:: option:: Option < NodeId > ,
154155 #[ prost( uint64, tag = "9" ) ]
@@ -552,14 +553,24 @@ pub struct WatchBlocksRequest {
552553 pub vault_id : :: core:: option:: Option < VaultId > ,
553554 /// First block height to stream. Must be >= 1 (0 is rejected with INVALID_ARGUMENT).
554555 ///
555- /// Behavior:
556+ /// Streaming behavior:
557+ ///
558+ /// 1. Historical replay: Streams committed blocks from start_height to current tip
559+ /// 1. Real-time push: After historical replay, stream stays open and pushes new
560+ /// blocks as they are committed (zero-polling live synchronization)
561+ /// 1. Stream lifetime: Remains open indefinitely until client disconnects
556562 ///
557- /// * If start_height \<= current tip: replays committed blocks first, then streams new
558- /// * If start_height > current tip: waits for that block, then streams
563+ /// Backpressure handling:
559564 ///
560- /// Typical usage:
561- /// tip = GetTip()
562- /// stream = WatchBlocks(start_height = tip.height + 1)
565+ /// * Uses tokio::sync::broadcast internally with buffer size 1024
566+ /// * Slow consumers that fall >1024 blocks behind receive a Lagged error
567+ /// * On Lagged error, reconnect with start_height = last_received_height + 1
568+ ///
569+ /// Typical usage for live sync:
570+ /// stream = WatchBlocks(start_height = last_known_height + 1)
571+ /// for block in stream:
572+ /// process(block)
573+ /// last_known_height = block.height
563574 #[ prost( uint64, tag = "3" ) ]
564575 pub start_height : u64 ,
565576}
@@ -946,6 +957,7 @@ pub mod get_namespace_request {
946957 }
947958}
948959/// Namespace registry info (routing metadata from \_system)
960+ /// Note: leader_hint is computed dynamically from Raft state via GetSystemState
949961#[ derive( Clone , PartialEq , :: prost:: Message ) ]
950962pub struct GetNamespaceResponse {
951963 #[ prost( message, optional, tag = "1" ) ]
@@ -958,13 +970,14 @@ pub struct GetNamespaceResponse {
958970 /// Nodes in the shard
959971 #[ prost( message, repeated, tag = "4" ) ]
960972 pub member_nodes : :: prost:: alloc:: vec:: Vec < NodeId > ,
961- /// May be stale
962- #[ prost( message, optional, tag = "5" ) ]
963- pub leader_hint : :: core:: option:: Option < NodeId > ,
973+ /// Lifecycle state
974+ #[ prost( enumeration = "NamespaceStatus" , tag = "5" ) ]
975+ pub status : i32 ,
976+ /// For cache invalidation
964977 #[ prost( uint64, tag = "6" ) ]
965978 pub config_version : u64 ,
966- #[ prost( enumeration = "NamespaceStatus" , tag = "7" ) ]
967- pub status : i32 ,
979+ #[ prost( message , optional , tag = "7" ) ]
980+ pub created_at : :: core :: option :: Option < :: prost_types :: Timestamp > ,
968981}
969982#[ derive( Clone , PartialEq , Eq , Hash , :: prost:: Message ) ]
970983pub struct ListNamespacesRequest {
@@ -1353,34 +1366,38 @@ pub struct NodeInfo {
13531366 /// gRPC port (same for all addresses), typically 5000
13541367 #[ prost( uint32, tag = "3" ) ]
13551368 pub grpc_port : u32 ,
1356- #[ prost( message, optional, tag = "4" ) ]
1357- pub capabilities : :: core:: option:: Option < NodeCapabilities > ,
1369+ /// Voter or Learner
1370+ #[ prost( enumeration = "NodeRole" , tag = "4" ) ]
1371+ pub role : i32 ,
13581372 #[ prost( message, optional, tag = "5" ) ]
13591373 pub last_heartbeat : :: core:: option:: Option < :: prost_types:: Timestamp > ,
1360- }
1361- #[ derive( Clone , Copy , PartialEq , Eq , Hash , :: prost:: Message ) ]
1362- pub struct NodeCapabilities {
1363- #[ prost( bool , tag = "1" ) ]
1364- pub can_lead : bool ,
1365- #[ prost( uint64, tag = "2" ) ]
1366- pub max_vaults : u64 ,
1374+ /// For voter election ordering
1375+ #[ prost( message, optional, tag = "6" ) ]
1376+ pub joined_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
13671377}
13681378/// Routing entry: namespace → shard assignment
1379+ /// Note: leader_hint is computed dynamically from Raft state, not stored here
13691380#[ derive( Clone , PartialEq , :: prost:: Message ) ]
13701381pub struct NamespaceRegistry {
13711382 #[ prost( message, optional, tag = "1" ) ]
13721383 pub namespace_id : :: core:: option:: Option < NamespaceId > ,
1384+ /// Human-readable namespace name
1385+ #[ prost( string, tag = "2" ) ]
1386+ pub name : :: prost:: alloc:: string:: String ,
13731387 /// Which Raft group hosts this namespace
1374- #[ prost( message, optional, tag = "2 " ) ]
1388+ #[ prost( message, optional, tag = "3 " ) ]
13751389 pub shard_id : :: core:: option:: Option < ShardId > ,
13761390 /// Nodes in the shard
1377- #[ prost( message, repeated, tag = "3 " ) ]
1391+ #[ prost( message, repeated, tag = "4 " ) ]
13781392 pub members : :: prost:: alloc:: vec:: Vec < NodeId > ,
1379- /// May be stale
1380- #[ prost( message, optional, tag = "4" ) ]
1381- pub leader_hint : :: core:: option:: Option < NodeId > ,
1382- #[ prost( uint64, tag = "5" ) ]
1393+ /// Lifecycle state
1394+ #[ prost( enumeration = "NamespaceStatus" , tag = "5" ) ]
1395+ pub status : i32 ,
1396+ /// For cache invalidation
1397+ #[ prost( uint64, tag = "6" ) ]
13831398 pub config_version : u64 ,
1399+ #[ prost( message, optional, tag = "7" ) ]
1400+ pub created_at : :: core:: option:: Option < :: prost_types:: Timestamp > ,
13841401}
13851402/// Raft vote (term + node_id + committed flag).
13861403#[ derive( Clone , Copy , PartialEq , Eq , Hash , :: prost:: Message ) ]
@@ -1493,6 +1510,50 @@ pub struct RaftMembershipConfig {
14931510 #[ prost( map = "uint64, string" , tag = "1" ) ]
14941511 pub members : :: std:: collections:: HashMap < u64 , :: prost:: alloc:: string:: String > ,
14951512}
1513+ /// User account status (lifecycle state machine)
1514+ #[ derive( Clone , Copy , Debug , PartialEq , Eq , Hash , PartialOrd , Ord , :: prost:: Enumeration ) ]
1515+ #[ repr( i32 ) ]
1516+ pub enum UserStatus {
1517+ Unspecified = 0 ,
1518+ /// User can authenticate
1519+ Active = 1 ,
1520+ /// Pending organization creation (saga in progress)
1521+ PendingOrg = 2 ,
1522+ /// User cannot authenticate
1523+ Suspended = 3 ,
1524+ /// Deletion cascade in progress
1525+ Deleting = 4 ,
1526+ /// Tombstone for audit
1527+ Deleted = 5 ,
1528+ }
1529+ impl UserStatus {
1530+ /// String value of the enum field names used in the ProtoBuf definition.
1531+ ///
1532+ /// The values are not transformed in any way and thus are considered stable
1533+ /// (if the ProtoBuf definition does not change) and safe for programmatic use.
1534+ pub fn as_str_name ( & self ) -> & ' static str {
1535+ match self {
1536+ Self :: Unspecified => "USER_STATUS_UNSPECIFIED" ,
1537+ Self :: Active => "USER_STATUS_ACTIVE" ,
1538+ Self :: PendingOrg => "USER_STATUS_PENDING_ORG" ,
1539+ Self :: Suspended => "USER_STATUS_SUSPENDED" ,
1540+ Self :: Deleting => "USER_STATUS_DELETING" ,
1541+ Self :: Deleted => "USER_STATUS_DELETED" ,
1542+ }
1543+ }
1544+ /// Creates an enum from field names used in the ProtoBuf definition.
1545+ pub fn from_str_name ( value : & str ) -> :: core:: option:: Option < Self > {
1546+ match value {
1547+ "USER_STATUS_UNSPECIFIED" => Some ( Self :: Unspecified ) ,
1548+ "USER_STATUS_ACTIVE" => Some ( Self :: Active ) ,
1549+ "USER_STATUS_PENDING_ORG" => Some ( Self :: PendingOrg ) ,
1550+ "USER_STATUS_SUSPENDED" => Some ( Self :: Suspended ) ,
1551+ "USER_STATUS_DELETING" => Some ( Self :: Deleting ) ,
1552+ "USER_STATUS_DELETED" => Some ( Self :: Deleted ) ,
1553+ _ => None ,
1554+ }
1555+ }
1556+ }
14961557#[ derive( Clone , Copy , Debug , PartialEq , Eq , Hash , PartialOrd , Ord , :: prost:: Enumeration ) ]
14971558#[ repr( i32 ) ]
14981559pub enum Direction {
@@ -1643,8 +1704,16 @@ impl ReadErrorCode {
16431704#[ repr( i32 ) ]
16441705pub enum NamespaceStatus {
16451706 Unspecified = 0 ,
1707+ /// Accepting requests
16461708 Active = 1 ,
1647- Deleted = 2 ,
1709+ /// Being migrated to another shard
1710+ Migrating = 2 ,
1711+ /// Billing or policy suspension
1712+ Suspended = 3 ,
1713+ /// Deletion in progress
1714+ Deleting = 4 ,
1715+ /// Tombstone
1716+ Deleted = 5 ,
16481717}
16491718impl NamespaceStatus {
16501719 /// String value of the enum field names used in the ProtoBuf definition.
@@ -1655,6 +1724,9 @@ impl NamespaceStatus {
16551724 match self {
16561725 Self :: Unspecified => "NAMESPACE_STATUS_UNSPECIFIED" ,
16571726 Self :: Active => "NAMESPACE_STATUS_ACTIVE" ,
1727+ Self :: Migrating => "NAMESPACE_STATUS_MIGRATING" ,
1728+ Self :: Suspended => "NAMESPACE_STATUS_SUSPENDED" ,
1729+ Self :: Deleting => "NAMESPACE_STATUS_DELETING" ,
16581730 Self :: Deleted => "NAMESPACE_STATUS_DELETED" ,
16591731 }
16601732 }
@@ -1663,6 +1735,9 @@ impl NamespaceStatus {
16631735 match value {
16641736 "NAMESPACE_STATUS_UNSPECIFIED" => Some ( Self :: Unspecified ) ,
16651737 "NAMESPACE_STATUS_ACTIVE" => Some ( Self :: Active ) ,
1738+ "NAMESPACE_STATUS_MIGRATING" => Some ( Self :: Migrating ) ,
1739+ "NAMESPACE_STATUS_SUSPENDED" => Some ( Self :: Suspended ) ,
1740+ "NAMESPACE_STATUS_DELETING" => Some ( Self :: Deleting ) ,
16661741 "NAMESPACE_STATUS_DELETED" => Some ( Self :: Deleted ) ,
16671742 _ => None ,
16681743 }
@@ -1831,6 +1906,38 @@ impl HealthStatus {
18311906 }
18321907 }
18331908}
1909+ /// Node role in the cluster (Raft membership type)
1910+ #[ derive( Clone , Copy , Debug , PartialEq , Eq , Hash , PartialOrd , Ord , :: prost:: Enumeration ) ]
1911+ #[ repr( i32 ) ]
1912+ pub enum NodeRole {
1913+ Unspecified = 0 ,
1914+ /// Participates in Raft elections (max 5 per cluster)
1915+ Voter = 1 ,
1916+ /// Replicates data but doesn't vote (for scaling)
1917+ Learner = 2 ,
1918+ }
1919+ impl NodeRole {
1920+ /// String value of the enum field names used in the ProtoBuf definition.
1921+ ///
1922+ /// The values are not transformed in any way and thus are considered stable
1923+ /// (if the ProtoBuf definition does not change) and safe for programmatic use.
1924+ pub fn as_str_name ( & self ) -> & ' static str {
1925+ match self {
1926+ Self :: Unspecified => "NODE_ROLE_UNSPECIFIED" ,
1927+ Self :: Voter => "NODE_ROLE_VOTER" ,
1928+ Self :: Learner => "NODE_ROLE_LEARNER" ,
1929+ }
1930+ }
1931+ /// Creates an enum from field names used in the ProtoBuf definition.
1932+ pub fn from_str_name ( value : & str ) -> :: core:: option:: Option < Self > {
1933+ match value {
1934+ "NODE_ROLE_UNSPECIFIED" => Some ( Self :: Unspecified ) ,
1935+ "NODE_ROLE_VOTER" => Some ( Self :: Voter ) ,
1936+ "NODE_ROLE_LEARNER" => Some ( Self :: Learner ) ,
1937+ _ => None ,
1938+ }
1939+ }
1940+ }
18341941/// Generated client implementations.
18351942pub mod read_service_client {
18361943 #![ allow(
0 commit comments