diff --git a/go/compliance/client/v1/client.pb.go b/go/compliance/client/v1/client.pb.go index b4af7bf1..bce03c56 100644 --- a/go/compliance/client/v1/client.pb.go +++ b/go/compliance/client/v1/client.pb.go @@ -38,10 +38,13 @@ type Client struct { // The executing user needs to have permission to perform client.Create in this group. // Required on creation. Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + // Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + // System set on creation. + Owners []string `protobuf:"bytes,3,rep,name=owners,proto3" json:"owners,omitempty"` // A non-unique, user-provided name for the client, used for display purposes // in user interfaces and reports. // Required on creation. - DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // Contains the specific data for the legal entity type. // Only one of these may be set at a time. // @@ -54,19 +57,19 @@ type Client struct { LegalPerson isClient_LegalPerson `protobuf_oneof:"legal_person"` // The definitive, most recent compliance status of the client (e.g., VERIFICATION_STATUS_VERIFIED, VERIFICATION_STATUS_FAILED). // Must always be a valid field - VerificationStatus VerificationStatus `protobuf:"varint,8,opt,name=verification_status,json=verificationStatus,proto3,enum=meshtrade.compliance.client.v1.VerificationStatus" json:"verification_status,omitempty"` + VerificationStatus VerificationStatus `protobuf:"varint,9,opt,name=verification_status,json=verificationStatus,proto3,enum=meshtrade.compliance.client.v1.VerificationStatus" json:"verification_status,omitempty"` // The resource name of the client (acting as a verifier) that last set the // `verification_status`. This provides an audit trail for status changes. // System set when verification_status changes. - VerificationAuthority string `protobuf:"bytes,9,opt,name=verification_authority,json=verificationAuthority,proto3" json:"verification_authority,omitempty"` + VerificationAuthority string `protobuf:"bytes,10,opt,name=verification_authority,json=verificationAuthority,proto3" json:"verification_authority,omitempty"` // The timestamp when the `verification_status` was last set to a conclusive // state, specifically `VERIFICATION_STATUS_VERIFIED`. // System set when verification_status changes to VERIFICATION_STATUS_VERIFIED. - VerificationDate *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=verification_date,json=verificationDate,proto3" json:"verification_date,omitempty"` + VerificationDate *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=verification_date,json=verificationDate,proto3" json:"verification_date,omitempty"` // The timestamp indicating when the client's next periodic compliance review // is due. This field drives re-verification workflows. // Optional for Verification. - NextVerificationDate *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=next_verification_date,json=nextVerificationDate,proto3" json:"next_verification_date,omitempty"` + NextVerificationDate *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=next_verification_date,json=nextVerificationDate,proto3" json:"next_verification_date,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -115,6 +118,13 @@ func (x *Client) GetOwner() string { return "" } +func (x *Client) GetOwners() []string { + if x != nil { + return x.Owners + } + return nil +} + func (x *Client) GetDisplayName() string { if x != nil { return x.DisplayName @@ -199,22 +209,22 @@ type isClient_LegalPerson interface { type Client_NaturalPerson struct { // Set when the legal entity is an individual human being. - NaturalPerson *NaturalPerson `protobuf:"bytes,4,opt,name=natural_person,json=naturalPerson,proto3,oneof"` + NaturalPerson *NaturalPerson `protobuf:"bytes,5,opt,name=natural_person,json=naturalPerson,proto3,oneof"` } type Client_Company struct { // Set when the legal entity is a company or corporation. - Company *Company `protobuf:"bytes,5,opt,name=company,proto3,oneof"` + Company *Company `protobuf:"bytes,6,opt,name=company,proto3,oneof"` } type Client_Fund struct { // Set when the legal entity is an investment fund. - Fund *Fund `protobuf:"bytes,6,opt,name=fund,proto3,oneof"` + Fund *Fund `protobuf:"bytes,7,opt,name=fund,proto3,oneof"` } type Client_Trust struct { // Set when the legal entity is a trust. - Trust *Trust `protobuf:"bytes,7,opt,name=trust,proto3,oneof"` + Trust *Trust `protobuf:"bytes,8,opt,name=trust,proto3,oneof"` } func (*Client_NaturalPerson) isClient_LegalPerson() {} @@ -229,23 +239,24 @@ var File_meshtrade_compliance_client_v1_client_proto protoreflect.FileDescriptor const file_meshtrade_compliance_client_v1_client_proto_rawDesc = "" + "\n" + - "+meshtrade/compliance/client/v1/client.proto\x12\x1emeshtrade.compliance.client.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a,meshtrade/compliance/client/v1/company.proto\x1a)meshtrade/compliance/client/v1/fund.proto\x1a3meshtrade/compliance/client/v1/natural_person.proto\x1a*meshtrade/compliance/client/v1/trust.proto\x1a8meshtrade/compliance/client/v1/verification_status.proto\"\x8c\t\n" + + "+meshtrade/compliance/client/v1/client.proto\x12\x1emeshtrade.compliance.client.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a,meshtrade/compliance/client/v1/company.proto\x1a)meshtrade/compliance/client/v1/fund.proto\x1a3meshtrade/compliance/client/v1/natural_person.proto\x1a*meshtrade/compliance/client/v1/trust.proto\x1a8meshtrade/compliance/client/v1/verification_status.proto\"\xe4\t\n" + "\x06Client\x12\xbe\x01\n" + "\x04name\x18\x01 \x01(\tB\xa9\x01\xbaH\xa5\x01\xba\x01\xa1\x01\n" + "\x14name.format.optional\x124name must be empty or in the format clients/{ULIDv2}\x1aSsize(this) == 0 || this.matches('^clients/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$')R\x04name\x12R\n" + - "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x120\n" + - "\fdisplay_name\x18\x03 \x01(\tB\r\xbaH\n" + + "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12V\n" + + "\x06owners\x18\x03 \x03(\tB>\xbaH;\x92\x018\"6r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x06owners\x120\n" + + "\fdisplay_name\x18\x04 \x01(\tB\r\xbaH\n" + "\xc8\x01\x01r\x05\x10\x01\x18\xff\x01R\vdisplayName\x12V\n" + - "\x0enatural_person\x18\x04 \x01(\v2-.meshtrade.compliance.client.v1.NaturalPersonH\x00R\rnaturalPerson\x12C\n" + - "\acompany\x18\x05 \x01(\v2'.meshtrade.compliance.client.v1.CompanyH\x00R\acompany\x12:\n" + - "\x04fund\x18\x06 \x01(\v2$.meshtrade.compliance.client.v1.FundH\x00R\x04fund\x12=\n" + - "\x05trust\x18\a \x01(\v2%.meshtrade.compliance.client.v1.TrustH\x00R\x05trust\x12p\n" + - "\x13verification_status\x18\b \x01(\x0e22.meshtrade.compliance.client.v1.VerificationStatusB\v\xbaH\b\xc8\x01\x01\x82\x01\x02\x10\x01R\x12verificationStatus\x12\x85\x02\n" + - "\x16verification_authority\x18\t \x01(\tB\xcd\x01\xbaH\xc9\x01\xba\x01\xc5\x01\n" + + "\x0enatural_person\x18\x05 \x01(\v2-.meshtrade.compliance.client.v1.NaturalPersonH\x00R\rnaturalPerson\x12C\n" + + "\acompany\x18\x06 \x01(\v2'.meshtrade.compliance.client.v1.CompanyH\x00R\acompany\x12:\n" + + "\x04fund\x18\a \x01(\v2$.meshtrade.compliance.client.v1.FundH\x00R\x04fund\x12=\n" + + "\x05trust\x18\b \x01(\v2%.meshtrade.compliance.client.v1.TrustH\x00R\x05trust\x12p\n" + + "\x13verification_status\x18\t \x01(\x0e22.meshtrade.compliance.client.v1.VerificationStatusB\v\xbaH\b\xc8\x01\x01\x82\x01\x02\x10\x01R\x12verificationStatus\x12\x85\x02\n" + + "\x16verification_authority\x18\n" + + " \x01(\tB\xcd\x01\xbaH\xc9\x01\xba\x01\xc5\x01\n" + "&verification_authority.format.optional\x12Fverification_authority must be empty or in the format clients/{ULIDv2}\x1aSsize(this) == 0 || this.matches('^clients/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$')R\x15verificationAuthority\x12G\n" + - "\x11verification_date\x18\n" + - " \x01(\v2\x1a.google.protobuf.TimestampR\x10verificationDate\x12P\n" + - "\x16next_verification_date\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\x14nextVerificationDateB\x0e\n" + + "\x11verification_date\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\x10verificationDate\x12P\n" + + "\x16next_verification_date\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\x14nextVerificationDateB\x0e\n" + "\flegal_personBc\n" + "%co.meshtrade.api.compliance.client.v1Z:github.com/meshtrade/api/go/compliance/client/v1;client_v1b\x06proto3" diff --git a/go/iam/api_user/v1/api_user.pb.go b/go/iam/api_user/v1/api_user.pb.go index 2c1b9121..a91ef459 100644 --- a/go/iam/api_user/v1/api_user.pb.go +++ b/go/iam/api_user/v1/api_user.pb.go @@ -152,20 +152,23 @@ type APIUser struct { // This field is required on creation and establishes the direct ownership link. // Format: groups/{ULIDv2}. Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + // Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + // System set on creation. + Owners []string `protobuf:"bytes,3,rep,name=owners,proto3" json:"owners,omitempty"` // A non-unique, user-provided name for the API user, used for display purposes. // Required on creation. - DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // The current state of the API user (active or inactive). // System set on creation to default value of inactive. - State APIUserState `protobuf:"varint,4,opt,name=state,proto3,enum=meshtrade.iam.api_user.v1.APIUserState" json:"state,omitempty"` + State APIUserState `protobuf:"varint,5,opt,name=state,proto3,enum=meshtrade.iam.api_user.v1.APIUserState" json:"state,omitempty"` // Roles is a list of the standard roles assigned to this API user, // prepended by the name of the group in which they have been assigned that role. // e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum. - Roles []string `protobuf:"bytes,5,rep,name=roles,proto3" json:"roles,omitempty"` + Roles []string `protobuf:"bytes,6,rep,name=roles,proto3" json:"roles,omitempty"` // The plaintext API key for the API user. // This field is only populated on the entity the first time it is returned after creation - it is NOT stored. // Populated once by system on creation. - ApiKey string `protobuf:"bytes,6,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` + ApiKey string `protobuf:"bytes,7,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -214,6 +217,13 @@ func (x *APIUser) GetOwner() string { return "" } +func (x *APIUser) GetOwners() []string { + if x != nil { + return x.Owners + } + return nil +} + func (x *APIUser) GetDisplayName() string { if x != nil { return x.DisplayName @@ -246,17 +256,18 @@ var File_meshtrade_iam_api_user_v1_api_user_proto protoreflect.FileDescriptor const file_meshtrade_iam_api_user_v1_api_user_proto_rawDesc = "" + "\n" + - "(meshtrade/iam/api_user/v1/api_user.proto\x12\x19meshtrade.iam.api_user.v1\x1a\x1bbuf/validate/validate.proto\"\xa0\x06\n" + + "(meshtrade/iam/api_user/v1/api_user.proto\x12\x19meshtrade.iam.api_user.v1\x1a\x1bbuf/validate/validate.proto\"\xf8\x06\n" + "\aAPIUser\x12\xc2\x01\n" + "\x04name\x18\x01 \x01(\tB\xad\x01\xbaH\xa9\x01\xba\x01\xa5\x01\n" + "\x14name.format.optional\x126name must be empty or in the format api_users/{ULIDv2}\x1aUsize(this) == 0 || this.matches('^api_users/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$')R\x04name\x12R\n" + - "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12\xb4\x01\n" + - "\fdisplay_name\x18\x03 \x01(\tB\x90\x01\xbaH\x8c\x01\xba\x01\x7f\n" + + "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12V\n" + + "\x06owners\x18\x03 \x03(\tB>\xbaH;\x92\x018\"6r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x06owners\x12\xb4\x01\n" + + "\fdisplay_name\x18\x04 \x01(\tB\x90\x01\xbaH\x8c\x01\xba\x01\x7f\n" + "\x15display_name.required\x12Adisplay name is required and must be between 1 and 255 characters\x1a#size(this) > 0 && size(this) <= 255\xc8\x01\x01r\x05\x10\x01\x18\xff\x01R\vdisplayName\x12\xbe\x01\n" + - "\x05state\x18\x04 \x01(\x0e2'.meshtrade.iam.api_user.v1.APIUserStateB\x7f\xbaH|\xba\x01t\n" + + "\x05state\x18\x05 \x01(\x0e2'.meshtrade.iam.api_user.v1.APIUserStateB\x7f\xbaH|\xba\x01t\n" + "\vstate.valid\x12/state must be a valid APIUserState if specified\x1a4int(this) == 0 || (int(this) >= 1 && int(this) <= 2)\x82\x01\x02\x10\x01R\x05state\x12k\n" + - "\x05roles\x18\x05 \x03(\tBU\xbaHR\x92\x01O\"MrK\x10/\x1802E^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}/roles/[1-9][0-9]{6,7}$R\x05roles\x12\x17\n" + - "\aapi_key\x18\x06 \x01(\tR\x06apiKey*f\n" + + "\x05roles\x18\x06 \x03(\tBU\xbaHR\x92\x01O\"MrK\x10/\x1802E^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}/roles/[1-9][0-9]{6,7}$R\x05roles\x12\x17\n" + + "\aapi_key\x18\a \x01(\tR\x06apiKey*f\n" + "\fAPIUserState\x12\x1e\n" + "\x1aAPI_USER_STATE_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15API_USER_STATE_ACTIVE\x10\x01\x12\x1b\n" + @@ -266,8 +277,8 @@ const file_meshtrade_iam_api_user_v1_api_user_proto_rawDesc = "" + "\x18API_USER_ACTION_ACTIVATE\x10\x01\x12\x1e\n" + "\x1aAPI_USER_ACTION_DEACTIVATE\x10\x02\x12\x1a\n" + "\x16API_USER_ACTION_CREATE\x10\x03\x12\x1a\n" + - "\x16API_USER_ACTION_UPDATE\x10\x04B[\n" + - " co.meshtrade.api.iam.api_user.v1Z7github.com/meshtrade/api/go/iam/api_user/v1;api_user_v1b\x06proto3" + "\x16API_USER_ACTION_UPDATE\x10\x04Bn\n" + + " co.meshtrade.api.iam.api_user.v1B\x11ApiUserOuterClassZ7github.com/meshtrade/api/go/iam/api_user/v1;api_user_v1b\x06proto3" var ( file_meshtrade_iam_api_user_v1_api_user_proto_rawDescOnce sync.Once diff --git a/go/iam/group/v1/group.pb.go b/go/iam/group/v1/group.pb.go index 80d48e74..09066eda 100644 --- a/go/iam/group/v1/group.pb.go +++ b/go/iam/group/v1/group.pb.go @@ -39,6 +39,9 @@ type Group struct { // This field is required on creation and establishes the direct ownership link. // Format: groups/{ULIDv2}. Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + // Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + // System set on creation. + Owners []string `protobuf:"bytes,3,rep,name=owners,proto3" json:"owners,omitempty"` // Human-readable name for organizational identification and display. // User-configurable and non-unique across the system. DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` @@ -92,6 +95,13 @@ func (x *Group) GetOwner() string { return "" } +func (x *Group) GetOwners() []string { + if x != nil { + return x.Owners + } + return nil +} + func (x *Group) GetDisplayName() string { if x != nil { return x.DisplayName @@ -110,11 +120,12 @@ var File_meshtrade_iam_group_v1_group_proto protoreflect.FileDescriptor const file_meshtrade_iam_group_v1_group_proto_rawDesc = "" + "\n" + - "\"meshtrade/iam/group/v1/group.proto\x12\x16meshtrade.iam.group.v1\x1a\x1bbuf/validate/validate.proto\"\xf8\x02\n" + + "\"meshtrade/iam/group/v1/group.proto\x12\x16meshtrade.iam.group.v1\x1a\x1bbuf/validate/validate.proto\"\xd0\x03\n" + "\x05Group\x12\xbc\x01\n" + "\x04name\x18\x01 \x01(\tB\xa7\x01\xbaH\xa3\x01\xba\x01\x9f\x01\n" + "\x14name.format.optional\x123name must be empty or in the format groups/{ULIDv2}\x1aRsize(this) == 0 || this.matches('^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$')R\x04name\x12R\n" + - "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x120\n" + + "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12V\n" + + "\x06owners\x18\x03 \x03(\tB>\xbaH;\x92\x018\"6r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x06owners\x120\n" + "\fdisplay_name\x18\x04 \x01(\tB\r\xbaH\n" + "\xc8\x01\x01r\x05\x10\x01\x18\xff\x01R\vdisplayName\x12*\n" + "\vdescription\x18\x05 \x01(\tB\b\xbaH\x05r\x03\x18\xe8\aR\vdescriptionBR\n" + diff --git a/go/iam/user/v1/user.pb.go b/go/iam/user/v1/user.pb.go index 53d19dd8..ff8c0290 100644 --- a/go/iam/user/v1/user.pb.go +++ b/go/iam/user/v1/user.pb.go @@ -37,13 +37,16 @@ type User struct { // This field is required on creation and establishes the direct ownership link. // Format: groups/{ULIDv2}. Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + // Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + // System set on creation. + Owners []string `protobuf:"bytes,3,rep,name=owners,proto3" json:"owners,omitempty"` // The unique email address of this user. // This field is required on creation and must be a valid email format. - Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + Email string `protobuf:"bytes,4,opt,name=email,proto3" json:"email,omitempty"` // Roles is a list of standard roles assigned to this user, // prepended by the name of the group in which they have been assigned that role. // e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum. - Roles []string `protobuf:"bytes,4,rep,name=roles,proto3" json:"roles,omitempty"` + Roles []string `protobuf:"bytes,5,rep,name=roles,proto3" json:"roles,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -92,6 +95,13 @@ func (x *User) GetOwner() string { return "" } +func (x *User) GetOwners() []string { + if x != nil { + return x.Owners + } + return nil +} + func (x *User) GetEmail() string { if x != nil { return x.Email @@ -110,14 +120,15 @@ var File_meshtrade_iam_user_v1_user_proto protoreflect.FileDescriptor const file_meshtrade_iam_user_v1_user_proto_rawDesc = "" + "\n" + - " meshtrade/iam/user/v1/user.proto\x12\x15meshtrade.iam.user.v1\x1a\x1bbuf/validate/validate.proto\"\xa6\x03\n" + + " meshtrade/iam/user/v1/user.proto\x12\x15meshtrade.iam.user.v1\x1a\x1bbuf/validate/validate.proto\"\xfe\x03\n" + "\x04User\x12\xba\x01\n" + "\x04name\x18\x01 \x01(\tB\xa5\x01\xbaH\xa1\x01\xba\x01\x9d\x01\n" + "\x14name.format.optional\x122name must be empty or in the format users/{ULIDv2}\x1aQsize(this) == 0 || this.matches('^users/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$')R\x04name\x12R\n" + - "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12 \n" + - "\x05email\x18\x03 \x01(\tB\n" + + "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12V\n" + + "\x06owners\x18\x03 \x03(\tB>\xbaH;\x92\x018\"6r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x06owners\x12 \n" + + "\x05email\x18\x04 \x01(\tB\n" + "\xbaH\a\xc8\x01\x01r\x02`\x01R\x05email\x12k\n" + - "\x05roles\x18\x04 \x03(\tBU\xbaHR\x92\x01O\"MrK\x10/\x1802E^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}/roles/[1-9][0-9]{6,7}$R\x05rolesBO\n" + + "\x05roles\x18\x05 \x03(\tBU\xbaHR\x92\x01O\"MrK\x10/\x1802E^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}/roles/[1-9][0-9]{6,7}$R\x05rolesBO\n" + "\x1cco.meshtrade.api.iam.user.v1Z/github.com/meshtrade/api/go/iam/user/v1;user_v1b\x06proto3" var ( diff --git a/go/studio/instrument/v1/instrument.pb.go b/go/studio/instrument/v1/instrument.pb.go index e2233577..72cff068 100644 --- a/go/studio/instrument/v1/instrument.pb.go +++ b/go/studio/instrument/v1/instrument.pb.go @@ -34,6 +34,9 @@ type Instrument struct { // Defines the immediate hierarchical relationship. // Required on creation. Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + // Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + // System set on creation. + Owners []string `protobuf:"bytes,3,rep,name=owners,proto3" json:"owners,omitempty"` // Human-readable name for organizational identification and display. // User-configurable and non-unique across the system. DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` @@ -87,6 +90,13 @@ func (x *Instrument) GetOwner() string { return "" } +func (x *Instrument) GetOwners() []string { + if x != nil { + return x.Owners + } + return nil +} + func (x *Instrument) GetDisplayName() string { if x != nil { return x.DisplayName @@ -105,12 +115,13 @@ var File_meshtrade_studio_instrument_v1_instrument_proto protoreflect.FileDescri const file_meshtrade_studio_instrument_v1_instrument_proto_rawDesc = "" + "\n" + - "/meshtrade/studio/instrument/v1/instrument.proto\x12\x1emeshtrade.studio.instrument.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1dmeshtrade/type/v1/token.proto\"\xfb\x02\n" + + "/meshtrade/studio/instrument/v1/instrument.proto\x12\x1emeshtrade.studio.instrument.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1dmeshtrade/type/v1/token.proto\"\xd3\x03\n" + "\n" + "Instrument\x12\xbc\x01\n" + "\x04name\x18\x01 \x01(\tB\xa7\x01\xbaH\xa3\x01\xba\x01\x9f\x01\n" + "\x14name.format.optional\x123name must be empty or in the format groups/{ULIDv2}\x1aRsize(this) == 0 || this.matches('^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$')R\x04name\x12O\n" + - "\x05owner\x18\x02 \x01(\tB9\xbaH6r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12-\n" + + "\x05owner\x18\x02 \x01(\tB9\xbaH6r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12V\n" + + "\x06owners\x18\x03 \x03(\tB>\xbaH;\x92\x018\"6r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x06owners\x12-\n" + "\fdisplay_name\x18\x04 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\xff\x01R\vdisplayName\x12.\n" + "\x05token\x18\x05 \x01(\v2\x18.meshtrade.type.v1.TokenR\x05tokenBg\n" + diff --git a/go/trading/limit_order/v1/limit_order.pb.go b/go/trading/limit_order/v1/limit_order.pb.go index 8c5a3da9..bd470b07 100644 --- a/go/trading/limit_order/v1/limit_order.pb.go +++ b/go/trading/limit_order/v1/limit_order.pb.go @@ -166,43 +166,46 @@ type LimitOrder struct { // This field is required on creation and establishes the direct ownership link. // Format: groups/{ULIDv2}. Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + // Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + // System set on creation. + Owners []string `protobuf:"bytes,3,rep,name=owners,proto3" json:"owners,omitempty"` // The account associated with this limit order. // Format: accounts/{ULIDv2}. // This field is required on creation. - Account string `protobuf:"bytes,3,opt,name=account,proto3" json:"account,omitempty"` + Account string `protobuf:"bytes,5,opt,name=account,proto3" json:"account,omitempty"` // External reference for client-side tracking and correlation. // This field allows clients to associate orders with their own identifiers. // // If specified, must be unique within the scope of limit orders owned by // this owner. The Mesh system enforces this constraint to ensure reliable // lookups via GetLimitOrderByExternalReference. - ExternalReference string `protobuf:"bytes,5,opt,name=external_reference,json=externalReference,proto3" json:"external_reference,omitempty"` + ExternalReference string `protobuf:"bytes,6,opt,name=external_reference,json=externalReference,proto3" json:"external_reference,omitempty"` // Order side indicating buy or sell. // This field is required on creation. - Side LimitOrderSide `protobuf:"varint,6,opt,name=side,proto3,enum=meshtrade.trading.limit_order.v1.LimitOrderSide" json:"side,omitempty"` + Side LimitOrderSide `protobuf:"varint,7,opt,name=side,proto3,enum=meshtrade.trading.limit_order.v1.LimitOrderSide" json:"side,omitempty"` // Limit price for the order. // This field is required on creation. - LimitPrice *v1.Amount `protobuf:"bytes,7,opt,name=limit_price,json=limitPrice,proto3" json:"limit_price,omitempty"` + LimitPrice *v1.Amount `protobuf:"bytes,8,opt,name=limit_price,json=limitPrice,proto3" json:"limit_price,omitempty"` // Order quantity. // This field is required on creation. - Quantity *v1.Amount `protobuf:"bytes,8,opt,name=quantity,proto3" json:"quantity,omitempty"` + Quantity *v1.Amount `protobuf:"bytes,9,opt,name=quantity,proto3" json:"quantity,omitempty"` // Fill price from live ledger data. // Calculated as the volume weighted average price (VWAP) of all trades // that filled this order. This value is computed in real-time from ledger // data and becomes final when the order is marked complete. // // Only populated when live_ledger_data=true in request. - FillPrice *v1.Amount `protobuf:"bytes,9,opt,name=fill_price,json=fillPrice,proto3" json:"fill_price,omitempty"` + FillPrice *v1.Amount `protobuf:"bytes,10,opt,name=fill_price,json=fillPrice,proto3" json:"fill_price,omitempty"` // Filled quantity from live ledger data. // Represents the total amount of the order that has been filled on the ledger. // This value is computed in real-time from ledger data and becomes final // when the order is marked complete. // // Only populated when live_ledger_data=true in request. - FilledQuantity *v1.Amount `protobuf:"bytes,10,opt,name=filled_quantity,json=filledQuantity,proto3" json:"filled_quantity,omitempty"` + FilledQuantity *v1.Amount `protobuf:"bytes,11,opt,name=filled_quantity,json=filledQuantity,proto3" json:"filled_quantity,omitempty"` // Order status from live ledger data. // Only populated when live_ledger_data=true in request. - Status LimitOrderStatus `protobuf:"varint,11,opt,name=status,proto3,enum=meshtrade.trading.limit_order.v1.LimitOrderStatus" json:"status,omitempty"` + Status LimitOrderStatus `protobuf:"varint,12,opt,name=status,proto3,enum=meshtrade.trading.limit_order.v1.LimitOrderStatus" json:"status,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -251,6 +254,13 @@ func (x *LimitOrder) GetOwner() string { return "" } +func (x *LimitOrder) GetOwners() []string { + if x != nil { + return x.Owners + } + return nil +} + func (x *LimitOrder) GetAccount() string { if x != nil { return x.Account @@ -311,24 +321,25 @@ var File_meshtrade_trading_limit_order_v1_limit_order_proto protoreflect.FileDes const file_meshtrade_trading_limit_order_v1_limit_order_proto_rawDesc = "" + "\n" + - "2meshtrade/trading/limit_order/v1/limit_order.proto\x12 meshtrade.trading.limit_order.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1emeshtrade/type/v1/amount.proto\"\xf1\x06\n" + + "2meshtrade/trading/limit_order/v1/limit_order.proto\x12 meshtrade.trading.limit_order.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1emeshtrade/type/v1/amount.proto\"\xc9\a\n" + "\n" + "LimitOrder\x12\xc8\x01\n" + "\x04name\x18\x01 \x01(\tB\xb3\x01\xbaH\xaf\x01\xba\x01\xab\x01\n" + "\x14name.format.optional\x129name must be empty or in the format limit_orders/{ULIDv2}\x1aXsize(this) == 0 || this.matches('^limit_orders/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$')R\x04name\x12R\n" + - "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12X\n" + - "\aaccount\x18\x03 \x01(\tB>\xbaH;\xc8\x01\x01r621^accounts/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01#R\aaccount\x127\n" + - "\x12external_reference\x18\x05 \x01(\tB\b\xbaH\x05r\x03\x18\xc8\x01R\x11externalReference\x12P\n" + - "\x04side\x18\x06 \x01(\x0e20.meshtrade.trading.limit_order.v1.LimitOrderSideB\n" + + "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12V\n" + + "\x06owners\x18\x03 \x03(\tB>\xbaH;\x92\x018\"6r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x06owners\x12X\n" + + "\aaccount\x18\x05 \x01(\tB>\xbaH;\xc8\x01\x01r621^accounts/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01#R\aaccount\x127\n" + + "\x12external_reference\x18\x06 \x01(\tB\b\xbaH\x05r\x03\x18\xc8\x01R\x11externalReference\x12P\n" + + "\x04side\x18\a \x01(\x0e20.meshtrade.trading.limit_order.v1.LimitOrderSideB\n" + "\xbaH\a\x82\x01\x04\x10\x01 \x00R\x04side\x12B\n" + - "\vlimit_price\x18\a \x01(\v2\x19.meshtrade.type.v1.AmountB\x06\xbaH\x03\xc8\x01\x01R\n" + + "\vlimit_price\x18\b \x01(\v2\x19.meshtrade.type.v1.AmountB\x06\xbaH\x03\xc8\x01\x01R\n" + "limitPrice\x12=\n" + - "\bquantity\x18\b \x01(\v2\x19.meshtrade.type.v1.AmountB\x06\xbaH\x03\xc8\x01\x01R\bquantity\x128\n" + + "\bquantity\x18\t \x01(\v2\x19.meshtrade.type.v1.AmountB\x06\xbaH\x03\xc8\x01\x01R\bquantity\x128\n" + "\n" + - "fill_price\x18\t \x01(\v2\x19.meshtrade.type.v1.AmountR\tfillPrice\x12B\n" + - "\x0ffilled_quantity\x18\n" + - " \x01(\v2\x19.meshtrade.type.v1.AmountR\x0efilledQuantity\x12J\n" + - "\x06status\x18\v \x01(\x0e22.meshtrade.trading.limit_order.v1.LimitOrderStatusR\x06statusJ\x04\b\x04\x10\x05R\fdisplay_name*g\n" + + "fill_price\x18\n" + + " \x01(\v2\x19.meshtrade.type.v1.AmountR\tfillPrice\x12B\n" + + "\x0ffilled_quantity\x18\v \x01(\v2\x19.meshtrade.type.v1.AmountR\x0efilledQuantity\x12J\n" + + "\x06status\x18\f \x01(\x0e22.meshtrade.trading.limit_order.v1.LimitOrderStatusR\x06statusJ\x04\b\x04\x10\x05R\fdisplay_name*g\n" + "\x0eLimitOrderSide\x12 \n" + "\x1cLIMIT_ORDER_SIDE_UNSPECIFIED\x10\x00\x12\x18\n" + "\x14LIMIT_ORDER_SIDE_BUY\x10\x01\x12\x19\n" + diff --git a/go/wallet/account/v1/account.pb.go b/go/wallet/account/v1/account.pb.go index 920d37bc..23a25662 100644 --- a/go/wallet/account/v1/account.pb.go +++ b/go/wallet/account/v1/account.pb.go @@ -98,6 +98,9 @@ type Account struct { // This field is required on creation and establishes the direct ownership link. // Format: groups/{ULIDv2}. Owner string `protobuf:"bytes,2,opt,name=owner,proto3" json:"owner,omitempty"` + // Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + // System set on creation. + Owners []string `protobuf:"bytes,3,rep,name=owners,proto3" json:"owners,omitempty"` // The Unique Mesh Account Number for simplified account identification. // Format: 7-digit number starting with 1 (e.g., 1234567). // This field is system-generated and immutable. @@ -180,6 +183,13 @@ func (x *Account) GetOwner() string { return "" } +func (x *Account) GetOwners() []string { + if x != nil { + return x.Owners + } + return nil +} + func (x *Account) GetNumber() string { if x != nil { return x.Number @@ -432,11 +442,12 @@ var File_meshtrade_wallet_account_v1_account_proto protoreflect.FileDescriptor const file_meshtrade_wallet_account_v1_account_proto_rawDesc = "" + "\n" + - ")meshtrade/wallet/account/v1/account.proto\x12\x1bmeshtrade.wallet.account.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a4meshtrade/studio/instrument/v1/instrument_type.proto\x1a)meshtrade/studio/instrument/v1/unit.proto\x1a\x1emeshtrade/type/v1/amount.proto\x1a\x1emeshtrade/type/v1/ledger.proto\"\x87\a\n" + + ")meshtrade/wallet/account/v1/account.proto\x12\x1bmeshtrade.wallet.account.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a4meshtrade/studio/instrument/v1/instrument_type.proto\x1a)meshtrade/studio/instrument/v1/unit.proto\x1a\x1emeshtrade/type/v1/amount.proto\x1a\x1emeshtrade/type/v1/ledger.proto\"\xdf\a\n" + "\aAccount\x12\xc0\x01\n" + "\x04name\x18\x01 \x01(\tB\xab\x01\xbaH\xa7\x01\xba\x01\xa3\x01\n" + "\x14name.format.optional\x125name must be empty or in the format accounts/{ULIDv2}\x1aTsize(this) == 0 || this.matches('^accounts/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$')R\x04name\x12R\n" + - "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12\xab\x01\n" + + "\x05owner\x18\x02 \x01(\tB<\xbaH9\xc8\x01\x01r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x05owner\x12V\n" + + "\x06owners\x18\x03 \x03(\tB>\xbaH;\x92\x018\"6r42/^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$\x98\x01!R\x06owners\x12\xab\x01\n" + "\x06number\x18\x05 \x01(\tB\x92\x01\xbaH\x8e\x01\xba\x01\x8a\x01\n" + "\x16number.format.optional\x12@number must be empty or a 7-digit account number starting with 1\x1a.size(this) == 0 || this.matches('^1[0-9]{6}$')R\x06number\x12%\n" + "\tledger_id\x18\x06 \x01(\tB\b\xbaH\x05r\x03\x18\xff\x01R\bledgerId\x12@\n" + diff --git a/java/src/main/java/co/meshtrade/api/iam/api_user/v1/ApiUser.java b/java/src/main/java/co/meshtrade/api/iam/api_user/v1/ApiUser.java deleted file mode 100644 index 87b8bd6f..00000000 --- a/java/src/main/java/co/meshtrade/api/iam/api_user/v1/ApiUser.java +++ /dev/null @@ -1,2165 +0,0 @@ -// Generated by the protocol buffer compiler. DO NOT EDIT! -// NO CHECKED-IN PROTOBUF GENCODE -// source: meshtrade/iam/api_user/v1/api_user.proto -// Protobuf Java Version: 4.33.0 - -package co.meshtrade.api.iam.api_user.v1; - -@com.google.protobuf.Generated -public final class ApiUser extends com.google.protobuf.GeneratedFile { - private ApiUser() {} - static { - com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 33, - /* patch= */ 0, - /* suffix= */ "", - "ApiUser"); - } - public static void registerAllExtensions( - com.google.protobuf.ExtensionRegistryLite registry) { - } - - public static void registerAllExtensions( - com.google.protobuf.ExtensionRegistry registry) { - registerAllExtensions( - (com.google.protobuf.ExtensionRegistryLite) registry); - } - /** - * Protobuf enum {@code meshtrade.iam.api_user.v1.APIUserState} - */ - public enum APIUserState - implements com.google.protobuf.ProtocolMessageEnum { - /** - *
-     *
-     * Unknown or not specified.
-     * This is a default value to prevent accidental assignment and should not be used.
-     * 
- * - * API_USER_STATE_UNSPECIFIED = 0; - */ - API_USER_STATE_UNSPECIFIED(0), - /** - *
-     *
-     * API user is active and associated API keys can be used.
-     * 
- * - * API_USER_STATE_ACTIVE = 1; - */ - API_USER_STATE_ACTIVE(1), - /** - *
-     *
-     * API user is inactive and associated API keys cannot be used.
-     * 
- * - * API_USER_STATE_INACTIVE = 2; - */ - API_USER_STATE_INACTIVE(2), - UNRECOGNIZED(-1), - ; - - static { - com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 33, - /* patch= */ 0, - /* suffix= */ "", - "APIUserState"); - } - /** - *
-     *
-     * Unknown or not specified.
-     * This is a default value to prevent accidental assignment and should not be used.
-     * 
- * - * API_USER_STATE_UNSPECIFIED = 0; - */ - public static final int API_USER_STATE_UNSPECIFIED_VALUE = 0; - /** - *
-     *
-     * API user is active and associated API keys can be used.
-     * 
- * - * API_USER_STATE_ACTIVE = 1; - */ - public static final int API_USER_STATE_ACTIVE_VALUE = 1; - /** - *
-     *
-     * API user is inactive and associated API keys cannot be used.
-     * 
- * - * API_USER_STATE_INACTIVE = 2; - */ - public static final int API_USER_STATE_INACTIVE_VALUE = 2; - - - public final int getNumber() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalArgumentException( - "Can't get the number of an unknown enum value."); - } - return value; - } - - /** - * @param value The numeric wire value of the corresponding enum entry. - * @return The enum associated with the given numeric wire value. - * @deprecated Use {@link #forNumber(int)} instead. - */ - @java.lang.Deprecated - public static APIUserState valueOf(int value) { - return forNumber(value); - } - - /** - * @param value The numeric wire value of the corresponding enum entry. - * @return The enum associated with the given numeric wire value. - */ - public static APIUserState forNumber(int value) { - switch (value) { - case 0: return API_USER_STATE_UNSPECIFIED; - case 1: return API_USER_STATE_ACTIVE; - case 2: return API_USER_STATE_INACTIVE; - default: return null; - } - } - - public static com.google.protobuf.Internal.EnumLiteMap - internalGetValueMap() { - return internalValueMap; - } - private static final com.google.protobuf.Internal.EnumLiteMap< - APIUserState> internalValueMap = - new com.google.protobuf.Internal.EnumLiteMap() { - public APIUserState findValueByNumber(int number) { - return APIUserState.forNumber(number); - } - }; - - public final com.google.protobuf.Descriptors.EnumValueDescriptor - getValueDescriptor() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalStateException( - "Can't get the descriptor of an unrecognized enum value."); - } - return getDescriptor().getValues().get(ordinal()); - } - public final com.google.protobuf.Descriptors.EnumDescriptor - getDescriptorForType() { - return getDescriptor(); - } - public static com.google.protobuf.Descriptors.EnumDescriptor - getDescriptor() { - return co.meshtrade.api.iam.api_user.v1.ApiUser.getDescriptor().getEnumTypes().get(0); - } - - private static final APIUserState[] VALUES = values(); - - public static APIUserState valueOf( - com.google.protobuf.Descriptors.EnumValueDescriptor desc) { - if (desc.getType() != getDescriptor()) { - throw new java.lang.IllegalArgumentException( - "EnumValueDescriptor is not for this type."); - } - if (desc.getIndex() == -1) { - return UNRECOGNIZED; - } - return VALUES[desc.getIndex()]; - } - - private final int value; - - private APIUserState(int value) { - this.value = value; - } - - // @@protoc_insertion_point(enum_scope:meshtrade.iam.api_user.v1.APIUserState) - } - - /** - * Protobuf enum {@code meshtrade.iam.api_user.v1.APIUserAction} - */ - public enum APIUserAction - implements com.google.protobuf.ProtocolMessageEnum { - /** - *
-     *
-     * Unknown or not specified.
-     * This is a default value to prevent accidental assignment and should not be used.
-     * 
- * - * API_USER_ACTION_UNSPECIFIED = 0; - */ - API_USER_ACTION_UNSPECIFIED(0), - /** - *
-     *
-     * Activate an API user.
-     * 
- * - * API_USER_ACTION_ACTIVATE = 1; - */ - API_USER_ACTION_ACTIVATE(1), - /** - *
-     *
-     * Deactivate an API user.
-     * 
- * - * API_USER_ACTION_DEACTIVATE = 2; - */ - API_USER_ACTION_DEACTIVATE(2), - /** - *
-     *
-     * Create an API user.
-     * 
- * - * API_USER_ACTION_CREATE = 3; - */ - API_USER_ACTION_CREATE(3), - /** - *
-     *
-     * Update an API user.
-     * 
- * - * API_USER_ACTION_UPDATE = 4; - */ - API_USER_ACTION_UPDATE(4), - UNRECOGNIZED(-1), - ; - - static { - com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 33, - /* patch= */ 0, - /* suffix= */ "", - "APIUserAction"); - } - /** - *
-     *
-     * Unknown or not specified.
-     * This is a default value to prevent accidental assignment and should not be used.
-     * 
- * - * API_USER_ACTION_UNSPECIFIED = 0; - */ - public static final int API_USER_ACTION_UNSPECIFIED_VALUE = 0; - /** - *
-     *
-     * Activate an API user.
-     * 
- * - * API_USER_ACTION_ACTIVATE = 1; - */ - public static final int API_USER_ACTION_ACTIVATE_VALUE = 1; - /** - *
-     *
-     * Deactivate an API user.
-     * 
- * - * API_USER_ACTION_DEACTIVATE = 2; - */ - public static final int API_USER_ACTION_DEACTIVATE_VALUE = 2; - /** - *
-     *
-     * Create an API user.
-     * 
- * - * API_USER_ACTION_CREATE = 3; - */ - public static final int API_USER_ACTION_CREATE_VALUE = 3; - /** - *
-     *
-     * Update an API user.
-     * 
- * - * API_USER_ACTION_UPDATE = 4; - */ - public static final int API_USER_ACTION_UPDATE_VALUE = 4; - - - public final int getNumber() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalArgumentException( - "Can't get the number of an unknown enum value."); - } - return value; - } - - /** - * @param value The numeric wire value of the corresponding enum entry. - * @return The enum associated with the given numeric wire value. - * @deprecated Use {@link #forNumber(int)} instead. - */ - @java.lang.Deprecated - public static APIUserAction valueOf(int value) { - return forNumber(value); - } - - /** - * @param value The numeric wire value of the corresponding enum entry. - * @return The enum associated with the given numeric wire value. - */ - public static APIUserAction forNumber(int value) { - switch (value) { - case 0: return API_USER_ACTION_UNSPECIFIED; - case 1: return API_USER_ACTION_ACTIVATE; - case 2: return API_USER_ACTION_DEACTIVATE; - case 3: return API_USER_ACTION_CREATE; - case 4: return API_USER_ACTION_UPDATE; - default: return null; - } - } - - public static com.google.protobuf.Internal.EnumLiteMap - internalGetValueMap() { - return internalValueMap; - } - private static final com.google.protobuf.Internal.EnumLiteMap< - APIUserAction> internalValueMap = - new com.google.protobuf.Internal.EnumLiteMap() { - public APIUserAction findValueByNumber(int number) { - return APIUserAction.forNumber(number); - } - }; - - public final com.google.protobuf.Descriptors.EnumValueDescriptor - getValueDescriptor() { - if (this == UNRECOGNIZED) { - throw new java.lang.IllegalStateException( - "Can't get the descriptor of an unrecognized enum value."); - } - return getDescriptor().getValues().get(ordinal()); - } - public final com.google.protobuf.Descriptors.EnumDescriptor - getDescriptorForType() { - return getDescriptor(); - } - public static com.google.protobuf.Descriptors.EnumDescriptor - getDescriptor() { - return co.meshtrade.api.iam.api_user.v1.ApiUser.getDescriptor().getEnumTypes().get(1); - } - - private static final APIUserAction[] VALUES = values(); - - public static APIUserAction valueOf( - com.google.protobuf.Descriptors.EnumValueDescriptor desc) { - if (desc.getType() != getDescriptor()) { - throw new java.lang.IllegalArgumentException( - "EnumValueDescriptor is not for this type."); - } - if (desc.getIndex() == -1) { - return UNRECOGNIZED; - } - return VALUES[desc.getIndex()]; - } - - private final int value; - - private APIUserAction(int value) { - this.value = value; - } - - // @@protoc_insertion_point(enum_scope:meshtrade.iam.api_user.v1.APIUserAction) - } - - public interface APIUserOrBuilder extends - // @@protoc_insertion_point(interface_extends:meshtrade.iam.api_user.v1.APIUser) - com.google.protobuf.MessageOrBuilder { - - /** - *
-     *
-     * The unique resource name for the API user.
-     * Format: api_users/{ULIDv2}.
-     * This field is system-generated and immutable upon creation.
-     * Any value provided on creation is ignored.
-     * 
- * - * string name = 1 [json_name = "name", (.buf.validate.field) = { ... } - * @return The name. - */ - java.lang.String getName(); - /** - *
-     *
-     * The unique resource name for the API user.
-     * Format: api_users/{ULIDv2}.
-     * This field is system-generated and immutable upon creation.
-     * Any value provided on creation is ignored.
-     * 
- * - * string name = 1 [json_name = "name", (.buf.validate.field) = { ... } - * @return The bytes for name. - */ - com.google.protobuf.ByteString - getNameBytes(); - - /** - *
-     *
-     * The resource name of the parent group that owns this API user.
-     * This field is required on creation and establishes the direct ownership link.
-     * Format: groups/{ULIDv2}.
-     * 
- * - * string owner = 2 [json_name = "owner", (.buf.validate.field) = { ... } - * @return The owner. - */ - java.lang.String getOwner(); - /** - *
-     *
-     * The resource name of the parent group that owns this API user.
-     * This field is required on creation and establishes the direct ownership link.
-     * Format: groups/{ULIDv2}.
-     * 
- * - * string owner = 2 [json_name = "owner", (.buf.validate.field) = { ... } - * @return The bytes for owner. - */ - com.google.protobuf.ByteString - getOwnerBytes(); - - /** - *
-     *
-     * A non-unique, user-provided name for the API user, used for display purposes.
-     * Required on creation.
-     * 
- * - * string display_name = 3 [json_name = "displayName", (.buf.validate.field) = { ... } - * @return The displayName. - */ - java.lang.String getDisplayName(); - /** - *
-     *
-     * A non-unique, user-provided name for the API user, used for display purposes.
-     * Required on creation.
-     * 
- * - * string display_name = 3 [json_name = "displayName", (.buf.validate.field) = { ... } - * @return The bytes for displayName. - */ - com.google.protobuf.ByteString - getDisplayNameBytes(); - - /** - *
-     *
-     * The current state of the API user (active or inactive).
-     * System set on creation to default value of inactive.
-     * 
- * - * .meshtrade.iam.api_user.v1.APIUserState state = 4 [json_name = "state", (.buf.validate.field) = { ... } - * @return The enum numeric value on the wire for state. - */ - int getStateValue(); - /** - *
-     *
-     * The current state of the API user (active or inactive).
-     * System set on creation to default value of inactive.
-     * 
- * - * .meshtrade.iam.api_user.v1.APIUserState state = 4 [json_name = "state", (.buf.validate.field) = { ... } - * @return The state. - */ - co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState getState(); - - /** - *
-     *
-     * Roles is a list of the standard roles assigned to this API user,
-     * prepended by the name of the group in which they have been assigned that role.
-     * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-     * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @return A list containing the roles. - */ - java.util.List - getRolesList(); - /** - *
-     *
-     * Roles is a list of the standard roles assigned to this API user,
-     * prepended by the name of the group in which they have been assigned that role.
-     * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-     * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @return The count of roles. - */ - int getRolesCount(); - /** - *
-     *
-     * Roles is a list of the standard roles assigned to this API user,
-     * prepended by the name of the group in which they have been assigned that role.
-     * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-     * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param index The index of the element to return. - * @return The roles at the given index. - */ - java.lang.String getRoles(int index); - /** - *
-     *
-     * Roles is a list of the standard roles assigned to this API user,
-     * prepended by the name of the group in which they have been assigned that role.
-     * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-     * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param index The index of the value to return. - * @return The bytes of the roles at the given index. - */ - com.google.protobuf.ByteString - getRolesBytes(int index); - - /** - *
-     *
-     * The plaintext API key for the API user.
-     * This field is only populated on the entity the first time it is returned after creation - it is NOT stored.
-     * Populated once by system on creation.
-     * 
- * - * string api_key = 6 [json_name = "apiKey"]; - * @return The apiKey. - */ - java.lang.String getApiKey(); - /** - *
-     *
-     * The plaintext API key for the API user.
-     * This field is only populated on the entity the first time it is returned after creation - it is NOT stored.
-     * Populated once by system on creation.
-     * 
- * - * string api_key = 6 [json_name = "apiKey"]; - * @return The bytes for apiKey. - */ - com.google.protobuf.ByteString - getApiKeyBytes(); - } - /** - *
-   *
-   * Represents an API user for automated authentication and authorization.
-   *
-   * API users enable programmatic access to the Mesh API through API key
-   * authentication. Each API user belongs to a specific group and has
-   * defined roles that determine their permissions within that group.
-   * 
- * - * Protobuf type {@code meshtrade.iam.api_user.v1.APIUser} - */ - public static final class APIUser extends - com.google.protobuf.GeneratedMessage implements - // @@protoc_insertion_point(message_implements:meshtrade.iam.api_user.v1.APIUser) - APIUserOrBuilder { - private static final long serialVersionUID = 0L; - static { - com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion( - com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC, - /* major= */ 4, - /* minor= */ 33, - /* patch= */ 0, - /* suffix= */ "", - "APIUser"); - } - // Use APIUser.newBuilder() to construct. - private APIUser(com.google.protobuf.GeneratedMessage.Builder builder) { - super(builder); - } - private APIUser() { - name_ = ""; - owner_ = ""; - displayName_ = ""; - state_ = 0; - roles_ = - com.google.protobuf.LazyStringArrayList.emptyList(); - apiKey_ = ""; - } - - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return co.meshtrade.api.iam.api_user.v1.ApiUser.internal_static_meshtrade_iam_api_user_v1_APIUser_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable - internalGetFieldAccessorTable() { - return co.meshtrade.api.iam.api_user.v1.ApiUser.internal_static_meshtrade_iam_api_user_v1_APIUser_fieldAccessorTable - .ensureFieldAccessorsInitialized( - co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser.class, co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser.Builder.class); - } - - public static final int NAME_FIELD_NUMBER = 1; - @SuppressWarnings("serial") - private volatile java.lang.Object name_ = ""; - /** - *
-     *
-     * The unique resource name for the API user.
-     * Format: api_users/{ULIDv2}.
-     * This field is system-generated and immutable upon creation.
-     * Any value provided on creation is ignored.
-     * 
- * - * string name = 1 [json_name = "name", (.buf.validate.field) = { ... } - * @return The name. - */ - @java.lang.Override - public java.lang.String getName() { - java.lang.Object ref = name_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - name_ = s; - return s; - } - } - /** - *
-     *
-     * The unique resource name for the API user.
-     * Format: api_users/{ULIDv2}.
-     * This field is system-generated and immutable upon creation.
-     * Any value provided on creation is ignored.
-     * 
- * - * string name = 1 [json_name = "name", (.buf.validate.field) = { ... } - * @return The bytes for name. - */ - @java.lang.Override - public com.google.protobuf.ByteString - getNameBytes() { - java.lang.Object ref = name_; - if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (java.lang.String) ref); - name_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int OWNER_FIELD_NUMBER = 2; - @SuppressWarnings("serial") - private volatile java.lang.Object owner_ = ""; - /** - *
-     *
-     * The resource name of the parent group that owns this API user.
-     * This field is required on creation and establishes the direct ownership link.
-     * Format: groups/{ULIDv2}.
-     * 
- * - * string owner = 2 [json_name = "owner", (.buf.validate.field) = { ... } - * @return The owner. - */ - @java.lang.Override - public java.lang.String getOwner() { - java.lang.Object ref = owner_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - owner_ = s; - return s; - } - } - /** - *
-     *
-     * The resource name of the parent group that owns this API user.
-     * This field is required on creation and establishes the direct ownership link.
-     * Format: groups/{ULIDv2}.
-     * 
- * - * string owner = 2 [json_name = "owner", (.buf.validate.field) = { ... } - * @return The bytes for owner. - */ - @java.lang.Override - public com.google.protobuf.ByteString - getOwnerBytes() { - java.lang.Object ref = owner_; - if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (java.lang.String) ref); - owner_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int DISPLAY_NAME_FIELD_NUMBER = 3; - @SuppressWarnings("serial") - private volatile java.lang.Object displayName_ = ""; - /** - *
-     *
-     * A non-unique, user-provided name for the API user, used for display purposes.
-     * Required on creation.
-     * 
- * - * string display_name = 3 [json_name = "displayName", (.buf.validate.field) = { ... } - * @return The displayName. - */ - @java.lang.Override - public java.lang.String getDisplayName() { - java.lang.Object ref = displayName_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - displayName_ = s; - return s; - } - } - /** - *
-     *
-     * A non-unique, user-provided name for the API user, used for display purposes.
-     * Required on creation.
-     * 
- * - * string display_name = 3 [json_name = "displayName", (.buf.validate.field) = { ... } - * @return The bytes for displayName. - */ - @java.lang.Override - public com.google.protobuf.ByteString - getDisplayNameBytes() { - java.lang.Object ref = displayName_; - if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (java.lang.String) ref); - displayName_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - public static final int STATE_FIELD_NUMBER = 4; - private int state_ = 0; - /** - *
-     *
-     * The current state of the API user (active or inactive).
-     * System set on creation to default value of inactive.
-     * 
- * - * .meshtrade.iam.api_user.v1.APIUserState state = 4 [json_name = "state", (.buf.validate.field) = { ... } - * @return The enum numeric value on the wire for state. - */ - @java.lang.Override public int getStateValue() { - return state_; - } - /** - *
-     *
-     * The current state of the API user (active or inactive).
-     * System set on creation to default value of inactive.
-     * 
- * - * .meshtrade.iam.api_user.v1.APIUserState state = 4 [json_name = "state", (.buf.validate.field) = { ... } - * @return The state. - */ - @java.lang.Override public co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState getState() { - co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState result = co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState.forNumber(state_); - return result == null ? co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState.UNRECOGNIZED : result; - } - - public static final int ROLES_FIELD_NUMBER = 5; - @SuppressWarnings("serial") - private com.google.protobuf.LazyStringArrayList roles_ = - com.google.protobuf.LazyStringArrayList.emptyList(); - /** - *
-     *
-     * Roles is a list of the standard roles assigned to this API user,
-     * prepended by the name of the group in which they have been assigned that role.
-     * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-     * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @return A list containing the roles. - */ - public com.google.protobuf.ProtocolStringList - getRolesList() { - return roles_; - } - /** - *
-     *
-     * Roles is a list of the standard roles assigned to this API user,
-     * prepended by the name of the group in which they have been assigned that role.
-     * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-     * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @return The count of roles. - */ - public int getRolesCount() { - return roles_.size(); - } - /** - *
-     *
-     * Roles is a list of the standard roles assigned to this API user,
-     * prepended by the name of the group in which they have been assigned that role.
-     * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-     * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param index The index of the element to return. - * @return The roles at the given index. - */ - public java.lang.String getRoles(int index) { - return roles_.get(index); - } - /** - *
-     *
-     * Roles is a list of the standard roles assigned to this API user,
-     * prepended by the name of the group in which they have been assigned that role.
-     * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-     * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param index The index of the value to return. - * @return The bytes of the roles at the given index. - */ - public com.google.protobuf.ByteString - getRolesBytes(int index) { - return roles_.getByteString(index); - } - - public static final int API_KEY_FIELD_NUMBER = 6; - @SuppressWarnings("serial") - private volatile java.lang.Object apiKey_ = ""; - /** - *
-     *
-     * The plaintext API key for the API user.
-     * This field is only populated on the entity the first time it is returned after creation - it is NOT stored.
-     * Populated once by system on creation.
-     * 
- * - * string api_key = 6 [json_name = "apiKey"]; - * @return The apiKey. - */ - @java.lang.Override - public java.lang.String getApiKey() { - java.lang.Object ref = apiKey_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; - } else { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - apiKey_ = s; - return s; - } - } - /** - *
-     *
-     * The plaintext API key for the API user.
-     * This field is only populated on the entity the first time it is returned after creation - it is NOT stored.
-     * Populated once by system on creation.
-     * 
- * - * string api_key = 6 [json_name = "apiKey"]; - * @return The bytes for apiKey. - */ - @java.lang.Override - public com.google.protobuf.ByteString - getApiKeyBytes() { - java.lang.Object ref = apiKey_; - if (ref instanceof java.lang.String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (java.lang.String) ref); - apiKey_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - - private byte memoizedIsInitialized = -1; - @java.lang.Override - public final boolean isInitialized() { - byte isInitialized = memoizedIsInitialized; - if (isInitialized == 1) return true; - if (isInitialized == 0) return false; - - memoizedIsInitialized = 1; - return true; - } - - @java.lang.Override - public void writeTo(com.google.protobuf.CodedOutputStream output) - throws java.io.IOException { - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(name_)) { - com.google.protobuf.GeneratedMessage.writeString(output, 1, name_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(owner_)) { - com.google.protobuf.GeneratedMessage.writeString(output, 2, owner_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(displayName_)) { - com.google.protobuf.GeneratedMessage.writeString(output, 3, displayName_); - } - if (state_ != co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState.API_USER_STATE_UNSPECIFIED.getNumber()) { - output.writeEnum(4, state_); - } - for (int i = 0; i < roles_.size(); i++) { - com.google.protobuf.GeneratedMessage.writeString(output, 5, roles_.getRaw(i)); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(apiKey_)) { - com.google.protobuf.GeneratedMessage.writeString(output, 6, apiKey_); - } - getUnknownFields().writeTo(output); - } - - @java.lang.Override - public int getSerializedSize() { - int size = memoizedSize; - if (size != -1) return size; - - size = 0; - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(name_)) { - size += com.google.protobuf.GeneratedMessage.computeStringSize(1, name_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(owner_)) { - size += com.google.protobuf.GeneratedMessage.computeStringSize(2, owner_); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(displayName_)) { - size += com.google.protobuf.GeneratedMessage.computeStringSize(3, displayName_); - } - if (state_ != co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState.API_USER_STATE_UNSPECIFIED.getNumber()) { - size += com.google.protobuf.CodedOutputStream - .computeEnumSize(4, state_); - } - { - int dataSize = 0; - for (int i = 0; i < roles_.size(); i++) { - dataSize += computeStringSizeNoTag(roles_.getRaw(i)); - } - size += dataSize; - size += 1 * getRolesList().size(); - } - if (!com.google.protobuf.GeneratedMessage.isStringEmpty(apiKey_)) { - size += com.google.protobuf.GeneratedMessage.computeStringSize(6, apiKey_); - } - size += getUnknownFields().getSerializedSize(); - memoizedSize = size; - return size; - } - - @java.lang.Override - public boolean equals(final java.lang.Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser)) { - return super.equals(obj); - } - co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser other = (co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser) obj; - - if (!getName() - .equals(other.getName())) return false; - if (!getOwner() - .equals(other.getOwner())) return false; - if (!getDisplayName() - .equals(other.getDisplayName())) return false; - if (state_ != other.state_) return false; - if (!getRolesList() - .equals(other.getRolesList())) return false; - if (!getApiKey() - .equals(other.getApiKey())) return false; - if (!getUnknownFields().equals(other.getUnknownFields())) return false; - return true; - } - - @java.lang.Override - public int hashCode() { - if (memoizedHashCode != 0) { - return memoizedHashCode; - } - int hash = 41; - hash = (19 * hash) + getDescriptor().hashCode(); - hash = (37 * hash) + NAME_FIELD_NUMBER; - hash = (53 * hash) + getName().hashCode(); - hash = (37 * hash) + OWNER_FIELD_NUMBER; - hash = (53 * hash) + getOwner().hashCode(); - hash = (37 * hash) + DISPLAY_NAME_FIELD_NUMBER; - hash = (53 * hash) + getDisplayName().hashCode(); - hash = (37 * hash) + STATE_FIELD_NUMBER; - hash = (53 * hash) + state_; - if (getRolesCount() > 0) { - hash = (37 * hash) + ROLES_FIELD_NUMBER; - hash = (53 * hash) + getRolesList().hashCode(); - } - hash = (37 * hash) + API_KEY_FIELD_NUMBER; - hash = (53 * hash) + getApiKey().hashCode(); - hash = (29 * hash) + getUnknownFields().hashCode(); - memoizedHashCode = hash; - return hash; - } - - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom( - java.nio.ByteBuffer data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom( - java.nio.ByteBuffer data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom( - com.google.protobuf.ByteString data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom( - com.google.protobuf.ByteString data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom(byte[] data) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data); - } - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom( - byte[] data, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - return PARSER.parseFrom(data, extensionRegistry); - } - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseDelimitedFrom(java.io.InputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input); - } - - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseDelimitedFrom( - java.io.InputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseDelimitedWithIOException(PARSER, input, extensionRegistry); - } - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom( - com.google.protobuf.CodedInputStream input) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input); - } - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser parseFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - return com.google.protobuf.GeneratedMessage - .parseWithIOException(PARSER, input, extensionRegistry); - } - - @java.lang.Override - public Builder newBuilderForType() { return newBuilder(); } - public static Builder newBuilder() { - return DEFAULT_INSTANCE.toBuilder(); - } - public static Builder newBuilder(co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser prototype) { - return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); - } - @java.lang.Override - public Builder toBuilder() { - return this == DEFAULT_INSTANCE - ? new Builder() : new Builder().mergeFrom(this); - } - - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessage.BuilderParent parent) { - Builder builder = new Builder(parent); - return builder; - } - /** - *
-     *
-     * Represents an API user for automated authentication and authorization.
-     *
-     * API users enable programmatic access to the Mesh API through API key
-     * authentication. Each API user belongs to a specific group and has
-     * defined roles that determine their permissions within that group.
-     * 
- * - * Protobuf type {@code meshtrade.iam.api_user.v1.APIUser} - */ - public static final class Builder extends - com.google.protobuf.GeneratedMessage.Builder implements - // @@protoc_insertion_point(builder_implements:meshtrade.iam.api_user.v1.APIUser) - co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserOrBuilder { - public static final com.google.protobuf.Descriptors.Descriptor - getDescriptor() { - return co.meshtrade.api.iam.api_user.v1.ApiUser.internal_static_meshtrade_iam_api_user_v1_APIUser_descriptor; - } - - @java.lang.Override - protected com.google.protobuf.GeneratedMessage.FieldAccessorTable - internalGetFieldAccessorTable() { - return co.meshtrade.api.iam.api_user.v1.ApiUser.internal_static_meshtrade_iam_api_user_v1_APIUser_fieldAccessorTable - .ensureFieldAccessorsInitialized( - co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser.class, co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser.Builder.class); - } - - // Construct using co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser.newBuilder() - private Builder() { - - } - - private Builder( - com.google.protobuf.GeneratedMessage.BuilderParent parent) { - super(parent); - - } - @java.lang.Override - public Builder clear() { - super.clear(); - bitField0_ = 0; - name_ = ""; - owner_ = ""; - displayName_ = ""; - state_ = 0; - roles_ = - com.google.protobuf.LazyStringArrayList.emptyList(); - apiKey_ = ""; - return this; - } - - @java.lang.Override - public com.google.protobuf.Descriptors.Descriptor - getDescriptorForType() { - return co.meshtrade.api.iam.api_user.v1.ApiUser.internal_static_meshtrade_iam_api_user_v1_APIUser_descriptor; - } - - @java.lang.Override - public co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser getDefaultInstanceForType() { - return co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser.getDefaultInstance(); - } - - @java.lang.Override - public co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser build() { - co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException(result); - } - return result; - } - - @java.lang.Override - public co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser buildPartial() { - co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser result = new co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser(this); - if (bitField0_ != 0) { buildPartial0(result); } - onBuilt(); - return result; - } - - private void buildPartial0(co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser result) { - int from_bitField0_ = bitField0_; - if (((from_bitField0_ & 0x00000001) != 0)) { - result.name_ = name_; - } - if (((from_bitField0_ & 0x00000002) != 0)) { - result.owner_ = owner_; - } - if (((from_bitField0_ & 0x00000004) != 0)) { - result.displayName_ = displayName_; - } - if (((from_bitField0_ & 0x00000008) != 0)) { - result.state_ = state_; - } - if (((from_bitField0_ & 0x00000010) != 0)) { - roles_.makeImmutable(); - result.roles_ = roles_; - } - if (((from_bitField0_ & 0x00000020) != 0)) { - result.apiKey_ = apiKey_; - } - } - - @java.lang.Override - public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser) { - return mergeFrom((co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser)other); - } else { - super.mergeFrom(other); - return this; - } - } - - public Builder mergeFrom(co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser other) { - if (other == co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser.getDefaultInstance()) return this; - if (!other.getName().isEmpty()) { - name_ = other.name_; - bitField0_ |= 0x00000001; - onChanged(); - } - if (!other.getOwner().isEmpty()) { - owner_ = other.owner_; - bitField0_ |= 0x00000002; - onChanged(); - } - if (!other.getDisplayName().isEmpty()) { - displayName_ = other.displayName_; - bitField0_ |= 0x00000004; - onChanged(); - } - if (other.state_ != 0) { - setStateValue(other.getStateValue()); - } - if (!other.roles_.isEmpty()) { - if (roles_.isEmpty()) { - roles_ = other.roles_; - bitField0_ |= 0x00000010; - } else { - ensureRolesIsMutable(); - roles_.addAll(other.roles_); - } - onChanged(); - } - if (!other.getApiKey().isEmpty()) { - apiKey_ = other.apiKey_; - bitField0_ |= 0x00000020; - onChanged(); - } - this.mergeUnknownFields(other.getUnknownFields()); - onChanged(); - return this; - } - - @java.lang.Override - public final boolean isInitialized() { - return true; - } - - @java.lang.Override - public Builder mergeFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws java.io.IOException { - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 10: { - name_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00000001; - break; - } // case 10 - case 18: { - owner_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00000002; - break; - } // case 18 - case 26: { - displayName_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00000004; - break; - } // case 26 - case 32: { - state_ = input.readEnum(); - bitField0_ |= 0x00000008; - break; - } // case 32 - case 42: { - java.lang.String s = input.readStringRequireUtf8(); - ensureRolesIsMutable(); - roles_.add(s); - break; - } // case 42 - case 50: { - apiKey_ = input.readStringRequireUtf8(); - bitField0_ |= 0x00000020; - break; - } // case 50 - default: { - if (!super.parseUnknownField(input, extensionRegistry, tag)) { - done = true; // was an endgroup tag - } - break; - } // default: - } // switch (tag) - } // while (!done) - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.unwrapIOException(); - } finally { - onChanged(); - } // finally - return this; - } - private int bitField0_; - - private java.lang.Object name_ = ""; - /** - *
-       *
-       * The unique resource name for the API user.
-       * Format: api_users/{ULIDv2}.
-       * This field is system-generated and immutable upon creation.
-       * Any value provided on creation is ignored.
-       * 
- * - * string name = 1 [json_name = "name", (.buf.validate.field) = { ... } - * @return The name. - */ - public java.lang.String getName() { - java.lang.Object ref = name_; - if (!(ref instanceof java.lang.String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - name_ = s; - return s; - } else { - return (java.lang.String) ref; - } - } - /** - *
-       *
-       * The unique resource name for the API user.
-       * Format: api_users/{ULIDv2}.
-       * This field is system-generated and immutable upon creation.
-       * Any value provided on creation is ignored.
-       * 
- * - * string name = 1 [json_name = "name", (.buf.validate.field) = { ... } - * @return The bytes for name. - */ - public com.google.protobuf.ByteString - getNameBytes() { - java.lang.Object ref = name_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (java.lang.String) ref); - name_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - *
-       *
-       * The unique resource name for the API user.
-       * Format: api_users/{ULIDv2}.
-       * This field is system-generated and immutable upon creation.
-       * Any value provided on creation is ignored.
-       * 
- * - * string name = 1 [json_name = "name", (.buf.validate.field) = { ... } - * @param value The name to set. - * @return This builder for chaining. - */ - public Builder setName( - java.lang.String value) { - if (value == null) { throw new NullPointerException(); } - name_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - /** - *
-       *
-       * The unique resource name for the API user.
-       * Format: api_users/{ULIDv2}.
-       * This field is system-generated and immutable upon creation.
-       * Any value provided on creation is ignored.
-       * 
- * - * string name = 1 [json_name = "name", (.buf.validate.field) = { ... } - * @return This builder for chaining. - */ - public Builder clearName() { - name_ = getDefaultInstance().getName(); - bitField0_ = (bitField0_ & ~0x00000001); - onChanged(); - return this; - } - /** - *
-       *
-       * The unique resource name for the API user.
-       * Format: api_users/{ULIDv2}.
-       * This field is system-generated and immutable upon creation.
-       * Any value provided on creation is ignored.
-       * 
- * - * string name = 1 [json_name = "name", (.buf.validate.field) = { ... } - * @param value The bytes for name to set. - * @return This builder for chaining. - */ - public Builder setNameBytes( - com.google.protobuf.ByteString value) { - if (value == null) { throw new NullPointerException(); } - checkByteStringIsUtf8(value); - name_ = value; - bitField0_ |= 0x00000001; - onChanged(); - return this; - } - - private java.lang.Object owner_ = ""; - /** - *
-       *
-       * The resource name of the parent group that owns this API user.
-       * This field is required on creation and establishes the direct ownership link.
-       * Format: groups/{ULIDv2}.
-       * 
- * - * string owner = 2 [json_name = "owner", (.buf.validate.field) = { ... } - * @return The owner. - */ - public java.lang.String getOwner() { - java.lang.Object ref = owner_; - if (!(ref instanceof java.lang.String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - owner_ = s; - return s; - } else { - return (java.lang.String) ref; - } - } - /** - *
-       *
-       * The resource name of the parent group that owns this API user.
-       * This field is required on creation and establishes the direct ownership link.
-       * Format: groups/{ULIDv2}.
-       * 
- * - * string owner = 2 [json_name = "owner", (.buf.validate.field) = { ... } - * @return The bytes for owner. - */ - public com.google.protobuf.ByteString - getOwnerBytes() { - java.lang.Object ref = owner_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (java.lang.String) ref); - owner_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - *
-       *
-       * The resource name of the parent group that owns this API user.
-       * This field is required on creation and establishes the direct ownership link.
-       * Format: groups/{ULIDv2}.
-       * 
- * - * string owner = 2 [json_name = "owner", (.buf.validate.field) = { ... } - * @param value The owner to set. - * @return This builder for chaining. - */ - public Builder setOwner( - java.lang.String value) { - if (value == null) { throw new NullPointerException(); } - owner_ = value; - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - /** - *
-       *
-       * The resource name of the parent group that owns this API user.
-       * This field is required on creation and establishes the direct ownership link.
-       * Format: groups/{ULIDv2}.
-       * 
- * - * string owner = 2 [json_name = "owner", (.buf.validate.field) = { ... } - * @return This builder for chaining. - */ - public Builder clearOwner() { - owner_ = getDefaultInstance().getOwner(); - bitField0_ = (bitField0_ & ~0x00000002); - onChanged(); - return this; - } - /** - *
-       *
-       * The resource name of the parent group that owns this API user.
-       * This field is required on creation and establishes the direct ownership link.
-       * Format: groups/{ULIDv2}.
-       * 
- * - * string owner = 2 [json_name = "owner", (.buf.validate.field) = { ... } - * @param value The bytes for owner to set. - * @return This builder for chaining. - */ - public Builder setOwnerBytes( - com.google.protobuf.ByteString value) { - if (value == null) { throw new NullPointerException(); } - checkByteStringIsUtf8(value); - owner_ = value; - bitField0_ |= 0x00000002; - onChanged(); - return this; - } - - private java.lang.Object displayName_ = ""; - /** - *
-       *
-       * A non-unique, user-provided name for the API user, used for display purposes.
-       * Required on creation.
-       * 
- * - * string display_name = 3 [json_name = "displayName", (.buf.validate.field) = { ... } - * @return The displayName. - */ - public java.lang.String getDisplayName() { - java.lang.Object ref = displayName_; - if (!(ref instanceof java.lang.String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - displayName_ = s; - return s; - } else { - return (java.lang.String) ref; - } - } - /** - *
-       *
-       * A non-unique, user-provided name for the API user, used for display purposes.
-       * Required on creation.
-       * 
- * - * string display_name = 3 [json_name = "displayName", (.buf.validate.field) = { ... } - * @return The bytes for displayName. - */ - public com.google.protobuf.ByteString - getDisplayNameBytes() { - java.lang.Object ref = displayName_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (java.lang.String) ref); - displayName_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - *
-       *
-       * A non-unique, user-provided name for the API user, used for display purposes.
-       * Required on creation.
-       * 
- * - * string display_name = 3 [json_name = "displayName", (.buf.validate.field) = { ... } - * @param value The displayName to set. - * @return This builder for chaining. - */ - public Builder setDisplayName( - java.lang.String value) { - if (value == null) { throw new NullPointerException(); } - displayName_ = value; - bitField0_ |= 0x00000004; - onChanged(); - return this; - } - /** - *
-       *
-       * A non-unique, user-provided name for the API user, used for display purposes.
-       * Required on creation.
-       * 
- * - * string display_name = 3 [json_name = "displayName", (.buf.validate.field) = { ... } - * @return This builder for chaining. - */ - public Builder clearDisplayName() { - displayName_ = getDefaultInstance().getDisplayName(); - bitField0_ = (bitField0_ & ~0x00000004); - onChanged(); - return this; - } - /** - *
-       *
-       * A non-unique, user-provided name for the API user, used for display purposes.
-       * Required on creation.
-       * 
- * - * string display_name = 3 [json_name = "displayName", (.buf.validate.field) = { ... } - * @param value The bytes for displayName to set. - * @return This builder for chaining. - */ - public Builder setDisplayNameBytes( - com.google.protobuf.ByteString value) { - if (value == null) { throw new NullPointerException(); } - checkByteStringIsUtf8(value); - displayName_ = value; - bitField0_ |= 0x00000004; - onChanged(); - return this; - } - - private int state_ = 0; - /** - *
-       *
-       * The current state of the API user (active or inactive).
-       * System set on creation to default value of inactive.
-       * 
- * - * .meshtrade.iam.api_user.v1.APIUserState state = 4 [json_name = "state", (.buf.validate.field) = { ... } - * @return The enum numeric value on the wire for state. - */ - @java.lang.Override public int getStateValue() { - return state_; - } - /** - *
-       *
-       * The current state of the API user (active or inactive).
-       * System set on creation to default value of inactive.
-       * 
- * - * .meshtrade.iam.api_user.v1.APIUserState state = 4 [json_name = "state", (.buf.validate.field) = { ... } - * @param value The enum numeric value on the wire for state to set. - * @return This builder for chaining. - */ - public Builder setStateValue(int value) { - state_ = value; - bitField0_ |= 0x00000008; - onChanged(); - return this; - } - /** - *
-       *
-       * The current state of the API user (active or inactive).
-       * System set on creation to default value of inactive.
-       * 
- * - * .meshtrade.iam.api_user.v1.APIUserState state = 4 [json_name = "state", (.buf.validate.field) = { ... } - * @return The state. - */ - @java.lang.Override - public co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState getState() { - co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState result = co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState.forNumber(state_); - return result == null ? co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState.UNRECOGNIZED : result; - } - /** - *
-       *
-       * The current state of the API user (active or inactive).
-       * System set on creation to default value of inactive.
-       * 
- * - * .meshtrade.iam.api_user.v1.APIUserState state = 4 [json_name = "state", (.buf.validate.field) = { ... } - * @param value The state to set. - * @return This builder for chaining. - */ - public Builder setState(co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState value) { - if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000008; - state_ = value.getNumber(); - onChanged(); - return this; - } - /** - *
-       *
-       * The current state of the API user (active or inactive).
-       * System set on creation to default value of inactive.
-       * 
- * - * .meshtrade.iam.api_user.v1.APIUserState state = 4 [json_name = "state", (.buf.validate.field) = { ... } - * @return This builder for chaining. - */ - public Builder clearState() { - bitField0_ = (bitField0_ & ~0x00000008); - state_ = 0; - onChanged(); - return this; - } - - private com.google.protobuf.LazyStringArrayList roles_ = - com.google.protobuf.LazyStringArrayList.emptyList(); - private void ensureRolesIsMutable() { - if (!roles_.isModifiable()) { - roles_ = new com.google.protobuf.LazyStringArrayList(roles_); - } - bitField0_ |= 0x00000010; - } - /** - *
-       *
-       * Roles is a list of the standard roles assigned to this API user,
-       * prepended by the name of the group in which they have been assigned that role.
-       * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-       * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @return A list containing the roles. - */ - public com.google.protobuf.ProtocolStringList - getRolesList() { - roles_.makeImmutable(); - return roles_; - } - /** - *
-       *
-       * Roles is a list of the standard roles assigned to this API user,
-       * prepended by the name of the group in which they have been assigned that role.
-       * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-       * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @return The count of roles. - */ - public int getRolesCount() { - return roles_.size(); - } - /** - *
-       *
-       * Roles is a list of the standard roles assigned to this API user,
-       * prepended by the name of the group in which they have been assigned that role.
-       * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-       * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param index The index of the element to return. - * @return The roles at the given index. - */ - public java.lang.String getRoles(int index) { - return roles_.get(index); - } - /** - *
-       *
-       * Roles is a list of the standard roles assigned to this API user,
-       * prepended by the name of the group in which they have been assigned that role.
-       * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-       * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param index The index of the value to return. - * @return The bytes of the roles at the given index. - */ - public com.google.protobuf.ByteString - getRolesBytes(int index) { - return roles_.getByteString(index); - } - /** - *
-       *
-       * Roles is a list of the standard roles assigned to this API user,
-       * prepended by the name of the group in which they have been assigned that role.
-       * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-       * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param index The index to set the value at. - * @param value The roles to set. - * @return This builder for chaining. - */ - public Builder setRoles( - int index, java.lang.String value) { - if (value == null) { throw new NullPointerException(); } - ensureRolesIsMutable(); - roles_.set(index, value); - bitField0_ |= 0x00000010; - onChanged(); - return this; - } - /** - *
-       *
-       * Roles is a list of the standard roles assigned to this API user,
-       * prepended by the name of the group in which they have been assigned that role.
-       * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-       * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param value The roles to add. - * @return This builder for chaining. - */ - public Builder addRoles( - java.lang.String value) { - if (value == null) { throw new NullPointerException(); } - ensureRolesIsMutable(); - roles_.add(value); - bitField0_ |= 0x00000010; - onChanged(); - return this; - } - /** - *
-       *
-       * Roles is a list of the standard roles assigned to this API user,
-       * prepended by the name of the group in which they have been assigned that role.
-       * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-       * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param values The roles to add. - * @return This builder for chaining. - */ - public Builder addAllRoles( - java.lang.Iterable values) { - ensureRolesIsMutable(); - com.google.protobuf.AbstractMessageLite.Builder.addAll( - values, roles_); - bitField0_ |= 0x00000010; - onChanged(); - return this; - } - /** - *
-       *
-       * Roles is a list of the standard roles assigned to this API user,
-       * prepended by the name of the group in which they have been assigned that role.
-       * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-       * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @return This builder for chaining. - */ - public Builder clearRoles() { - roles_ = - com.google.protobuf.LazyStringArrayList.emptyList(); - bitField0_ = (bitField0_ & ~0x00000010);; - onChanged(); - return this; - } - /** - *
-       *
-       * Roles is a list of the standard roles assigned to this API user,
-       * prepended by the name of the group in which they have been assigned that role.
-       * e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum.
-       * 
- * - * repeated string roles = 5 [json_name = "roles", (.buf.validate.field) = { ... } - * @param value The bytes of the roles to add. - * @return This builder for chaining. - */ - public Builder addRolesBytes( - com.google.protobuf.ByteString value) { - if (value == null) { throw new NullPointerException(); } - checkByteStringIsUtf8(value); - ensureRolesIsMutable(); - roles_.add(value); - bitField0_ |= 0x00000010; - onChanged(); - return this; - } - - private java.lang.Object apiKey_ = ""; - /** - *
-       *
-       * The plaintext API key for the API user.
-       * This field is only populated on the entity the first time it is returned after creation - it is NOT stored.
-       * Populated once by system on creation.
-       * 
- * - * string api_key = 6 [json_name = "apiKey"]; - * @return The apiKey. - */ - public java.lang.String getApiKey() { - java.lang.Object ref = apiKey_; - if (!(ref instanceof java.lang.String)) { - com.google.protobuf.ByteString bs = - (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - apiKey_ = s; - return s; - } else { - return (java.lang.String) ref; - } - } - /** - *
-       *
-       * The plaintext API key for the API user.
-       * This field is only populated on the entity the first time it is returned after creation - it is NOT stored.
-       * Populated once by system on creation.
-       * 
- * - * string api_key = 6 [json_name = "apiKey"]; - * @return The bytes for apiKey. - */ - public com.google.protobuf.ByteString - getApiKeyBytes() { - java.lang.Object ref = apiKey_; - if (ref instanceof String) { - com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8( - (java.lang.String) ref); - apiKey_ = b; - return b; - } else { - return (com.google.protobuf.ByteString) ref; - } - } - /** - *
-       *
-       * The plaintext API key for the API user.
-       * This field is only populated on the entity the first time it is returned after creation - it is NOT stored.
-       * Populated once by system on creation.
-       * 
- * - * string api_key = 6 [json_name = "apiKey"]; - * @param value The apiKey to set. - * @return This builder for chaining. - */ - public Builder setApiKey( - java.lang.String value) { - if (value == null) { throw new NullPointerException(); } - apiKey_ = value; - bitField0_ |= 0x00000020; - onChanged(); - return this; - } - /** - *
-       *
-       * The plaintext API key for the API user.
-       * This field is only populated on the entity the first time it is returned after creation - it is NOT stored.
-       * Populated once by system on creation.
-       * 
- * - * string api_key = 6 [json_name = "apiKey"]; - * @return This builder for chaining. - */ - public Builder clearApiKey() { - apiKey_ = getDefaultInstance().getApiKey(); - bitField0_ = (bitField0_ & ~0x00000020); - onChanged(); - return this; - } - /** - *
-       *
-       * The plaintext API key for the API user.
-       * This field is only populated on the entity the first time it is returned after creation - it is NOT stored.
-       * Populated once by system on creation.
-       * 
- * - * string api_key = 6 [json_name = "apiKey"]; - * @param value The bytes for apiKey to set. - * @return This builder for chaining. - */ - public Builder setApiKeyBytes( - com.google.protobuf.ByteString value) { - if (value == null) { throw new NullPointerException(); } - checkByteStringIsUtf8(value); - apiKey_ = value; - bitField0_ |= 0x00000020; - onChanged(); - return this; - } - - // @@protoc_insertion_point(builder_scope:meshtrade.iam.api_user.v1.APIUser) - } - - // @@protoc_insertion_point(class_scope:meshtrade.iam.api_user.v1.APIUser) - private static final co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser DEFAULT_INSTANCE; - static { - DEFAULT_INSTANCE = new co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser(); - } - - public static co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser getDefaultInstance() { - return DEFAULT_INSTANCE; - } - - private static final com.google.protobuf.Parser - PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override - public APIUser parsePartialFrom( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - Builder builder = newBuilder(); - try { - builder.mergeFrom(input, extensionRegistry); - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(builder.buildPartial()); - } catch (com.google.protobuf.UninitializedMessageException e) { - throw e.asInvalidProtocolBufferException().setUnfinishedMessage(builder.buildPartial()); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e) - .setUnfinishedMessage(builder.buildPartial()); - } - return builder.buildPartial(); - } - }; - - public static com.google.protobuf.Parser parser() { - return PARSER; - } - - @java.lang.Override - public com.google.protobuf.Parser getParserForType() { - return PARSER; - } - - @java.lang.Override - public co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser getDefaultInstanceForType() { - return DEFAULT_INSTANCE; - } - - } - - private static final com.google.protobuf.Descriptors.Descriptor - internal_static_meshtrade_iam_api_user_v1_APIUser_descriptor; - private static final - com.google.protobuf.GeneratedMessage.FieldAccessorTable - internal_static_meshtrade_iam_api_user_v1_APIUser_fieldAccessorTable; - - public static com.google.protobuf.Descriptors.FileDescriptor - getDescriptor() { - return descriptor; - } - private static com.google.protobuf.Descriptors.FileDescriptor - descriptor; - static { - java.lang.String[] descriptorData = { - "\n(meshtrade/iam/api_user/v1/api_user.pro" + - "to\022\031meshtrade.iam.api_user.v1\032\033buf/valid" + - "ate/validate.proto\"\240\006\n\007APIUser\022\302\001\n\004name\030" + - "\001 \001(\tB\255\001\272H\251\001\272\001\245\001\n\024name.format.optional\0226" + - "name must be empty or in the format api_" + - "users/{ULIDv2}\032Usize(this) == 0 || this." + - "matches(\'^api_users/[0123456789ABCDEFGHJ" + - "KMNPQRSTVWXYZ]{26}$\')R\004name\022R\n\005owner\030\002 \001" + - "(\tB<\272H9r42/^groups/[0123456789ABCDEFGHJK" + - "MNPQRSTVWXYZ]{26}$\230\001!\310\001\001R\005owner\022\264\001\n\014disp" + - "lay_name\030\003 \001(\tB\220\001\272H\214\001r\005\020\001\030\377\001\272\001\177\n\025display" + - "_name.required\022Adisplay name is required" + - " and must be between 1 and 255 character" + - "s\032#size(this) > 0 && size(this) <= 255\310\001" + - "\001R\013displayName\022\276\001\n\005state\030\004 \001(\0162\'.meshtra" + - "de.iam.api_user.v1.APIUserStateB\177\272H|\202\001\002\020" + - "\001\272\001t\n\013state.valid\022/state must be a valid" + - " APIUserState if specified\0324int(this) ==" + - " 0 || (int(this) >= 1 && int(this) <= 2)" + - "R\005state\022k\n\005roles\030\005 \003(\tBU\272HR\222\001O\"MrK\020/\03002E" + - "^groups/[0123456789ABCDEFGHJKMNPQRSTVWXY" + - "Z]{26}/roles/[1-9][0-9]{6,7}$R\005roles\022\027\n\007" + - "api_key\030\006 \001(\tR\006apiKey*f\n\014APIUserState\022\036\n" + - "\032API_USER_STATE_UNSPECIFIED\020\000\022\031\n\025API_USE" + - "R_STATE_ACTIVE\020\001\022\033\n\027API_USER_STATE_INACT" + - "IVE\020\002*\246\001\n\rAPIUserAction\022\037\n\033API_USER_ACTI" + - "ON_UNSPECIFIED\020\000\022\034\n\030API_USER_ACTION_ACTI" + - "VATE\020\001\022\036\n\032API_USER_ACTION_DEACTIVATE\020\002\022\032" + - "\n\026API_USER_ACTION_CREATE\020\003\022\032\n\026API_USER_A" + - "CTION_UPDATE\020\004B[\n co.meshtrade.api.iam.a" + - "pi_user.v1Z7github.com/meshtrade/api/go/" + - "iam/api_user/v1;api_user_v1b\006proto3" - }; - descriptor = com.google.protobuf.Descriptors.FileDescriptor - .internalBuildGeneratedFileFrom(descriptorData, - new com.google.protobuf.Descriptors.FileDescriptor[] { - build.buf.validate.ValidateProto.getDescriptor(), - }); - internal_static_meshtrade_iam_api_user_v1_APIUser_descriptor = - getDescriptor().getMessageType(0); - internal_static_meshtrade_iam_api_user_v1_APIUser_fieldAccessorTable = new - com.google.protobuf.GeneratedMessage.FieldAccessorTable( - internal_static_meshtrade_iam_api_user_v1_APIUser_descriptor, - new java.lang.String[] { "Name", "Owner", "DisplayName", "State", "Roles", "ApiKey", }); - descriptor.resolveAllFeaturesImmutable(); - build.buf.validate.ValidateProto.getDescriptor(); - com.google.protobuf.ExtensionRegistry registry = - com.google.protobuf.ExtensionRegistry.newInstance(); - registry.add(build.buf.validate.ValidateProto.field); - com.google.protobuf.Descriptors.FileDescriptor - .internalUpdateFileDescriptor(descriptor, registry); - } - - // @@protoc_insertion_point(outer_class_scope) -} diff --git a/java/src/main/java/co/meshtrade/api/iam/api_user/v1/ApiUserStateMachine.java b/java/src/main/java/co/meshtrade/api/iam/api_user/v1/ApiUserStateMachine.java index b2dd954c..1c68b330 100644 --- a/java/src/main/java/co/meshtrade/api/iam/api_user/v1/ApiUserStateMachine.java +++ b/java/src/main/java/co/meshtrade/api/iam/api_user/v1/ApiUserStateMachine.java @@ -3,8 +3,8 @@ import java.util.EnumSet; import java.util.Set; -import co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserAction; -import co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState; +import co.meshtrade.api.iam.api_user.v1.ApiUserOuterClass.APIUserAction; +import co.meshtrade.api.iam.api_user.v1.ApiUserOuterClass.APIUserState; /** * State machine utilities for API User lifecycle management. diff --git a/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserServiceIntegrationTest.java b/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserServiceIntegrationTest.java index d80e3737..34b0fd3a 100644 --- a/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserServiceIntegrationTest.java +++ b/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserServiceIntegrationTest.java @@ -23,8 +23,8 @@ import co.meshtrade.api.auth.CredentialsDiscovery; import co.meshtrade.api.config.ServiceOptions; import co.meshtrade.api.grpc.BaseGRPCClient; -import co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser; -import co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState; +import co.meshtrade.api.iam.api_user.v1.ApiUserOuterClass.APIUser; +import co.meshtrade.api.iam.api_user.v1.ApiUserOuterClass.APIUserState; import co.meshtrade.api.iam.api_user.v1.Service.ActivateAPIUserRequest; import co.meshtrade.api.iam.api_user.v1.Service.AssignRolesToAPIUserRequest; import co.meshtrade.api.iam.api_user.v1.Service.RevokeRolesFromAPIUserRequest; diff --git a/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserServiceValidationTest.java b/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserServiceValidationTest.java index cfbd2412..68aff06e 100644 --- a/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserServiceValidationTest.java +++ b/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserServiceValidationTest.java @@ -8,8 +8,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import co.meshtrade.api.iam.api_user.v1.ApiUser.APIUser; -import co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState; +import co.meshtrade.api.iam.api_user.v1.ApiUserOuterClass.APIUser; +import co.meshtrade.api.iam.api_user.v1.ApiUserOuterClass.APIUserState; import co.meshtrade.api.iam.api_user.v1.Service.CreateAPIUserRequest; /** diff --git a/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserStateMachineTest.java b/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserStateMachineTest.java index 21fe5c80..b9671b46 100644 --- a/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserStateMachineTest.java +++ b/java/src/test/java/co/meshtrade/api/iam/api_user/v1/ApiUserStateMachineTest.java @@ -9,8 +9,8 @@ import org.junit.jupiter.api.Test; import co.meshtrade.api.iam.api_user.v1.ApiUserStateMachine; -import co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserAction; -import co.meshtrade.api.iam.api_user.v1.ApiUser.APIUserState; +import co.meshtrade.api.iam.api_user.v1.ApiUserOuterClass.APIUserAction; +import co.meshtrade.api.iam.api_user.v1.ApiUserOuterClass.APIUserState; /** * Comprehensive tests for ApiUserStateMachine utility methods. diff --git a/proto/meshtrade/compliance/client/v1/client.proto b/proto/meshtrade/compliance/client/v1/client.proto index 133ba03e..0e082302 100644 --- a/proto/meshtrade/compliance/client/v1/client.proto +++ b/proto/meshtrade/compliance/client/v1/client.proto @@ -47,12 +47,27 @@ message Client { } }]; + /* + Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + System set on creation. + */ + repeated string owners = 3 [(buf.validate.field) = { + repeated: { + items: { + string: { + len: 33 + pattern: "^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" + } + } + } + }]; + /* A non-unique, user-provided name for the client, used for display purposes in user interfaces and reports. Required on creation. */ - string display_name = 3 [(buf.validate.field) = { + string display_name = 4 [(buf.validate.field) = { required: true string: { min_len: 1 @@ -68,29 +83,29 @@ message Client { /* Set when the legal entity is an individual human being. */ - meshtrade.compliance.client.v1.NaturalPerson natural_person = 4; + meshtrade.compliance.client.v1.NaturalPerson natural_person = 5; /* Set when the legal entity is a company or corporation. */ - meshtrade.compliance.client.v1.Company company = 5; + meshtrade.compliance.client.v1.Company company = 6; /* Set when the legal entity is an investment fund. */ - meshtrade.compliance.client.v1.Fund fund = 6; + meshtrade.compliance.client.v1.Fund fund = 7; /* Set when the legal entity is a trust. */ - meshtrade.compliance.client.v1.Trust trust = 7; + meshtrade.compliance.client.v1.Trust trust = 8; } /* The definitive, most recent compliance status of the client (e.g., VERIFICATION_STATUS_VERIFIED, VERIFICATION_STATUS_FAILED). Must always be a valid field */ - meshtrade.compliance.client.v1.VerificationStatus verification_status = 8 [(buf.validate.field) = { + meshtrade.compliance.client.v1.VerificationStatus verification_status = 9 [(buf.validate.field) = { required: true enum: {defined_only: true} }]; @@ -100,7 +115,7 @@ message Client { `verification_status`. This provides an audit trail for status changes. System set when verification_status changes. */ - string verification_authority = 9 [(buf.validate.field) = { + string verification_authority = 10 [(buf.validate.field) = { cel: { id: "verification_authority.format.optional" message: "verification_authority must be empty or in the format clients/{ULIDv2}" @@ -113,12 +128,12 @@ message Client { state, specifically `VERIFICATION_STATUS_VERIFIED`. System set when verification_status changes to VERIFICATION_STATUS_VERIFIED. */ - google.protobuf.Timestamp verification_date = 10; + google.protobuf.Timestamp verification_date = 11; /* The timestamp indicating when the client's next periodic compliance review is due. This field drives re-verification workflows. Optional for Verification. */ - google.protobuf.Timestamp next_verification_date = 11; + google.protobuf.Timestamp next_verification_date = 12; } diff --git a/proto/meshtrade/iam/api_user/v1/api_user.proto b/proto/meshtrade/iam/api_user/v1/api_user.proto index fc4b3b3d..141cd058 100644 --- a/proto/meshtrade/iam/api_user/v1/api_user.proto +++ b/proto/meshtrade/iam/api_user/v1/api_user.proto @@ -6,6 +6,7 @@ import "buf/validate/validate.proto"; option go_package = "github.com/meshtrade/api/go/iam/api_user/v1;api_user_v1"; option java_package = "co.meshtrade.api.iam.api_user.v1"; +option java_outer_classname = "ApiUserOuterClass"; /* Represents an API user for automated authentication and authorization. @@ -42,11 +43,26 @@ message APIUser { } }]; + /* + Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + System set on creation. + */ + repeated string owners = 3 [(buf.validate.field) = { + repeated: { + items: { + string: { + len: 33 + pattern: "^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" + } + } + } + }]; + /* A non-unique, user-provided name for the API user, used for display purposes. Required on creation. */ - string display_name = 3 [(buf.validate.field) = { + string display_name = 4 [(buf.validate.field) = { required: true string: { min_len: 1 @@ -63,7 +79,7 @@ message APIUser { The current state of the API user (active or inactive). System set on creation to default value of inactive. */ - meshtrade.iam.api_user.v1.APIUserState state = 4 [(buf.validate.field) = { + meshtrade.iam.api_user.v1.APIUserState state = 5 [(buf.validate.field) = { enum: {defined_only: true} cel: { id: "state.valid" @@ -77,7 +93,7 @@ message APIUser { prepended by the name of the group in which they have been assigned that role. e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum. */ - repeated string roles = 5 [(buf.validate.field) = { + repeated string roles = 6 [(buf.validate.field) = { repeated: { items: { string: { @@ -94,7 +110,7 @@ message APIUser { This field is only populated on the entity the first time it is returned after creation - it is NOT stored. Populated once by system on creation. */ - string api_key = 6; + string api_key = 7; } enum APIUserState { diff --git a/proto/meshtrade/iam/group/v1/group.proto b/proto/meshtrade/iam/group/v1/group.proto index 9c29fb24..2481bb11 100644 --- a/proto/meshtrade/iam/group/v1/group.proto +++ b/proto/meshtrade/iam/group/v1/group.proto @@ -43,6 +43,21 @@ message Group { } }]; + /* + Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + System set on creation. + */ + repeated string owners = 3 [(buf.validate.field) = { + repeated: { + items: { + string: { + len: 33 + pattern: "^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" + } + } + } + }]; + /* Human-readable name for organizational identification and display. User-configurable and non-unique across the system. diff --git a/proto/meshtrade/iam/user/v1/user.proto b/proto/meshtrade/iam/user/v1/user.proto index 428f2a79..6de0d780 100644 --- a/proto/meshtrade/iam/user/v1/user.proto +++ b/proto/meshtrade/iam/user/v1/user.proto @@ -41,11 +41,26 @@ message User { } }]; + /* + Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + System set on creation. + */ + repeated string owners = 3 [(buf.validate.field) = { + repeated: { + items: { + string: { + len: 33 + pattern: "^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" + } + } + } + }]; + /* The unique email address of this user. This field is required on creation and must be a valid email format. */ - string email = 3 [(buf.validate.field) = { + string email = 4 [(buf.validate.field) = { required: true string: {email: true} }]; @@ -55,7 +70,7 @@ message User { prepended by the name of the group in which they have been assigned that role. e.g. groups/{ULIDv2}/roles/{role}, where role is a value of the meshtrade.iam.role.v1.Role enum. */ - repeated string roles = 4 [(buf.validate.field) = { + repeated string roles = 5 [(buf.validate.field) = { repeated: { items: { string: { diff --git a/proto/meshtrade/studio/instrument/v1/instrument.proto b/proto/meshtrade/studio/instrument/v1/instrument.proto index c4dd78ed..f6ba3317 100644 --- a/proto/meshtrade/studio/instrument/v1/instrument.proto +++ b/proto/meshtrade/studio/instrument/v1/instrument.proto @@ -37,6 +37,21 @@ message Instrument { } }]; + /* + Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + System set on creation. + */ + repeated string owners = 3 [(buf.validate.field) = { + repeated: { + items: { + string: { + len: 33 + pattern: "^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" + } + } + } + }]; + /* Human-readable name for organizational identification and display. User-configurable and non-unique across the system. diff --git a/proto/meshtrade/trading/limit_order/v1/limit_order.proto b/proto/meshtrade/trading/limit_order/v1/limit_order.proto index e8f13e35..3387defb 100644 --- a/proto/meshtrade/trading/limit_order/v1/limit_order.proto +++ b/proto/meshtrade/trading/limit_order/v1/limit_order.proto @@ -47,12 +47,27 @@ message LimitOrder { } }]; + /* + Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + System set on creation. + */ + repeated string owners = 3 [(buf.validate.field) = { + repeated: { + items: { + string: { + len: 33 + pattern: "^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" + } + } + } + }]; + /* The account associated with this limit order. Format: accounts/{ULIDv2}. This field is required on creation. */ - string account = 3 [(buf.validate.field) = { + string account = 5 [(buf.validate.field) = { required: true string: { len: 35 @@ -68,7 +83,7 @@ message LimitOrder { this owner. The Mesh system enforces this constraint to ensure reliable lookups via GetLimitOrderByExternalReference. */ - string external_reference = 5 [(buf.validate.field) = { + string external_reference = 6 [(buf.validate.field) = { string: {max_len: 200} }]; @@ -76,7 +91,7 @@ message LimitOrder { Order side indicating buy or sell. This field is required on creation. */ - LimitOrderSide side = 6 [(buf.validate.field) = { + LimitOrderSide side = 7 [(buf.validate.field) = { enum: { defined_only: true not_in: [0] @@ -87,13 +102,13 @@ message LimitOrder { Limit price for the order. This field is required on creation. */ - meshtrade.type.v1.Amount limit_price = 7 [(buf.validate.field) = {required: true}]; + meshtrade.type.v1.Amount limit_price = 8 [(buf.validate.field) = {required: true}]; /* Order quantity. This field is required on creation. */ - meshtrade.type.v1.Amount quantity = 8 [(buf.validate.field) = {required: true}]; + meshtrade.type.v1.Amount quantity = 9 [(buf.validate.field) = {required: true}]; /* Fill price from live ledger data. @@ -103,7 +118,7 @@ message LimitOrder { Only populated when live_ledger_data=true in request. */ - meshtrade.type.v1.Amount fill_price = 9; + meshtrade.type.v1.Amount fill_price = 10; /* Filled quantity from live ledger data. @@ -113,13 +128,13 @@ message LimitOrder { Only populated when live_ledger_data=true in request. */ - meshtrade.type.v1.Amount filled_quantity = 10; + meshtrade.type.v1.Amount filled_quantity = 11; /* Order status from live ledger data. Only populated when live_ledger_data=true in request. */ - LimitOrderStatus status = 11; + LimitOrderStatus status = 12; } /* diff --git a/proto/meshtrade/wallet/account/v1/account.proto b/proto/meshtrade/wallet/account/v1/account.proto index 8a5e56b9..e0cfd259 100644 --- a/proto/meshtrade/wallet/account/v1/account.proto +++ b/proto/meshtrade/wallet/account/v1/account.proto @@ -48,6 +48,21 @@ message Account { } }]; + /* + Ownership hiearchy of groups that have access to this resource in the format groups/{group_id}. + System set on creation. + */ + repeated string owners = 3 [(buf.validate.field) = { + repeated: { + items: { + string: { + len: 33 + pattern: "^groups/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" + } + } + } + }]; + /* The Unique Mesh Account Number for simplified account identification. Format: 7-digit number starting with 1 (e.g., 1234567). diff --git a/tool/protoc-gen-mesh_ts_node/src/index.ts b/tool/protoc-gen-mesh_ts_node/src/index.ts index cbabbda2..67aaa8ce 100644 --- a/tool/protoc-gen-mesh_ts_node/src/index.ts +++ b/tool/protoc-gen-mesh_ts_node/src/index.ts @@ -45,55 +45,88 @@ function generateConnectClientManually(schema: Schema, file: DescFile) { } // Import request/response types - // Separate types defined in service_pb from those in other files + // Collect types defined in service_pb const serviceTypes = new Set(); - const externalTypes = new Set(); + const requestTypes = new Set(); // Track request types separately for schema imports for (const service of file.services) { for (const method of service.methods) { - // Request types are typically defined in service.proto + // Request types defined in this service file if (method.input.file === file) { serviceTypes.add(method.input.name); - } else { - externalTypes.add(method.input.name); + requestTypes.add(method.input.name); // Track for schema import } - // Response types may be defined in different files + // Response types defined in this service file if (method.output.file === file) { serviceTypes.add(method.output.name); - } else { - externalTypes.add(method.output.name); + // Note: Response types don't need schemas - we only validate requests } } } // Import types from service_pb (request/response messages defined in the service file) + // Also import the Schema types for request types only (used for validation) if (serviceTypes.size > 0) { const sortedServiceTypes = Array.from(serviceTypes).sort(); + const importsWithSchemas: string[] = []; + for (const type of sortedServiceTypes) { + importsWithSchemas.push(type); + // Only import Schema for request types, not response types + if (requestTypes.has(type)) { + importsWithSchemas.push(`${type}Schema`); + } + } content += `import {\n`; - for (let i = 0; i < sortedServiceTypes.length; i++) { - content += ` ${sortedServiceTypes[i]}${i < sortedServiceTypes.length - 1 ? ',' : ''}\n`; + for (let i = 0; i < importsWithSchemas.length; i++) { + content += ` ${importsWithSchemas[i]}${i < importsWithSchemas.length - 1 ? "," : ""}\n`; } content += `} from "./service_pb";\n`; } - // Import types from their respective files (e.g., APIUser from api_user_pb) - if (externalTypes.size > 0) { - const sortedExternalTypes = Array.from(externalTypes).sort(); - for (const typeName of sortedExternalTypes) { - // Generate import based on the file naming pattern - // Convert "APIUser" -> "api_user_pb" - const importPath = `./${convertToSnakeCase(typeName)}_pb`; - content += `import { ${typeName} } from "${importPath}";\n`; + // Import types from their respective files + // Group by source file to avoid duplicate imports + const externalTypesByFile = new Map>(); + + for (const service of file.services) { + for (const method of service.methods) { + // Check input type + if (method.input.file !== file && !serviceTypes.has(method.input.name)) { + const fileName = method.input.file.name; + if (!externalTypesByFile.has(fileName)) { + externalTypesByFile.set(fileName, new Set()); + } + externalTypesByFile.get(fileName)!.add(method.input.name); + } + + // Check output type + if ( + method.output.file !== file && + !serviceTypes.has(method.output.name) + ) { + const fileName = method.output.file.name; + if (!externalTypesByFile.has(fileName)) { + externalTypesByFile.set(fileName, new Set()); + } + externalTypesByFile.get(fileName)!.add(method.output.name); + } } } - // Generate imports for common utilities with dynamic relative paths + // Generate imports for external types + for (const [sourceFileName, types] of externalTypesByFile) { + const sortedTypes = Array.from(types).sort(); + // Calculate relative path from current file to the external file + const relativePath = calculateRelativeImportPath(file.name, sourceFileName); + content += `import { ${sortedTypes.join(", ")} } from "${relativePath}";\n`; + } + + // Generate imports for utilities with dynamic relative paths const outputFilePath = getOutputFilePath(file); - const relativePathToCommon = getRelativePathToCommon(outputFilePath); - content += `import { ConfigOpts, getConfigFromOpts } from "${relativePathToCommon}/config";\n`; - content += `import { validateRequest } from "${relativePathToCommon}/validation";\n`; - content += `import { createGroupInterceptor, createApiKeyInterceptor, createJwtInterceptor } from "${relativePathToCommon}/connectInterceptors";\n`; + const relativePathToMeshtrade = getRelativePathToMeshtrade(outputFilePath); + content += `import { ClientOption, ClientConfig, buildConfigFromOptions, WithAPIKey, WithJWTAccessToken, WithGroup, WithServerUrl } from "${relativePathToMeshtrade}/config";\n`; + content += `import { createValidator } from "@bufbuild/protovalidate";\n`; + content += `import { createGroupInterceptor, createApiKeyInterceptor, createJwtInterceptor, createLoggingInterceptor } from "${relativePathToMeshtrade}/interceptors";\n`; content += `\n`; // Generate client class for each service @@ -105,6 +138,33 @@ function generateConnectClientManually(schema: Schema, file: DescFile) { writeTypescriptFile(outputFilePath, content); } +/** + * Calculate the relative import path from one proto file to another. + * + * Example: + * - From: "meshtrade/iam/api_user/v1/service" + * To: "meshtrade/iam/api_user/v1/api_user" + * -> Result: "./api_user_pb" + */ +function calculateRelativeImportPath(fromFile: string, toFile: string): string { + const fromDir = path.dirname(fromFile); + const toDir = path.dirname(toFile); + const toBasename = path.basename(toFile); + + // Calculate relative path from fromDir to toDir + const relativePath = path.relative(fromDir, toDir); + + // Construct the import path with _pb suffix + const importPath = path.join(relativePath, toBasename + "_pb"); + + // Ensure it starts with ./ or ../ + if (!importPath.startsWith(".")) { + return "./" + importPath; + } + + return importPath; +} + function getOutputFilePath(file: DescFile): string { // Convert protobuf file path to TypeScript Node output path // Example: "meshtrade/iam/api_user/v1/service.proto" -> "ts-node/src/meshtrade/iam/api_user/v1/service_node_meshts.ts" @@ -114,13 +174,13 @@ function getOutputFilePath(file: DescFile): string { return path.join(outputDir, fileName); } -function getRelativePathToCommon(outputFilePath: string): string { - // Calculate the relative path from the generated file to the common directory +function getRelativePathToMeshtrade(outputFilePath: string): string { + // Calculate the relative path from the generated file to the ts-node/src/meshtrade root // Example: from "ts-node/src/meshtrade/iam/api_user/v1/service_node_meshts.ts" - // to "ts-node/src/meshtrade/common/" returns "../../../common" + // to "ts-node/src/meshtrade/" returns "../../../" const generatedFileDir = path.dirname(outputFilePath); - const commonDir = path.join("ts-node", "src", "meshtrade", "common"); - return path.relative(generatedFileDir, commonDir); + const meshtradeDir = path.join("ts-node", "src", "meshtrade"); + return path.relative(generatedFileDir, meshtradeDir); } function writeTypescriptFile(filePath: string, content: string): void { @@ -130,7 +190,7 @@ function writeTypescriptFile(filePath: string, content: string): void { fs.mkdirSync(dir, { recursive: true }); // Write the TypeScript content to the file - fs.writeFileSync(filePath, content, 'utf8'); + fs.writeFileSync(filePath, content, "utf8"); console.error(`Generated TypeScript Connect client: ${filePath}`); } catch (error) { @@ -139,81 +199,121 @@ function writeTypescriptFile(filePath: string, content: string): void { } } -function generateServiceClientString(service: DescService, file: DescFile): string { +function generateServiceClientString( + service: DescService, + file: DescFile, +): string { const serviceName = service.name; const clientClassName = `${serviceName}Node`; // Extract resource name from the service (e.g., ApiUser from ApiUserService) - const resourceName = serviceName.replace(/Service$/, ''); + const resourceName = serviceName.replace(/Service$/, ""); let content = ""; // Generate class JSDoc content += "/**\n"; content += ` * Node.js client for interacting with the ${file.proto.package} ${toReadableResourceName(resourceName)} v1 API resource service.\n`; - content += " * Uses Connect-ES with gRPC transport for Node.js gRPC communication.\n"; + content += + " * Uses Connect-ES with gRPC transport for Node.js gRPC communication.\n"; content += " *\n"; - content += " * Supports three authentication modes:\n"; + content += + " * Supports flexible authentication modes using functional options pattern:\n"; content += " *\n"; content += " * 1. **No Authentication** (public APIs):\n"; content += " * ```typescript\n"; - content += ` * const client = new ${clientClassName}({ apiServerURL: \"http://localhost:10000\" });\n`; + content += ` * const client = new ${clientClassName}(\n`; + content += ' * WithServerUrl("http://localhost:10000")\n'; + content += " * );\n"; content += " * ```\n"; content += " *\n"; content += " * 2. **API Key Authentication** (backend services):\n"; content += " * ```typescript\n"; - content += ` * const client = new ${clientClassName}({\n`; - content += " * apiServerURL: \"https://api.example.com\",\n"; - content += " * apiKey: \"your-api-key\",\n"; - content += " * group: \"groups/01ARZ3NDEKTSV4YWVF8F5BH32\"\n"; - content += " * });\n"; + content += ` * const client = new ${clientClassName}(\n`; + content += ' * WithAPIKey("your-api-key"),\n'; + content += ' * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"),\n'; + content += ' * WithServerUrl("https://api.example.com")\n'; + content += " * );\n"; content += " * ```\n"; content += " *\n"; - content += " * 3. **JWT Token Authentication** (Next.js backend with user session):\n"; + content += + " * 3. **JWT Token Authentication** (Next.js backend with user session):\n"; content += " * ```typescript\n"; - content += ` * const client = new ${clientClassName}({\n`; - content += " * apiServerURL: \"https://api.example.com\",\n"; - content += " * jwtToken: \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\"\n"; - content += " * });\n"; + content += ` * const client = new ${clientClassName}(\n`; + content += + ' * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."),\n'; + content += ' * WithServerUrl("https://api.example.com")\n'; + content += " * );\n"; content += " * ```\n"; + content += " *\n"; + content += + " * 4. **JWT with Group Context** (user session with specific group):\n"; + content += " * ```typescript\n"; + content += ` * const client = new ${clientClassName}(\n`; + content += + ' * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."),\n'; + content += ' * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"),\n'; + content += ' * WithServerUrl("https://api.example.com")\n'; + content += " * );\n"; + content += " * ```\n"; + content += " *\n"; + content += " * Available options:\n"; + content += + " * - `WithAPIKey(key)` - API key authentication (mutually exclusive with JWT)\n"; + content += + " * - `WithJWTAccessToken(token)` - JWT authentication (mutually exclusive with API key)\n"; + content += + " * - `WithGroup(group)` - Group context (optional, works with both auth modes)\n"; + content += + " * - `WithServerUrl(url)` - Custom server URL (optional, defaults to production)\n"; content += " */\n"; // Generate class declaration content += `export class ${clientClassName} {\n`; content += ` private _client: ConnectClient;\n`; - content += ` private readonly _config: ReturnType;\n`; + content += ` private readonly _config: ClientConfig;\n`; content += ` private readonly _interceptors: Interceptor[];\n`; + content += ` private readonly _validator: ReturnType;\n`; content += "\n"; // Generate constructor content += " /**\n"; content += ` * Constructs an instance of ${clientClassName}.\n`; - content += " * @param {ConfigOpts} [config] - Optional configuration for the client.\n"; - content += " * @param {Interceptor[]} [interceptors] - For internal use by \`withGroup\`.\n"; + content += " *\n"; + content += + " * Uses functional options pattern for flexible configuration:\n"; + content += " * - `WithAPIKey(key)` - API key authentication\n"; + content += " * - `WithJWTAccessToken(token)` - JWT authentication\n"; + content += " * - `WithGroup(group)` - Group context (optional)\n"; + content += " * - `WithServerUrl(url)` - Custom server URL (optional)\n"; + content += " *\n"; + content += + " * @param {...ClientOption} opts - Variable number of configuration options\n"; content += " */\n"; - content += " constructor(config?: ConfigOpts, interceptors?: Interceptor[]) {\n"; - content += " this._config = getConfigFromOpts(config);\n"; + content += " constructor(...opts: ClientOption[]) {\n"; + content += " // Build configuration from options\n"; + content += " this._config = buildConfigFromOptions(...opts);\n"; + content += "\n"; + content += " // Initialize validator for request validation\n"; + content += " this._validator = createValidator();\n"; content += "\n"; - content += " // If interceptors are provided (from withGroup), use them\n"; - content += " // Otherwise, create auth interceptors based on config\n"; - content += " if (interceptors) {\n"; - content += " this._interceptors = interceptors;\n"; - content += " } else {\n"; - content += " this._interceptors = [];\n"; + content += " this._interceptors = [];\n"; content += "\n"; - content += " // Add authentication interceptor based on configuration\n"; - content += " if (this._config.apiKey && this._config.group) {\n"; - content += " // API Key authentication mode\n"; - content += " this._interceptors.push(\n"; - content += " createApiKeyInterceptor(this._config.apiKey, this._config.group)\n"; - content += " );\n"; - content += " } else if (this._config.jwtToken) {\n"; - content += " // JWT authentication mode\n"; - content += " this._interceptors.push(\n"; - content += " createJwtInterceptor(this._config.jwtToken)\n"; - content += " );\n"; - content += " }\n"; - content += " // If neither is configured, no authentication (public API mode)\n"; + content += " this._interceptors.push(createLoggingInterceptor());\n"; + content += "\n"; + content += " if (this._config.apiKey) {\n"; + content += + " this._interceptors.push(createApiKeyInterceptor(this._config.apiKey));\n"; + content += " }\n"; + content += "\n"; + content += " if (this._config.jwtToken) {\n"; + content += + " this._interceptors.push(createJwtInterceptor(this._config.jwtToken));\n"; + content += " }\n"; + content += "\n"; + content += " if (this._config.group) {\n"; + content += + " this._interceptors.push(createGroupInterceptor(this._config.group));\n"; content += " }\n"; content += "\n"; content += " // Create the gRPC transport for Node.js with interceptors\n"; @@ -230,48 +330,52 @@ function generateServiceClientString(service: DescService, file: DescFile): stri // Generate withGroup method content += " /**\n"; - content += " * Returns a new client instance configured to send the specified group\n"; - content += " * resource name in the request headers for subsequent API calls.\n"; + content += + " * Returns a new client instance configured to send the specified group\n"; + content += + " * resource name in the request headers for subsequent API calls.\n"; content += " *\n"; - content += " * **Important**: This method only works with API key authentication.\n"; - content += " * - For **API key auth**: Creates a new client with updated group context\n"; - content += " * - For **JWT auth**: Throws error (group comes from JWT token claims)\n"; - content += " * - For **no auth**: Throws error (group requires authentication)\n"; + content += + " * This method creates a new client with the same authentication configuration\n"; + content += + " * but with the group context updated to the specified value.\n"; + content += " *\n"; + content += " * **Compatibility**: Works with all authentication modes:\n"; + content += + " * - **API key auth**: Creates new client with API key + new group\n"; + content += " * - **JWT auth**: Creates new client with JWT + new group\n"; + content += + " * - **No auth**: Creates new client with standalone group interceptor\n"; content += " * \n"; - content += " * @param {string} group - The operating group context to inject into the request\n"; - content += " * in the format \`groups/{ulid}\` where {ulid} is a 26-character ULID.\n"; - content += " * Example: 'groups/01ARZ3NDEKTSV4YWVF8F5BH32'\n"; + content += + " * @param {string} group - The operating group context to inject into the request\n"; + content += + " * in the format `groups/{ulid}` where {ulid} is a 26-character ULID.\n"; + content += + " * Example: 'groups/01ARZ3NDEKTSV4YWVF8F5BH32'\n"; content += ` * @returns {${clientClassName}} A new, configured instance of the client.\n`; - content += " * @throws {Error} If used with JWT authentication or no authentication\n"; content += " * @throws {Error} If the group format is invalid\n"; content += " */\n"; content += ` withGroup(group: string): ${clientClassName} {\n`; - content += " // Check authentication mode\n"; - content += " if (this._config.jwtToken) {\n"; - content += " throw new Error(\n"; - content += ' "Cannot use withGroup() with JWT authentication. " +\n'; - content += ' "The group context is determined by the JWT token claims."\n'; - content += " );\n"; - content += " }\n"; + content += + " // Build new options array with existing auth and updated group\n"; + content += " const newOpts: ClientOption[] = [];\n"; content += "\n"; - content += " if (!this._config.apiKey) {\n"; - content += " throw new Error(\n"; - content += ' "Cannot use withGroup() without authentication. " +\n'; - content += ' "Please configure API key authentication to use group context."\n'; - content += " );\n"; + content += " // Add server URL\n"; + content += " newOpts.push(WithServerUrl(this._config.apiServerURL));\n"; + content += "\n"; + content += " // Add authentication (preserve existing mode)\n"; + content += " if (this._config.apiKey) {\n"; + content += " newOpts.push(WithAPIKey(this._config.apiKey));\n"; + content += " } else if (this._config.jwtToken) {\n"; + content += " newOpts.push(WithJWTAccessToken(this._config.jwtToken));\n"; content += " }\n"; content += "\n"; - content += " // For API key authentication, create new client with updated group\n"; - content += " // Replace the existing API key interceptor with one that has the new group\n"; - content += " const newInterceptors = [\n"; - content += " createApiKeyInterceptor(this._config.apiKey, group)\n"; - content += " ];\n"; + content += " // Add the new group\n"; + content += " newOpts.push(WithGroup(group));\n"; content += "\n"; - content += " // Return a new client instance with updated group context\n"; - content += ` return new ${clientClassName}(\n`; - content += " this._config,\n"; - content += " newInterceptors,\n"; - content += " );\n"; + content += " // Return a new client instance with updated configuration\n"; + content += ` return new ${clientClassName}(...newOpts);\n`; content += " }\n"; content += "\n"; @@ -290,7 +394,7 @@ function generateStreamingMethodString( methodName: string, requestType: string, responseType: string, - resourceName: string + resourceName: string, ): string { let content = ""; @@ -313,7 +417,13 @@ function generateStreamingMethodString( // Generate method signature and implementation with validation content += ` ${methodName}(request: ${requestType}): AsyncIterable<${responseType}> {\n`; content += " // Validate request before initiating stream\n"; - content += " validateRequest(request);\n"; + content += ` const result = this._validator.validate(${requestType}Schema, request);\n`; + content += " if (result.kind === \"invalid\") {\n"; + content += " const violations = result.violations.map(v => `${v.field.toString()}: ${v.message}`).join(\"; \");\n"; + content += " throw new Error(`Validation failed: ${violations}`);\n"; + content += " } else if (result.kind === \"error\") {\n"; + content += " throw result.error;\n"; + content += " }\n"; content += "\n"; content += ` return this._client.${methodName}(request);\n`; content += " }\n"; @@ -322,7 +432,11 @@ function generateStreamingMethodString( return content; } -function generateServiceMethodString(method: DescMethod, service: DescService, resourceName: string): string { +function generateServiceMethodString( + method: DescMethod, + service: DescService, + resourceName: string, +): string { const methodName = camelCase(method.name); const requestType = method.input.name; const responseType = method.output.name; @@ -333,7 +447,13 @@ function generateServiceMethodString(method: DescMethod, service: DescService, r let content = ""; if (isServerStreaming) { - return generateStreamingMethodString(method, methodName, requestType, responseType, resourceName); + return generateStreamingMethodString( + method, + methodName, + requestType, + responseType, + resourceName, + ); } // Generate method JSDoc @@ -346,7 +466,13 @@ function generateServiceMethodString(method: DescMethod, service: DescService, r // Generate method signature and implementation content += ` ${methodName}(request: ${requestType}): Promise<${responseType}> {\n`; content += " // Validate request\n"; - content += " validateRequest(request);\n"; + content += ` const result = this._validator.validate(${requestType}Schema, request);\n`; + content += " if (result.kind === \"invalid\") {\n"; + content += " const violations = result.violations.map(v => `${v.field.toString()}: ${v.message}`).join(\"; \");\n"; + content += " throw new Error(`Validation failed: ${violations}`);\n"; + content += " } else if (result.kind === \"error\") {\n"; + content += " throw result.error;\n"; + content += " }\n"; content += "\n"; content += ` return this._client.${methodName}(request);\n`; content += " }\n"; @@ -359,39 +485,45 @@ function camelCase(str: string): string { return str.charAt(0).toLowerCase() + str.slice(1); } -function getMethodDescription(methodName: string, resourceName: string): string { +function getMethodDescription( + methodName: string, + resourceName: string, +): string { const method = methodName.toLowerCase(); const resource = toReadableResourceName(resourceName); - if (method.startsWith('get')) { + if (method.startsWith("get")) { return `Retrieves ${getArticle(resource)} ${resource}.`; - } else if (method.startsWith('create')) { + } else if (method.startsWith("create")) { return `Creates a new ${resource}.`; - } else if (method.startsWith('update')) { + } else if (method.startsWith("update")) { return `Updates an existing ${resource}.`; - } else if (method.startsWith('delete')) { + } else if (method.startsWith("delete")) { return `Deletes ${getArticle(resource)} ${resource}.`; - } else if (method.startsWith('list')) { + } else if (method.startsWith("list")) { return `Retrieves a list of ${resource}s.`; - } else if (method.startsWith('search')) { + } else if (method.startsWith("search")) { return `Searches for ${resource}s.`; - } else if (method.startsWith('activate')) { + } else if (method.startsWith("activate")) { return `Activates ${getArticle(resource)} ${resource}.`; - } else if (method.startsWith('deactivate')) { + } else if (method.startsWith("deactivate")) { return `Deactivates ${getArticle(resource)} ${resource}.`; } else { return `Performs ${method} operation on ${resource}.`; } } -function getMethodReturnDescription(methodName: string, resourceName: string): string { +function getMethodReturnDescription( + methodName: string, + resourceName: string, +): string { const method = methodName.toLowerCase(); const resource = toReadableResourceName(resourceName); - if (method.startsWith('list')) { + if (method.startsWith("list")) { return `list of ${resource}s`; - } else if (method.startsWith('search')) { - return 'search results'; + } else if (method.startsWith("search")) { + return "search results"; } else { return resource; } @@ -400,21 +532,13 @@ function getMethodReturnDescription(methodName: string, resourceName: string): s function toReadableResourceName(resourceName: string): string { // Convert PascalCase to readable format, e.g., "ApiUser" -> "API user" return resourceName - .replace(/([A-Z])([a-z])/g, '$1$2') // Add space before capital followed by lowercase - .replace(/([a-z])([A-Z])/g, '$1 $2') // Add space between lowercase and capital + .replace(/([A-Z])([a-z])/g, "$1$2") // Add space before capital followed by lowercase + .replace(/([a-z])([A-Z])/g, "$1 $2") // Add space between lowercase and capital .toLowerCase(); } function getArticle(word: string): string { // Return appropriate article (a/an) based on first letter const firstLetter = word.charAt(0).toLowerCase(); - return ['a', 'e', 'i', 'o', 'u'].includes(firstLetter) ? 'an' : 'a'; -} - -function convertToSnakeCase(str: string): string { - // Convert PascalCase to snake_case: "APIUser" -> "api_user" - return str - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') // Handle sequences like "API" -> "API_" - .replace(/([a-z\d])([A-Z])/g, '$1_$2') // Handle transitions like "aB" -> "a_B" - .toLowerCase(); + return ["a", "e", "i", "o", "u"].includes(firstLetter) ? "an" : "a"; } diff --git a/tool/protoc-gen-mesh_ts_web/src/index.ts b/tool/protoc-gen-mesh_ts_web/src/index.ts index fdf7faad..fcf3e660 100644 --- a/tool/protoc-gen-mesh_ts_web/src/index.ts +++ b/tool/protoc-gen-mesh_ts_web/src/index.ts @@ -34,7 +34,7 @@ function generateConnectClientManually(schema: Schema, file: DescFile) { content += `/* eslint-disable */\n`; content += `\n`; - // Generate imports for Connect-ES + // Generate imports for Connect-ES (Web) content += `import { createClient, type Client as ConnectClient, Interceptor } from "@connectrpc/connect";\n`; content += `import { createGrpcWebTransport } from "@connectrpc/connect-web";\n`; @@ -45,55 +45,88 @@ function generateConnectClientManually(schema: Schema, file: DescFile) { } // Import request/response types - // Separate types defined in service_pb from those in other files + // Collect types defined in service_pb const serviceTypes = new Set(); - const externalTypes = new Set(); + const requestTypes = new Set(); // Track request types separately for schema imports for (const service of file.services) { for (const method of service.methods) { - // Request types are typically defined in service.proto + // Request types defined in this service file if (method.input.file === file) { serviceTypes.add(method.input.name); - } else { - externalTypes.add(method.input.name); + requestTypes.add(method.input.name); // Track for schema import } - // Response types may be defined in different files + // Response types defined in this service file if (method.output.file === file) { serviceTypes.add(method.output.name); - } else { - externalTypes.add(method.output.name); + // Note: Response types don't need schemas - we only validate requests } } } // Import types from service_pb (request/response messages defined in the service file) + // Also import the Schema types for request types only (used for validation) if (serviceTypes.size > 0) { const sortedServiceTypes = Array.from(serviceTypes).sort(); + const importsWithSchemas: string[] = []; + for (const type of sortedServiceTypes) { + importsWithSchemas.push(type); + // Only import Schema for request types, not response types + if (requestTypes.has(type)) { + importsWithSchemas.push(`${type}Schema`); + } + } content += `import {\n`; - for (let i = 0; i < sortedServiceTypes.length; i++) { - content += ` ${sortedServiceTypes[i]}${i < sortedServiceTypes.length - 1 ? ',' : ''}\n`; + for (let i = 0; i < importsWithSchemas.length; i++) { + content += ` ${importsWithSchemas[i]}${i < importsWithSchemas.length - 1 ? "," : ""}\n`; } content += `} from "./service_pb";\n`; } - // Import types from their respective files (e.g., APIUser from api_user_pb) - if (externalTypes.size > 0) { - const sortedExternalTypes = Array.from(externalTypes).sort(); - for (const typeName of sortedExternalTypes) { - // Generate import based on the file naming pattern - // Convert "APIUser" -> "api_user_pb" - const importPath = `./${convertToSnakeCase(typeName)}_pb`; - content += `import { ${typeName} } from "${importPath}";\n`; + // Import types from their respective files + // Group by source file to avoid duplicate imports + const externalTypesByFile = new Map>(); + + for (const service of file.services) { + for (const method of service.methods) { + // Check input type + if (method.input.file !== file && !serviceTypes.has(method.input.name)) { + const fileName = method.input.file.name; + if (!externalTypesByFile.has(fileName)) { + externalTypesByFile.set(fileName, new Set()); + } + externalTypesByFile.get(fileName)!.add(method.input.name); + } + + // Check output type + if ( + method.output.file !== file && + !serviceTypes.has(method.output.name) + ) { + const fileName = method.output.file.name; + if (!externalTypesByFile.has(fileName)) { + externalTypesByFile.set(fileName, new Set()); + } + externalTypesByFile.get(fileName)!.add(method.output.name); + } } } - // Generate imports for common utilities with dynamic relative paths + // Generate imports for external types + for (const [sourceFileName, types] of externalTypesByFile) { + const sortedTypes = Array.from(types).sort(); + // Calculate relative path from current file to the external file + const relativePath = calculateRelativeImportPath(file.name, sourceFileName); + content += `import { ${sortedTypes.join(", ")} } from "${relativePath}";\n`; + } + + // Generate imports for utilities with dynamic relative paths const outputFilePath = getOutputFilePath(file); - const relativePathToCommon = getRelativePathToCommon(outputFilePath); - content += `import { ConfigOpts, getConfigFromOpts } from "${relativePathToCommon}/config";\n`; - content += `import { validateRequest } from "${relativePathToCommon}/validation";\n`; - content += `import { createGroupInterceptor } from "${relativePathToCommon}/connectInterceptors";\n`; + const relativePathToMeshtrade = getRelativePathToMeshtrade(outputFilePath); + content += `import { ClientOption, ClientConfig, buildConfigFromOptions, WithAPIKey, WithJWTAccessToken, WithGroup, WithServerUrl } from "${relativePathToMeshtrade}/config";\n`; + content += `import { createValidator } from "@bufbuild/protovalidate";\n`; + content += `import { createGroupInterceptor, createApiKeyInterceptor, createJwtInterceptor, createLoggingInterceptor } from "${relativePathToMeshtrade}/interceptors";\n`; content += `\n`; // Generate client class for each service @@ -105,6 +138,33 @@ function generateConnectClientManually(schema: Schema, file: DescFile) { writeTypescriptFile(outputFilePath, content); } +/** + * Calculate the relative import path from one proto file to another. + * + * Example: + * - From: "meshtrade/iam/api_user/v1/service" + * To: "meshtrade/iam/api_user/v1/api_user" + * -> Result: "./api_user_pb" + */ +function calculateRelativeImportPath(fromFile: string, toFile: string): string { + const fromDir = path.dirname(fromFile); + const toDir = path.dirname(toFile); + const toBasename = path.basename(toFile); + + // Calculate relative path from fromDir to toDir + const relativePath = path.relative(fromDir, toDir); + + // Construct the import path with _pb suffix + const importPath = path.join(relativePath, toBasename + "_pb"); + + // Ensure it starts with ./ or ../ + if (!importPath.startsWith(".")) { + return "./" + importPath; + } + + return importPath; +} + function getOutputFilePath(file: DescFile): string { // Convert protobuf file path to TypeScript Web output path // Example: "meshtrade/iam/api_user/v1/service.proto" -> "ts-web/src/meshtrade/iam/api_user/v1/service_web_meshts.ts" @@ -114,13 +174,13 @@ function getOutputFilePath(file: DescFile): string { return path.join(outputDir, fileName); } -function getRelativePathToCommon(outputFilePath: string): string { - // Calculate the relative path from the generated file to the common directory +function getRelativePathToMeshtrade(outputFilePath: string): string { + // Calculate the relative path from the generated file to the ts-web/src/meshtrade root // Example: from "ts-web/src/meshtrade/iam/api_user/v1/service_web_meshts.ts" - // to "ts-web/src/meshtrade/common/" returns "../../../common" + // to "ts-web/src/meshtrade/" returns "../../../" const generatedFileDir = path.dirname(outputFilePath); - const commonDir = path.join("ts-web", "src", "meshtrade", "common"); - return path.relative(generatedFileDir, commonDir); + const meshtradeDir = path.join("ts-web", "src", "meshtrade"); + return path.relative(generatedFileDir, meshtradeDir); } function writeTypescriptFile(filePath: string, content: string): void { @@ -130,7 +190,7 @@ function writeTypescriptFile(filePath: string, content: string): void { fs.mkdirSync(dir, { recursive: true }); // Write the TypeScript content to the file - fs.writeFileSync(filePath, content, 'utf8'); + fs.writeFileSync(filePath, content, "utf8"); console.error(`Generated TypeScript Connect client: ${filePath}`); } catch (error) { @@ -139,44 +199,127 @@ function writeTypescriptFile(filePath: string, content: string): void { } } -function generateServiceClientString(service: DescService, file: DescFile): string { +function generateServiceClientString( + service: DescService, + file: DescFile, +): string { const serviceName = service.name; const clientClassName = `${serviceName}Web`; // Extract resource name from the service (e.g., ApiUser from ApiUserService) - const resourceName = serviceName.replace(/Service$/, ''); + const resourceName = serviceName.replace(/Service$/, ""); let content = ""; // Generate class JSDoc content += "/**\n"; content += ` * Web client for interacting with the ${file.proto.package} ${toReadableResourceName(resourceName)} v1 API resource service.\n`; - content += " * Uses Connect-ES with gRPC-Web transport for browser-compatible gRPC communication.\n"; + content += + " * Uses Connect-ES with gRPC-Web transport for browser-based communication.\n"; + content += " *\n"; + content += + " * Supports flexible authentication modes using functional options pattern:\n"; + content += " *\n"; + content += " * 1. **No Authentication** (public APIs):\n"; + content += " * ```typescript\n"; + content += ` * const client = new ${clientClassName}(\n`; + content += ' * WithServerUrl("http://localhost:10000")\n'; + content += " * );\n"; + content += " * ```\n"; + content += " *\n"; + content += " * 2. **API Key Authentication** (backend services):\n"; + content += " * ```typescript\n"; + content += ` * const client = new ${clientClassName}(\n`; + content += ' * WithAPIKey("your-api-key"),\n'; + content += ' * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"),\n'; + content += ' * WithServerUrl("https://api.example.com")\n'; + content += " * );\n"; + content += " * ```\n"; + content += " *\n"; + content += + " * 3. **JWT Token Authentication** (Next.js frontend with user session):\n"; + content += " * ```typescript\n"; + content += ` * const client = new ${clientClassName}(\n`; + content += + ' * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."),\n'; + content += ' * WithServerUrl("https://api.example.com")\n'; + content += " * );\n"; + content += " * ```\n"; + content += " *\n"; + content += + " * 4. **JWT with Group Context** (user session with specific group):\n"; + content += " * ```typescript\n"; + content += ` * const client = new ${clientClassName}(\n`; + content += + ' * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."),\n'; + content += ' * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"),\n'; + content += ' * WithServerUrl("https://api.example.com")\n'; + content += " * );\n"; + content += " * ```\n"; + content += " *\n"; + content += " * Available options:\n"; + content += + " * - `WithAPIKey(key)` - API key authentication (mutually exclusive with JWT)\n"; + content += + " * - `WithJWTAccessToken(token)` - JWT authentication (mutually exclusive with API key)\n"; + content += + " * - `WithGroup(group)` - Group context (optional, works with both auth modes)\n"; + content += + " * - `WithServerUrl(url)` - Custom server URL (optional, defaults to production)\n"; content += " */\n"; // Generate class declaration content += `export class ${clientClassName} {\n`; content += ` private _client: ConnectClient;\n`; - content += ` private readonly _config: ReturnType;\n`; + content += ` private readonly _config: ClientConfig;\n`; content += ` private readonly _interceptors: Interceptor[];\n`; + content += ` private readonly _validator: ReturnType;\n`; content += "\n"; // Generate constructor content += " /**\n"; content += ` * Constructs an instance of ${clientClassName}.\n`; - content += " * @param {ConfigOpts} [config] - Optional configuration for the client.\n"; - content += " * @param {Interceptor[]} [interceptors] - For internal use by \`withGroup\`.\n"; + content += " *\n"; + content += + " * Uses functional options pattern for flexible configuration:\n"; + content += " * - `WithAPIKey(key)` - API key authentication\n"; + content += " * - `WithJWTAccessToken(token)` - JWT authentication\n"; + content += " * - `WithGroup(group)` - Group context (optional)\n"; + content += " * - `WithServerUrl(url)` - Custom server URL (optional)\n"; + content += " *\n"; + content += + " * @param {...ClientOption} opts - Variable number of configuration options\n"; content += " */\n"; - content += " constructor(config?: ConfigOpts, interceptors?: Interceptor[]) {\n"; - content += " this._config = getConfigFromOpts(config);\n"; - content += " this._interceptors = interceptors || [];\n"; + content += " constructor(...opts: ClientOption[]) {\n"; + content += " // Build configuration from options\n"; + content += " this._config = buildConfigFromOptions(...opts);\n"; + content += "\n"; + content += " // Initialize validator for request validation\n"; + content += " this._validator = createValidator();\n"; + content += "\n"; + content += " this._interceptors = [];\n"; + content += "\n"; + content += " this._interceptors.push(createLoggingInterceptor());\n"; content += "\n"; - content += " // Create the gRPC-Web transport with interceptors\n"; + content += " if (this._config.apiKey) {\n"; + content += + " this._interceptors.push(createApiKeyInterceptor(this._config.apiKey));\n"; + content += " }\n"; + content += "\n"; + content += " if (this._config.jwtToken) {\n"; + content += + " this._interceptors.push(createJwtInterceptor(this._config.jwtToken));\n"; + content += " }\n"; + content += "\n"; + content += " if (this._config.group) {\n"; + content += + " this._interceptors.push(createGroupInterceptor(this._config.group));\n"; + content += " }\n"; + content += "\n"; + content += " // Create the gRPC-Web transport for browser with interceptors\n"; content += " const transport = createGrpcWebTransport({\n"; content += " baseUrl: this._config.apiServerURL,\n"; content += " interceptors: this._interceptors,\n"; - content += " // Enable credentials (cookies) for cross-origin requests\n"; - content += " fetch: (input, init) => globalThis.fetch(input, { ...init, credentials: 'include' }),\n"; content += " });\n"; content += "\n"; content += " // Construct the Connect-ES client\n"; @@ -186,39 +329,52 @@ function generateServiceClientString(service: DescService, file: DescFile): stri // Generate withGroup method content += " /**\n"; - content += " * Returns a new client instance configured to send the specified group\n"; - content += " * resource name in the request headers for subsequent API calls.\n"; + content += + " * Returns a new client instance configured to send the specified group\n"; + content += + " * resource name in the request headers for subsequent API calls.\n"; + content += " *\n"; + content += + " * This method creates a new client with the same authentication configuration\n"; + content += + " * but with the group context updated to the specified value.\n"; + content += " *\n"; + content += " * **Compatibility**: Works with all authentication modes:\n"; + content += + " * - **API key auth**: Creates new client with API key + new group\n"; + content += " * - **JWT auth**: Creates new client with JWT + new group\n"; + content += + " * - **No auth**: Creates new client with standalone group interceptor\n"; content += " * \n"; - content += " * @param {string} group - The operating group context to inject into the request\n"; - content += " * in the format \`groups/{ulid}\` where {ulid} is a 26-character ULID.\n"; - content += " * Example: 'groups/01ARZ3NDEKTSV4YWVF8F5BH32'\n"; + content += + " * @param {string} group - The operating group context to inject into the request\n"; + content += + " * in the format `groups/{ulid}` where {ulid} is a 26-character ULID.\n"; + content += + " * Example: 'groups/01ARZ3NDEKTSV4YWVF8F5BH32'\n"; content += ` * @returns {${clientClassName}} A new, configured instance of the client.\n`; - content += " * @throws {Error} If the group format is invalid (validation occurs in createGroupInterceptor)\n"; + content += " * @throws {Error} If the group format is invalid\n"; content += " */\n"; content += ` withGroup(group: string): ${clientClassName} {\n`; - content += " // Check if a group interceptor already exists.\n"; - content += " // Group interceptors are identified by having a 'groupContext' property\n"; - content += " const hasGroupInterceptor = this._interceptors.some(\n"; - content += " (interceptor: any) => interceptor.groupContext !== undefined\n"; - content += " );\n"; + content += + " // Build new options array with existing auth and updated group\n"; + content += " const newOpts: ClientOption[] = [];\n"; + content += "\n"; + content += " // Add server URL\n"; + content += " newOpts.push(WithServerUrl(this._config.apiServerURL));\n"; content += "\n"; - content += " if (hasGroupInterceptor) {\n"; - content += " throw new Error(\n"; - content += ' "Attempted to set group context twice. A group has already been set for this client instance."\n'; - content += " );\n"; + content += " // Add authentication (preserve existing mode)\n"; + content += " if (this._config.apiKey) {\n"; + content += " newOpts.push(WithAPIKey(this._config.apiKey));\n"; + content += " } else if (this._config.jwtToken) {\n"; + content += " newOpts.push(WithJWTAccessToken(this._config.jwtToken));\n"; content += " }\n"; content += "\n"; - content += " // Create a new interceptor for the group context\n"; - content += " const groupInterceptor = createGroupInterceptor(group);\n"; + content += " // Add the new group\n"; + content += " newOpts.push(WithGroup(group));\n"; content += "\n"; - content += " // Return a new client instance with the existing interceptors plus the new one\n"; - content += ` return new ${clientClassName}(\n`; - content += " this._config,\n"; - content += " [\n"; - content += " ...this._interceptors,\n"; - content += " groupInterceptor,\n"; - content += " ],\n"; - content += " );\n"; + content += " // Return a new client instance with updated configuration\n"; + content += ` return new ${clientClassName}(...newOpts);\n`; content += " }\n"; content += "\n"; @@ -237,7 +393,7 @@ function generateStreamingMethodString( methodName: string, requestType: string, responseType: string, - resourceName: string + resourceName: string, ): string { let content = ""; @@ -260,7 +416,13 @@ function generateStreamingMethodString( // Generate method signature and implementation with validation content += ` ${methodName}(request: ${requestType}): AsyncIterable<${responseType}> {\n`; content += " // Validate request before initiating stream\n"; - content += " validateRequest(request);\n"; + content += ` const result = this._validator.validate(${requestType}Schema, request);\n`; + content += ' if (result.kind === "invalid") {\n'; + content += ' const violations = result.violations.map(v => `${v.field.toString()}: ${v.message}`).join("; ");\n'; + content += ' throw new Error(`Validation failed: ${violations}`);\n'; + content += ' } else if (result.kind === "error") {\n'; + content += " throw result.error;\n"; + content += " }\n"; content += "\n"; content += ` return this._client.${methodName}(request);\n`; content += " }\n"; @@ -269,7 +431,11 @@ function generateStreamingMethodString( return content; } -function generateServiceMethodString(method: DescMethod, service: DescService, resourceName: string): string { +function generateServiceMethodString( + method: DescMethod, + service: DescService, + resourceName: string, +): string { const methodName = camelCase(method.name); const requestType = method.input.name; const responseType = method.output.name; @@ -280,7 +446,13 @@ function generateServiceMethodString(method: DescMethod, service: DescService, r let content = ""; if (isServerStreaming) { - return generateStreamingMethodString(method, methodName, requestType, responseType, resourceName); + return generateStreamingMethodString( + method, + methodName, + requestType, + responseType, + resourceName, + ); } // Generate method JSDoc @@ -293,7 +465,13 @@ function generateServiceMethodString(method: DescMethod, service: DescService, r // Generate method signature and implementation content += ` ${methodName}(request: ${requestType}): Promise<${responseType}> {\n`; content += " // Validate request\n"; - content += " validateRequest(request);\n"; + content += ` const result = this._validator.validate(${requestType}Schema, request);\n`; + content += ' if (result.kind === "invalid") {\n'; + content += ' const violations = result.violations.map(v => `${v.field.toString()}: ${v.message}`).join("; ");\n'; + content += ' throw new Error(`Validation failed: ${violations}`);\n'; + content += ' } else if (result.kind === "error") {\n'; + content += " throw result.error;\n"; + content += " }\n"; content += "\n"; content += ` return this._client.${methodName}(request);\n`; content += " }\n"; @@ -306,39 +484,45 @@ function camelCase(str: string): string { return str.charAt(0).toLowerCase() + str.slice(1); } -function getMethodDescription(methodName: string, resourceName: string): string { +function getMethodDescription( + methodName: string, + resourceName: string, +): string { const method = methodName.toLowerCase(); const resource = toReadableResourceName(resourceName); - if (method.startsWith('get')) { + if (method.startsWith("get")) { return `Retrieves ${getArticle(resource)} ${resource}.`; - } else if (method.startsWith('create')) { + } else if (method.startsWith("create")) { return `Creates a new ${resource}.`; - } else if (method.startsWith('update')) { + } else if (method.startsWith("update")) { return `Updates an existing ${resource}.`; - } else if (method.startsWith('delete')) { + } else if (method.startsWith("delete")) { return `Deletes ${getArticle(resource)} ${resource}.`; - } else if (method.startsWith('list')) { + } else if (method.startsWith("list")) { return `Retrieves a list of ${resource}s.`; - } else if (method.startsWith('search')) { + } else if (method.startsWith("search")) { return `Searches for ${resource}s.`; - } else if (method.startsWith('activate')) { + } else if (method.startsWith("activate")) { return `Activates ${getArticle(resource)} ${resource}.`; - } else if (method.startsWith('deactivate')) { + } else if (method.startsWith("deactivate")) { return `Deactivates ${getArticle(resource)} ${resource}.`; } else { return `Performs ${method} operation on ${resource}.`; } } -function getMethodReturnDescription(methodName: string, resourceName: string): string { +function getMethodReturnDescription( + methodName: string, + resourceName: string, +): string { const method = methodName.toLowerCase(); const resource = toReadableResourceName(resourceName); - if (method.startsWith('list')) { + if (method.startsWith("list")) { return `list of ${resource}s`; - } else if (method.startsWith('search')) { - return 'search results'; + } else if (method.startsWith("search")) { + return "search results"; } else { return resource; } @@ -347,21 +531,13 @@ function getMethodReturnDescription(methodName: string, resourceName: string): s function toReadableResourceName(resourceName: string): string { // Convert PascalCase to readable format, e.g., "ApiUser" -> "API user" return resourceName - .replace(/([A-Z])([a-z])/g, '$1$2') // Add space before capital followed by lowercase - .replace(/([a-z])([A-Z])/g, '$1 $2') // Add space between lowercase and capital + .replace(/([A-Z])([a-z])/g, "$1$2") // Add space before capital followed by lowercase + .replace(/([a-z])([A-Z])/g, "$1 $2") // Add space between lowercase and capital .toLowerCase(); } function getArticle(word: string): string { // Return appropriate article (a/an) based on first letter const firstLetter = word.charAt(0).toLowerCase(); - return ['a', 'e', 'i', 'o', 'u'].includes(firstLetter) ? 'an' : 'a'; -} - -function convertToSnakeCase(str: string): string { - // Convert PascalCase to snake_case: "APIUser" -> "api_user" - return str - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') // Handle sequences like "API" -> "API_" - .replace(/([a-z\d])([A-Z])/g, '$1_$2') // Handle transitions like "aB" -> "a_B" - .toLowerCase(); + return ["a", "e", "i", "o", "u"].includes(firstLetter) ? "an" : "a"; } diff --git a/tool/protoc-gen-meshjava/src/main/java/co/meshtrade/protoc/model/MethodModel.java b/tool/protoc-gen-meshjava/src/main/java/co/meshtrade/protoc/model/MethodModel.java index b8597181..a334270d 100644 --- a/tool/protoc-gen-meshjava/src/main/java/co/meshtrade/protoc/model/MethodModel.java +++ b/tool/protoc-gen-meshjava/src/main/java/co/meshtrade/protoc/model/MethodModel.java @@ -230,13 +230,7 @@ private String inferOuterClassName(String protoPackageName, String typeName) { // Convert proto file name to PascalCase String pascalCaseFileName = snakeCaseToPascalCase(protoFileName); - - // Special case: api_user proto generates ApiUser.java (not ApiUserOuterClass.java) - // This happens when the proto file name closely matches the main message name - if ("api_user".equals(protoFileName) && ("APIUser".equals(typeName) || "ApiCredentials".equals(typeName))) { - return pascalCaseFileName; - } - + // Default case: use OuterClass pattern return pascalCaseFileName + "OuterClass"; } diff --git a/ts-node/package.json b/ts-node/package.json index b36fbffc..351de9b8 100644 --- a/ts-node/package.json +++ b/ts-node/package.json @@ -81,6 +81,16 @@ "require": "./dist/meshtrade/option/v1/index.js", "import": "./dist/meshtrade/option/v1/index.js" }, + "./config": { + "types": "./dist/meshtrade/config/index.d.ts", + "require": "./dist/meshtrade/config/index.js", + "import": "./dist/meshtrade/config/index.js" + }, + "./interceptors": { + "types": "./dist/meshtrade/interceptors/index.d.ts", + "require": "./dist/meshtrade/interceptors/index.js", + "import": "./dist/meshtrade/interceptors/index.js" + }, "./*": { "types": "./dist/meshtrade/*", "require": "./dist/meshtrade/*", @@ -128,6 +138,12 @@ "option/v1": [ "dist/meshtrade/option/v1/index.d.ts" ], + "config": [ + "dist/meshtrade/config/index.d.ts" + ], + "interceptors": [ + "dist/meshtrade/interceptors/index.d.ts" + ], "*": [ "dist/meshtrade/*" ] @@ -138,6 +154,7 @@ ], "dependencies": { "@bufbuild/protobuf": "^2.10.1", + "@bufbuild/protovalidate": "^1.0.0", "@connectrpc/connect": "^2.1.0", "@connectrpc/connect-node": "^2.1.0", "bignumber.js": "^9.3.0" @@ -159,7 +176,7 @@ }, "scripts": { "clean": "rimraf ./dist", - "build:ts": "tsc", + "build:ts": "tsc --project tsconfig.build.json", "build": "yarn run clean && yarn run build:ts", "lint": "eslint . --ext .ts", "test": "jest" diff --git a/ts-node/src/meshtrade/common/config.ts b/ts-node/src/meshtrade/common/config.ts deleted file mode 100644 index 48a7cb4b..00000000 --- a/ts-node/src/meshtrade/common/config.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Configuration options for Meshtrade API clients. - * - * Supports three authentication modes: - * - * 1. **No Authentication** (public APIs): - * ```typescript - * const client = new ServiceNode({ apiServerURL: "http://localhost:10000" }); - * ``` - * - * 2. **API Key Authentication** (backend services): - * ```typescript - * const client = new ServiceNode({ - * apiServerURL: "https://api.example.com", - * apiKey: "your-api-key", - * group: "groups/01ARZ3NDEKTSV4YWVF8F5BH32" - * }); - * ``` - * - * 3. **JWT Token Authentication** (Next.js backend with user session): - * ```typescript - * const client = new ServiceNode({ - * apiServerURL: "https://api.example.com", - * jwtToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - * }); - * ``` - */ -export type ConfigOpts = { - /** API server URL (default: "http://localhost:10000") */ - apiServerURL?: string; - - /** API key for service-to-service authentication (requires group) */ - apiKey?: string; - - /** Group context for API key authentication in format "groups/{ulid}" */ - group?: string; - - /** JWT token for user session authentication (injected as AccessToken cookie) */ - jwtToken?: string; -}; - -export type Config = { - apiServerURL: string; - apiKey?: string; - group?: string; - jwtToken?: string; -}; - -/** - * Validates and creates configuration from options. - * - * @throws {Error} If API key is provided without group, or vice versa - * @throws {Error} If both API key and JWT token are provided (mutually exclusive) - */ -export function getConfigFromOpts(config?: ConfigOpts): Config { - const apiServerURL = config?.apiServerURL ?? "http://localhost:10000"; - const apiKey = config?.apiKey; - const group = config?.group; - const jwtToken = config?.jwtToken; - - // Validate authentication configuration - if (apiKey && jwtToken) { - throw new Error( - "API key and JWT token authentication are mutually exclusive. " + - "Please provide either apiKey+group OR jwtToken, not both." - ); - } - - if (apiKey && !group) { - throw new Error( - "API key authentication requires a group. " + - "Please provide both 'apiKey' and 'group' options." - ); - } - - if (group && !apiKey) { - throw new Error( - "Group context requires API key authentication. " + - "Please provide both 'apiKey' and 'group' options." - ); - } - - return { - apiServerURL, - apiKey, - group, - jwtToken, - }; -} diff --git a/ts-node/src/meshtrade/common/validation.test.ts b/ts-node/src/meshtrade/common/validation.test.ts deleted file mode 100644 index 17ff84db..00000000 --- a/ts-node/src/meshtrade/common/validation.test.ts +++ /dev/null @@ -1,274 +0,0 @@ -/** - * @jest-environment node - */ - -import { isValidULID, isValidGroupResourceName } from "./validation"; - -/** - * Extracts the ULID from a group resource name. - * - * @param groupResourceName - The group resource name in format "groups/{ulid}" - * @returns the ULID portion if valid, null if the format is invalid - * - * @example - * ```typescript - * extractULIDFromGroupName('groups/01ARZ3NDEKTSV4YWVF8F5BH32'); // '01ARZ3NDEKTSV4YWVF8F5BH32' - * extractULIDFromGroupName('invalid'); // null - * ``` - */ -function extractULIDFromGroupName(groupResourceName: string): string | null { - if (!isValidGroupResourceName(groupResourceName)) { - return null; - } - return groupResourceName.substring(7); // Remove "groups/" prefix -} - -/** - * Creates a group resource name from a ULID. - * - * @param ulid - The ULID to convert to a group resource name - * @returns the group resource name in format "groups/{ulid}", or null if ULID is invalid - * - * @example - * ```typescript - * createGroupResourceName('01ARZ3NDEKTSV4YWVF8F5BH32'); // 'groups/01ARZ3NDEKTSV4YWVF8F5BH32' - * createGroupResourceName('invalid'); // null - * ``` - */ -function createGroupResourceName(ulid: string): string | null { - if (!isValidULID(ulid)) { - return null; - } - return `groups/${ulid}`; -} - -/** - * Generic resource name validation for patterns like "{resourceType}/{ulid}". - * - * @param resourceName - The resource name to validate - * @param resourceType - The expected resource type (e.g., "groups", "api_users", "roles") - * @returns true if the resource name matches the expected pattern, false otherwise - * - * @example - * ```typescript - * isValidResourceName('groups/01ARZ3NDEKTSV4YWVF8F5BH32', 'groups'); // true - * isValidResourceName('api_users/01ARZ3NDEKTSV4YWVF8F5BH32', 'api_users'); // true - * isValidResourceName('groups/invalid', 'groups'); // false - * ``` - */ -function isValidResourceName( - resourceName: string, - resourceType: string -): boolean { - const pattern = new RegExp(`^${resourceType}\\/[0-9A-Z]{26}$`); - return pattern.test(resourceName); -} - -describe("validation utilities", () => { - // Valid ULID examples (26 characters, uppercase alphanumeric) - const validULIDs = [ - "01ARZ3NDEKTSV4YWVF8F5BH32Q", // Example ULID (26 chars) - "01BX5ZZKBKACTAV9WEVGEMMVR0", // Another valid ULID (26 chars) - "00000000000000000000000000", // All zeros (valid ULID) - "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", // All Z's (valid ULID) - ]; - - // Invalid ULID examples - const invalidULIDs = [ - "", // Empty string - "01ARZ3NDEKTSV4YWVF8F5BH3", // Too short (25 chars) - "01ARZ3NDEKTSV4YWVF8F5BH321X", // Too long (27 chars) - "01arz3ndektsv4ywvf8f5bh32q", // Lowercase (invalid) - "01ARZ3NDEKTSV4YWVF8F5BH3!", // Contains special character - "01ARZ3NDEKTSV4YWVF8F5BH3 ", // Contains space - "invalid", // Random string - "01ARZ3NDEKTSV4YWVF8F5BH3a", // Contains lowercase letter - "01ARZ3NDEKTSV4YWVF8F5BH3-", // Contains hyphen - "01ARZ3NDEKTSV4YWVF8F5BH3_", // Contains underscore - "01ARZ3NDEKTSV4YWVF8F5BH3.", // Contains period - ]; - - describe("isValidULID", () => { - test("should return true for valid ULIDs", () => { - validULIDs.forEach((ulid) => { - expect(isValidULID(ulid)).toBe(true); - }); - }); - - test("should return false for invalid ULIDs", () => { - invalidULIDs.forEach((ulid) => { - expect(isValidULID(ulid)).toBe(false); - }); - }); - - test("should handle edge cases", () => { - expect(isValidULID("123456789012345678901234567")).toBe(false); // Numbers only but too long - expect(isValidULID("ABCDEFGHIJKLMNOPQRSTUVWXYZ")).toBe(true); // All letters, 26 chars - valid - expect(isValidULID("0123456789ABCDEFGHIJKLMNPQ")).toBe(true); // Valid mix, 26 chars - }); - }); - - describe("isValidGroupResourceName", () => { - const validGroupNames = validULIDs.map((ulid) => `groups/${ulid}`); - - test("should return true for valid group resource names", () => { - validGroupNames.forEach((groupName) => { - expect(isValidGroupResourceName(groupName)).toBe(true); - }); - }); - - test("should return false for invalid group resource names", () => { - const invalidGroupNames = [ - "", // Empty string - "groups/", // Missing ULID - "groups/invalid", // Invalid ULID - "01ARZ3NDEKTSV4YWVF8F5BH32Q", // Missing "groups/" prefix - "users/01ARZ3NDEKTSV4YWVF8F5BH32Q", // Wrong resource type - "Groups/01ARZ3NDEKTSV4YWVF8F5BH32Q", // Wrong case - "groups/01arz3ndektsv4ywvf8f5bh32q", // Lowercase ULID - "groups/01ARZ3NDEKTSV4YWVF8F5BH3", // Too short ULID - "groups/01ARZ3NDEKTSV4YWVF8F5BH321X", // Too long ULID - "groups/01ARZ3NDEKTSV4YWVF8F5BH3!", // Invalid character in ULID - "groups//01ARZ3NDEKTSV4YWVF8F5BH32Q", // Double slash - "/groups/01ARZ3NDEKTSV4YWVF8F5BH32Q", // Leading slash - "groups/01ARZ3NDEKTSV4YWVF8F5BH32Q/", // Trailing slash - ]; - - invalidGroupNames.forEach((groupName) => { - expect(isValidGroupResourceName(groupName)).toBe(false); - }); - }); - }); - - describe("extractULIDFromGroupName", () => { - test("should extract ULID from valid group resource names", () => { - validULIDs.forEach((ulid) => { - const groupName = `groups/${ulid}`; - expect(extractULIDFromGroupName(groupName)).toBe(ulid); - }); - }); - - test("should return null for invalid group resource names", () => { - const invalidGroupNames = [ - "", // Empty string - "groups/", // Missing ULID - "groups/invalid", // Invalid ULID - "01ARZ3NDEKTSV4YWVF8F5BH32", // Missing prefix - "users/01ARZ3NDEKTSV4YWVF8F5BH32", // Wrong resource type - ]; - - invalidGroupNames.forEach((groupName) => { - expect(extractULIDFromGroupName(groupName)).toBeNull(); - }); - }); - }); - - describe("createGroupResourceName", () => { - test("should create valid group resource names from valid ULIDs", () => { - validULIDs.forEach((ulid) => { - const expected = `groups/${ulid}`; - expect(createGroupResourceName(ulid)).toBe(expected); - }); - }); - - test("should return null for invalid ULIDs", () => { - invalidULIDs.forEach((ulid) => { - expect(createGroupResourceName(ulid)).toBeNull(); - }); - }); - }); - - describe("isValidResourceName", () => { - test("should validate group resource names", () => { - validULIDs.forEach((ulid) => { - expect(isValidResourceName(`groups/${ulid}`, "groups")).toBe(true); - }); - }); - - test("should validate api_users resource names", () => { - validULIDs.forEach((ulid) => { - expect(isValidResourceName(`api_users/${ulid}`, "api_users")).toBe( - true - ); - }); - }); - - test("should validate roles resource names", () => { - validULIDs.forEach((ulid) => { - expect(isValidResourceName(`roles/${ulid}`, "roles")).toBe(true); - }); - }); - - test("should return false for mismatched resource types", () => { - const ulid = "01ARZ3NDEKTSV4YWVF8F5BH32Q"; - expect(isValidResourceName(`groups/${ulid}`, "users")).toBe(false); - expect(isValidResourceName(`api_users/${ulid}`, "groups")).toBe(false); - expect(isValidResourceName(`roles/${ulid}`, "api_users")).toBe(false); - }); - - test("should return false for invalid ULIDs", () => { - invalidULIDs.forEach((ulid) => { - expect(isValidResourceName(`groups/${ulid}`, "groups")).toBe(false); - expect(isValidResourceName(`api_users/${ulid}`, "api_users")).toBe( - false - ); - }); - }); - - test("should handle special characters in resource type", () => { - const ulid = "01ARZ3NDEKTSV4YWVF8F5BH32Q"; - // Test with resource types that contain underscores - expect(isValidResourceName(`api_users/${ulid}`, "api_users")).toBe(true); - expect( - isValidResourceName(`some_resource/${ulid}`, "some_resource") - ).toBe(true); - }); - - test("should be case sensitive for resource types", () => { - const ulid = "01ARZ3NDEKTSV4YWVF8F5BH32Q"; - expect(isValidResourceName(`Groups/${ulid}`, "groups")).toBe(false); - expect(isValidResourceName(`groups/${ulid}`, "Groups")).toBe(false); - }); - }); - - describe("integration tests", () => { - test("should work together for complete workflow", () => { - const ulid = "01ARZ3NDEKTSV4YWVF8F5BH32Q"; - - // Validate the ULID - expect(isValidULID(ulid)).toBe(true); - - // Create a group resource name - const groupName = createGroupResourceName(ulid); - expect(groupName).toBe(`groups/${ulid}`); - - // Validate the group resource name - expect(isValidGroupResourceName(groupName!)).toBe(true); - - // Extract the ULID back - const extractedULID = extractULIDFromGroupName(groupName!); - expect(extractedULID).toBe(ulid); - - // Generic validation - expect(isValidResourceName(groupName!, "groups")).toBe(true); - }); - - test("should handle error cases in workflow", () => { - const invalidULID = "invalid"; - - // Invalid ULID should fail validation - expect(isValidULID(invalidULID)).toBe(false); - - // Creating group name should return null - const groupName = createGroupResourceName(invalidULID); - expect(groupName).toBeNull(); - - // Manual invalid group name should fail validation - const invalidGroupName = `groups/${invalidULID}`; - expect(isValidGroupResourceName(invalidGroupName)).toBe(false); - - // Extraction should return null - expect(extractULIDFromGroupName(invalidGroupName)).toBeNull(); - }); - }); -}); diff --git a/ts-node/src/meshtrade/common/validation.ts b/ts-node/src/meshtrade/common/validation.ts deleted file mode 100644 index b8cea400..00000000 --- a/ts-node/src/meshtrade/common/validation.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Generic validation utilities for Meshtrade API resource names and identifiers. - */ - -/** - * Validates if a string is a valid ULID (Universally Unique Lexicographically Sortable Identifier). - * - * Note: This implementation uses a simplified character set for ULIDs that includes - * all uppercase letters A-Z and digits 0-9, unlike the standard ULID specification - * which excludes certain ambiguous characters (I, L, O, U). - * - * ULIDs in this system are 26-character identifiers that are: - * - Lexicographically sortable - * - Uppercase alphanumeric only - * - Contain timestamp information for natural ordering - * - * @param ulid - The string to validate as a ULID - * @returns true if the string is a valid ULID format, false otherwise - * - * @example - * ```typescript - * isValidULID('01ARZ3NDEKTSV4YWVF8F5BH32'); // true - * isValidULID('invalid'); // false - * isValidULID('01arz3ndektsv4ywvf8f5bh32'); // false (lowercase) - * ``` - */ -export function isValidULID(ulid: string): boolean { - return /^[0-9A-Z]{26}$/.test(ulid); -} - -/** - * Validates if a resource name follows the groups/{ulid} format. - * - * Group resource names in the Meshtrade API follow the pattern "groups/{ulid}" - * where {ulid} is a 26-character ULID identifier. - * - * @param resourceName - The resource name string to validate - * @returns true if the resource name is a valid group resource name, false otherwise - * - * @example - * ```typescript - * isValidGroupResourceName('groups/01ARZ3NDEKTSV4YWVF8F5BH32'); // true - * isValidGroupResourceName('groups/invalid'); // false - * isValidGroupResourceName('users/01ARZ3NDEKTSV4YWVF8F5BH32'); // false - * isValidGroupResourceName('01ARZ3NDEKTSV4YWVF8F5BH32'); // false - * ``` - */ -export function isValidGroupResourceName(resourceName: string): boolean { - return /^groups\/[0-9A-Z]{26}$/.test(resourceName); -} - -/** - * Validates a protobuf request message before sending to the server. - * - * This function serves as a client-side validation hook that can be extended - * to include protovalidate integration or other validation logic. - * - * Currently performs basic null/undefined checks. Future enhancements may include - * protovalidate integration for comprehensive message validation. - * - * @param request - The protobuf request message to validate - * @throws {Error} If the request is null or undefined - * - * @example - * ```typescript - * validateRequest(myRequest); // Throws if request is invalid - * ``` - */ -export function validateRequest(request: unknown): void { - if (request === null || request === undefined) { - throw new Error("Request cannot be null or undefined"); - } - // Future: Integrate protovalidate for comprehensive message validation - // For now, basic validation is sufficient as the server also validates -} diff --git a/ts-node/src/meshtrade/config/index.ts b/ts-node/src/meshtrade/config/index.ts new file mode 100644 index 00000000..d13ac669 --- /dev/null +++ b/ts-node/src/meshtrade/config/index.ts @@ -0,0 +1,230 @@ +/** + * Configuration options for Meshtrade API clients using functional options pattern. + * + * Supports flexible authentication modes with optional group context: + * + * 1. **No Authentication** (public APIs): + * ```typescript + * const client = new ServiceNode( + * WithServerUrl("http://localhost:10000") + * ); + * ``` + * + * 2. **API Key Authentication** (backend services): + * ```typescript + * const client = new ServiceNode( + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"), + * WithServerUrl("https://api.example.com") + * ); + * ``` + * + * 3. **JWT Token Authentication** (Next.js backend with user session): + * ```typescript + * const client = new ServiceNode( + * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."), + * WithServerUrl("https://api.example.com") + * ); + * ``` + * + * 4. **JWT with Group Context** (user session with specific group): + * ```typescript + * const client = new ServiceNode( + * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"), + * WithServerUrl("https://api.example.com") + * ); + * ``` + */ + +/** + * Internal configuration class used to build client configuration. + */ +export class ClientConfig { + /** API server URL (default: production) */ + apiServerURL: string = "http://localhost:10000"; + + /** API key for service-to-service authentication */ + apiKey?: string; + + /** JWT token for user session authentication */ + jwtToken?: string; + + /** Group context in format "groups/{ulid}" */ + group?: string; + + /** + * Validates the configuration. + * @throws {Error} If both API key and JWT token are provided (mutually exclusive) + */ + validate(): void { + if (this.apiKey && this.jwtToken) { + throw new Error( + "API key and JWT token authentication are mutually exclusive. " + + "Please use WithAPIKey() OR WithJWTAccessToken(), not both." + ); + } + } +} + +/** + * Client option function type for functional options pattern. + * Each option function modifies the ClientConfig. + */ +export type ClientOption = (config: ClientConfig) => void; + +/** + * Configures the client with an API key for service-to-service authentication. + * + * **Mutually Exclusive**: Cannot be used with WithJWTAccessToken(). + * **Optional**: Can be combined with WithGroup() for group-specific operations. + * + * @param apiKey - The API key for authentication + * @returns A client option function + * + * @example + * ```typescript + * const client = new ServiceNode( + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * ``` + */ +export function WithAPIKey(apiKey: string): ClientOption { + return (config: ClientConfig) => { + if (!apiKey || apiKey.trim() === "") { + throw new Error("API key cannot be empty"); + } + if (config.jwtToken) { + throw new Error( + "Cannot use both WithAPIKey() and WithJWTAccessToken(). " + + "Please choose one authentication method." + ); + } + config.apiKey = apiKey; + }; +} + +/** + * Configures the client with a JWT access token for user session authentication. + * + * **Mutually Exclusive**: Cannot be used with WithAPIKey(). + * **Optional**: Can be combined with WithGroup() for group-specific operations. + * + * The JWT is injected as a cookie header (Cookie: AccessToken=) + * so the server can extract it from the request. + * + * @param token - The JWT access token from the user's session + * @returns A client option function + * + * @example + * ```typescript + * const client = new ServiceNode( + * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * ``` + */ +export function WithJWTAccessToken(token: string): ClientOption { + return (config: ClientConfig) => { + if (!token || token.trim() === "") { + throw new Error("JWT token cannot be empty"); + } + if (config.apiKey) { + throw new Error( + "Cannot use both WithJWTAccessToken() and WithAPIKey(). " + + "Please choose one authentication method." + ); + } + config.jwtToken = token; + }; +} + +/** + * Configures the client with a group context for operations. + * + * **Optional**: Can be used with WithAPIKey() or WithJWTAccessToken(). + * When used alone without authentication, adds group header to requests. + * + * @param group - The group resource name in format "groups/{ulid}" + * @returns A client option function + * + * @example + * ```typescript + * // With API Key + * const client = new ServiceNode( + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * + * // With JWT + * const client = new ServiceNode( + * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * ``` + */ +export function WithGroup(group: string): ClientOption { + return (config: ClientConfig) => { + if (!group || group.trim() === "") { + throw new Error("Group cannot be empty"); + } + config.group = group; + }; +} + +/** + * Configures the client with a custom server URL. + * + * **Optional**: If not provided, defaults to localhost:10000. + * + * @param url - The API server URL + * @returns A client option function + * + * @example + * ```typescript + * const client = new ServiceNode( + * WithServerUrl("http://localhost:10000"), + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * ``` + */ +export function WithServerUrl(url: string): ClientOption { + return (config: ClientConfig) => { + if (!url || url.trim() === "") { + throw new Error("Server URL cannot be empty"); + } + config.apiServerURL = url; + }; +} + +/** + * Builds client configuration from an array of option functions. + * + * @param opts - Variable number of option functions + * @returns A validated ClientConfig instance + * @throws {Error} If configuration is invalid (e.g., both API key and JWT provided) + * + * @example + * ```typescript + * const config = buildConfigFromOptions( + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"), + * WithServerUrl("https://api.example.com") + * ); + * ``` + */ +export function buildConfigFromOptions(...opts: ClientOption[]): ClientConfig { + const config = new ClientConfig(); + + // Apply each option + for (const opt of opts) { + opt(config); + } + + // Validate the final configuration + config.validate(); + + return config; +} diff --git a/ts-node/src/meshtrade/iam/api_user/v1/validation.integration.test.ts b/ts-node/src/meshtrade/iam/api_user/v1/validation.integration.test.ts new file mode 100644 index 00000000..42f32c3e --- /dev/null +++ b/ts-node/src/meshtrade/iam/api_user/v1/validation.integration.test.ts @@ -0,0 +1,193 @@ +/** + * Integration tests for APIUserServiceNode validation + * Tests buf.validate schema validation before network calls + */ + +import { create } from "@bufbuild/protobuf"; +import { + GetAPIUserRequestSchema, + GetAPIUserByKeyHashRequestSchema, + AssignRolesToAPIUserRequestSchema, +} from "./service_pb"; +import { APIUserServiceNode } from "./service_node_meshts"; +import { WithServerUrl } from "../../../config"; + +describe("APIUserServiceNode - Request validation (before network call)", () => { + let client: APIUserServiceNode; + + beforeEach(() => { + // Create client with dummy server URL - validation happens before network call + client = new APIUserServiceNode(WithServerUrl("http://localhost:9999")); + }); + + describe("GetAPIUserRequest validation", () => { + it("should pass validation with valid request and fail at network layer", async () => { + const request = create(GetAPIUserRequestSchema, { + name: "api_users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + }); + + try { + await client.getAPIUser(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for empty name", async () => { + const request = create(GetAPIUserRequestSchema, { + name: "", + }); + + try { + await client.getAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid ULID format", async () => { + const request = create(GetAPIUserRequestSchema, { + name: "api_users/invalid", + }); + + try { + await client.getAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for lowercase ULID", async () => { + const request = create(GetAPIUserRequestSchema, { + name: "api_users/01arz3ndektsv4ywvf8f5bh3ab", + }); + + try { + await client.getAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); + + describe("GetAPIUserByKeyHashRequest validation", () => { + it("should pass validation with valid base64 key hash and fail at network layer", async () => { + const request = create(GetAPIUserByKeyHashRequestSchema, { + // 44-character base64: 43 base64 chars + 1 '=' padding + keyHash: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ=", + }); + + try { + await client.getAPIUserByKeyHash(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid base64 format", async () => { + const request = create(GetAPIUserByKeyHashRequestSchema, { + keyHash: "invalid-base64", + }); + + try { + await client.getAPIUserByKeyHash(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for empty key hash", async () => { + const request = create(GetAPIUserByKeyHashRequestSchema, { + keyHash: "", + }); + + try { + await client.getAPIUserByKeyHash(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); + + describe("AssignRolesToAPIUserRequest validation", () => { + it("should pass validation with valid inputs and fail at network layer", async () => { + const request = create(AssignRolesToAPIUserRequestSchema, { + name: "api_users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: [ + "groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/1234567", + "groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/12345678", + ], + }); + + try { + await client.assignRolesToAPIUser(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid role format", async () => { + const request = create(AssignRolesToAPIUserRequestSchema, { + name: "api_users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: ["invalid-role-format"], + }); + + try { + await client.assignRolesToAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for empty roles array", async () => { + const request = create(AssignRolesToAPIUserRequestSchema, { + name: "api_users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: [], + }); + + try { + await client.assignRolesToAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid api_user name", async () => { + const request = create(AssignRolesToAPIUserRequestSchema, { + name: "invalid", + roles: ["groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/1234567"], + }); + + try { + await client.assignRolesToAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); +}); diff --git a/ts-node/src/meshtrade/iam/user/v1/validation.integration.test.ts b/ts-node/src/meshtrade/iam/user/v1/validation.integration.test.ts new file mode 100644 index 00000000..ec0a33f8 --- /dev/null +++ b/ts-node/src/meshtrade/iam/user/v1/validation.integration.test.ts @@ -0,0 +1,192 @@ +/** + * Integration tests for UserServiceNode validation + * Tests buf.validate schema validation before network calls + */ + +import { create } from "@bufbuild/protobuf"; +import { + GetUserRequestSchema, + GetUserByEmailRequestSchema, + AssignRolesToUserRequestSchema, +} from "./service_pb"; +import { UserServiceNode } from "./service_node_meshts"; +import { WithServerUrl } from "../../../config"; + +describe("UserServiceNode - Request validation (before network call)", () => { + let client: UserServiceNode; + + beforeEach(() => { + // Create client with dummy server URL - validation happens before network call + client = new UserServiceNode(WithServerUrl("http://localhost:9999")); + }); + + describe("GetUserRequest validation", () => { + it("should pass validation with valid request and fail at network layer", async () => { + const request = create(GetUserRequestSchema, { + name: "users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + }); + + try { + await client.getUser(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for empty name", async () => { + const request = create(GetUserRequestSchema, { + name: "", + }); + + try { + await client.getUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid ULID format", async () => { + const request = create(GetUserRequestSchema, { + name: "users/invalid", + }); + + try { + await client.getUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for lowercase ULID", async () => { + const request = create(GetUserRequestSchema, { + name: "users/01arz3ndektsv4ywvf8f5bh3ab", + }); + + try { + await client.getUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); + + describe("GetUserByEmailRequest validation", () => { + it("should pass validation with valid email and fail at network layer", async () => { + const request = create(GetUserByEmailRequestSchema, { + email: "user@example.com", + }); + + try { + await client.getUserByEmail(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid email format", async () => { + const request = create(GetUserByEmailRequestSchema, { + email: "not-an-email", + }); + + try { + await client.getUserByEmail(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for empty email", async () => { + const request = create(GetUserByEmailRequestSchema, { + email: "", + }); + + try { + await client.getUserByEmail(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); + + describe("AssignRolesToUserRequest validation", () => { + it("should pass validation with valid inputs and fail at network layer", async () => { + const request = create(AssignRolesToUserRequestSchema, { + name: "users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: [ + "groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/1234567", + "groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/12345678", + ], + }); + + try { + await client.assignRolesToUser(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid role format", async () => { + const request = create(AssignRolesToUserRequestSchema, { + name: "users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: ["invalid-role-format"], + }); + + try { + await client.assignRolesToUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for empty roles array", async () => { + const request = create(AssignRolesToUserRequestSchema, { + name: "users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: [], + }); + + try { + await client.assignRolesToUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid user name", async () => { + const request = create(AssignRolesToUserRequestSchema, { + name: "invalid", + roles: ["groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/1234567"], + }); + + try { + await client.assignRolesToUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); +}); diff --git a/ts-web/src/meshtrade/common/connectInterceptors.ts b/ts-node/src/meshtrade/interceptors/index.ts similarity index 60% rename from ts-web/src/meshtrade/common/connectInterceptors.ts rename to ts-node/src/meshtrade/interceptors/index.ts index 807c439e..7531daa9 100644 --- a/ts-web/src/meshtrade/common/connectInterceptors.ts +++ b/ts-node/src/meshtrade/interceptors/index.ts @@ -1,27 +1,31 @@ /** - * Connect-ES interceptors for the Meshtrade API client (Web/Browser). + * Connect-ES interceptors for the Meshtrade API client. * * Provides interceptor utilities for use with @connectrpc/connect clients, - * including group context injection for multi-tenant operations. - * - * ## Authentication in Browser Environments - * - * The Web SDK uses browser-native cookie-based authentication via the - * `credentials: 'include'` fetch option. This automatically sends HTTP-only - * cookies (like AccessToken) with each request, which is the standard and - * secure authentication pattern for browser applications. - * - * Unlike the Node.js SDK which supports explicit API key and JWT interceptors, - * the Web SDK relies on the browser's automatic cookie handling. This is why - * this module only provides group context and logging interceptors - authentication - * is handled implicitly by the browser's cookie mechanism. - * - * For backend/server-side authentication needs, use the Node.js SDK instead - * (@meshtrade/api-node), which provides explicit API key and JWT token support. + * including authentication (API key, JWT) and group context injection for + * multi-tenant operations. */ import { Interceptor } from "@connectrpc/connect"; -import { isValidGroupResourceName } from "./validation"; + +/** + * HTTP header names for authentication. + * Must match the server-side header constants. + */ +const API_KEY_HEADER = "x-api-key"; +const GROUP_HEADER = "x-group"; +const COOKIE_HEADER = "cookie"; +const ACCESS_TOKEN_COOKIE_NAME = "AccessToken"; + +/** + * Validates if a resource name follows the groups/{ulid} format. + * + * @param resourceName - The resource name string to validate + * @returns true if the resource name is a valid group resource name, false otherwise + */ +function isValidGroupResourceName(resourceName: string): boolean { + return /^groups\/[0-9A-Z]{26}$/.test(resourceName); +} /** * Creates a Connect-ES interceptor that injects operating group context @@ -67,10 +71,7 @@ export function createGroupInterceptor( // Create the interceptor function const interceptor: Interceptor = (next) => async (req) => { - // Add the x-group header to the request - req.header.set("x-group", group); - - // Call the next interceptor in the chain + req.header.set(GROUP_HEADER, group); return await next(req); }; @@ -79,6 +80,51 @@ export function createGroupInterceptor( return Object.assign(interceptor, { groupContext: group }); } +/** + * Creates a Connect-ES interceptor that injects API key authentication. + * + * @param apiKey - The API key for authentication + * @returns An interceptor that adds x-api-key header + * @throws {Error} If apiKey is empty + */ +export function createApiKeyInterceptor( + apiKey: string +): Interceptor & { apiKeyAuth: true } { + if (!apiKey || apiKey.trim() === "") { + throw new Error("API key cannot be empty"); + } + + const interceptor: Interceptor = (next) => async (req) => { + req.header.set(API_KEY_HEADER, apiKey); + return await next(req); + }; + + return Object.assign(interceptor, { apiKeyAuth: true as const }); +} + +/** + * Creates a Connect-ES interceptor that injects JWT token authentication. + * + * @param jwtToken - The JWT token from the user's session + * @returns An interceptor that adds AccessToken cookie + * @throws {Error} If jwtToken is empty + */ +export function createJwtInterceptor( + jwtToken: string +): Interceptor & { jwtAuth: true } { + if (!jwtToken || jwtToken.trim() === "") { + throw new Error("JWT token cannot be empty"); + } + + const interceptor: Interceptor = (next) => async (req) => { + const cookieValue = `${ACCESS_TOKEN_COOKIE_NAME}=${jwtToken}`; + req.header.set(COOKIE_HEADER, cookieValue); + return await next(req); + }; + + return Object.assign(interceptor, { jwtAuth: true as const }); +} + /** * Creates a logging interceptor that logs all requests and responses. * Useful for debugging and development. @@ -104,7 +150,7 @@ export function createLoggingInterceptor(): Interceptor { }); // Log the request - console.log(`[Connect] ${req.method.name} request:`, { + console.debug(`[Connect] ${req.method.name} request:`, { service: req.service.typeName, method: req.method.name, headers, @@ -115,7 +161,7 @@ export function createLoggingInterceptor(): Interceptor { const response = await next(req); // Log successful response - console.log(`[Connect] ${req.method.name} response:`, { + console.debug(`[Connect] ${req.method.name} response:`, { service: req.service.typeName, method: req.method.name, status: "success", diff --git a/ts-node/tsconfig.build.json b/ts-node/tsconfig.build.json new file mode 100644 index 00000000..ce1a9d17 --- /dev/null +++ b/ts-node/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "dist", + "**/*.test.ts", + "**/*.spec.ts" + ] +} diff --git a/ts-node/tsconfig.json b/ts-node/tsconfig.json index 590a2b78..cd289874 100644 --- a/ts-node/tsconfig.json +++ b/ts-node/tsconfig.json @@ -10,6 +10,8 @@ "ES2020", "DOM" ], + // Include Jest types for test file type checking in editors + "types": ["jest"], // --- Library Build Settings --- // Generates .d.ts files so other TypeScript projects can use your library. ESSENTIAL. "declaration": true, @@ -32,10 +34,9 @@ "include": [ "src/**/*.ts" ], - // Tells TypeScript to ignore its own output directory and test files. + // Tells TypeScript to ignore its own output directory. + // Note: Test files are included for editor type checking but excluded from build via separate config. "exclude": [ - "dist", - "**/*.test.ts", - "**/*.spec.ts" + "dist" ] } \ No newline at end of file diff --git a/ts-old/package.json b/ts-old/package.json index 7aefc26f..8eef89c0 100644 --- a/ts-old/package.json +++ b/ts-old/package.json @@ -160,7 +160,7 @@ "scripts": { "clean": "rimraf ./dist", "copy-proto": "copyfiles -u 1 \"src/**/*.js\" \"src/**/*.d.ts\" dist", - "build:ts": "tsc", + "build:ts": "tsc --project tsconfig.build.json", "build": "yarn run clean && yarn run copy-proto && yarn run build:ts", "lint": "eslint . --ext .ts", "test": "jest" diff --git a/ts-old/tsconfig.build.json b/ts-old/tsconfig.build.json new file mode 100644 index 00000000..ce1a9d17 --- /dev/null +++ b/ts-old/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "dist", + "**/*.test.ts", + "**/*.spec.ts" + ] +} diff --git a/ts-old/tsconfig.json b/ts-old/tsconfig.json index 590a2b78..cd289874 100644 --- a/ts-old/tsconfig.json +++ b/ts-old/tsconfig.json @@ -10,6 +10,8 @@ "ES2020", "DOM" ], + // Include Jest types for test file type checking in editors + "types": ["jest"], // --- Library Build Settings --- // Generates .d.ts files so other TypeScript projects can use your library. ESSENTIAL. "declaration": true, @@ -32,10 +34,9 @@ "include": [ "src/**/*.ts" ], - // Tells TypeScript to ignore its own output directory and test files. + // Tells TypeScript to ignore its own output directory. + // Note: Test files are included for editor type checking but excluded from build via separate config. "exclude": [ - "dist", - "**/*.test.ts", - "**/*.spec.ts" + "dist" ] } \ No newline at end of file diff --git a/ts-web/package.json b/ts-web/package.json index a74e64a2..497904f7 100644 --- a/ts-web/package.json +++ b/ts-web/package.json @@ -81,6 +81,16 @@ "require": "./dist/meshtrade/option/v1/index.js", "import": "./dist/meshtrade/option/v1/index.js" }, + "./config": { + "types": "./dist/meshtrade/config/index.d.ts", + "require": "./dist/meshtrade/config/index.js", + "import": "./dist/meshtrade/config/index.js" + }, + "./interceptors": { + "types": "./dist/meshtrade/interceptors/index.d.ts", + "require": "./dist/meshtrade/interceptors/index.js", + "import": "./dist/meshtrade/interceptors/index.js" + }, "./*": { "types": "./dist/meshtrade/*", "require": "./dist/meshtrade/*", @@ -128,6 +138,12 @@ "option/v1": [ "dist/meshtrade/option/v1/index.d.ts" ], + "config": [ + "dist/meshtrade/config/index.d.ts" + ], + "interceptors": [ + "dist/meshtrade/interceptors/index.d.ts" + ], "*": [ "dist/meshtrade/*" ] @@ -138,6 +154,7 @@ ], "dependencies": { "@bufbuild/protobuf": "^2.10.1", + "@bufbuild/protovalidate": "^1.0.0", "@connectrpc/connect": "^2.1.0", "@connectrpc/connect-web": "^2.1.0", "bignumber.js": "^9.3.0" @@ -159,7 +176,7 @@ }, "scripts": { "clean": "rimraf ./dist", - "build:ts": "tsc", + "build:ts": "tsc --project tsconfig.build.json", "build": "yarn run clean && yarn run build:ts", "lint": "eslint . --ext .ts", "test": "jest" diff --git a/ts-web/src/meshtrade/common/config.ts b/ts-web/src/meshtrade/common/config.ts deleted file mode 100644 index 4c29b46c..00000000 --- a/ts-web/src/meshtrade/common/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type ConfigOpts = { - apiServerURL?: string; -}; - -export type Config = { - apiServerURL: string; -}; - -export function getConfigFromOpts(config?: ConfigOpts): Config { - const apiServerURL = config?.apiServerURL ?? "http://localhost:10000"; - - return { - apiServerURL, - }; -} diff --git a/ts-web/src/meshtrade/common/validation.test.ts b/ts-web/src/meshtrade/common/validation.test.ts deleted file mode 100644 index 17ff84db..00000000 --- a/ts-web/src/meshtrade/common/validation.test.ts +++ /dev/null @@ -1,274 +0,0 @@ -/** - * @jest-environment node - */ - -import { isValidULID, isValidGroupResourceName } from "./validation"; - -/** - * Extracts the ULID from a group resource name. - * - * @param groupResourceName - The group resource name in format "groups/{ulid}" - * @returns the ULID portion if valid, null if the format is invalid - * - * @example - * ```typescript - * extractULIDFromGroupName('groups/01ARZ3NDEKTSV4YWVF8F5BH32'); // '01ARZ3NDEKTSV4YWVF8F5BH32' - * extractULIDFromGroupName('invalid'); // null - * ``` - */ -function extractULIDFromGroupName(groupResourceName: string): string | null { - if (!isValidGroupResourceName(groupResourceName)) { - return null; - } - return groupResourceName.substring(7); // Remove "groups/" prefix -} - -/** - * Creates a group resource name from a ULID. - * - * @param ulid - The ULID to convert to a group resource name - * @returns the group resource name in format "groups/{ulid}", or null if ULID is invalid - * - * @example - * ```typescript - * createGroupResourceName('01ARZ3NDEKTSV4YWVF8F5BH32'); // 'groups/01ARZ3NDEKTSV4YWVF8F5BH32' - * createGroupResourceName('invalid'); // null - * ``` - */ -function createGroupResourceName(ulid: string): string | null { - if (!isValidULID(ulid)) { - return null; - } - return `groups/${ulid}`; -} - -/** - * Generic resource name validation for patterns like "{resourceType}/{ulid}". - * - * @param resourceName - The resource name to validate - * @param resourceType - The expected resource type (e.g., "groups", "api_users", "roles") - * @returns true if the resource name matches the expected pattern, false otherwise - * - * @example - * ```typescript - * isValidResourceName('groups/01ARZ3NDEKTSV4YWVF8F5BH32', 'groups'); // true - * isValidResourceName('api_users/01ARZ3NDEKTSV4YWVF8F5BH32', 'api_users'); // true - * isValidResourceName('groups/invalid', 'groups'); // false - * ``` - */ -function isValidResourceName( - resourceName: string, - resourceType: string -): boolean { - const pattern = new RegExp(`^${resourceType}\\/[0-9A-Z]{26}$`); - return pattern.test(resourceName); -} - -describe("validation utilities", () => { - // Valid ULID examples (26 characters, uppercase alphanumeric) - const validULIDs = [ - "01ARZ3NDEKTSV4YWVF8F5BH32Q", // Example ULID (26 chars) - "01BX5ZZKBKACTAV9WEVGEMMVR0", // Another valid ULID (26 chars) - "00000000000000000000000000", // All zeros (valid ULID) - "ZZZZZZZZZZZZZZZZZZZZZZZZZZ", // All Z's (valid ULID) - ]; - - // Invalid ULID examples - const invalidULIDs = [ - "", // Empty string - "01ARZ3NDEKTSV4YWVF8F5BH3", // Too short (25 chars) - "01ARZ3NDEKTSV4YWVF8F5BH321X", // Too long (27 chars) - "01arz3ndektsv4ywvf8f5bh32q", // Lowercase (invalid) - "01ARZ3NDEKTSV4YWVF8F5BH3!", // Contains special character - "01ARZ3NDEKTSV4YWVF8F5BH3 ", // Contains space - "invalid", // Random string - "01ARZ3NDEKTSV4YWVF8F5BH3a", // Contains lowercase letter - "01ARZ3NDEKTSV4YWVF8F5BH3-", // Contains hyphen - "01ARZ3NDEKTSV4YWVF8F5BH3_", // Contains underscore - "01ARZ3NDEKTSV4YWVF8F5BH3.", // Contains period - ]; - - describe("isValidULID", () => { - test("should return true for valid ULIDs", () => { - validULIDs.forEach((ulid) => { - expect(isValidULID(ulid)).toBe(true); - }); - }); - - test("should return false for invalid ULIDs", () => { - invalidULIDs.forEach((ulid) => { - expect(isValidULID(ulid)).toBe(false); - }); - }); - - test("should handle edge cases", () => { - expect(isValidULID("123456789012345678901234567")).toBe(false); // Numbers only but too long - expect(isValidULID("ABCDEFGHIJKLMNOPQRSTUVWXYZ")).toBe(true); // All letters, 26 chars - valid - expect(isValidULID("0123456789ABCDEFGHIJKLMNPQ")).toBe(true); // Valid mix, 26 chars - }); - }); - - describe("isValidGroupResourceName", () => { - const validGroupNames = validULIDs.map((ulid) => `groups/${ulid}`); - - test("should return true for valid group resource names", () => { - validGroupNames.forEach((groupName) => { - expect(isValidGroupResourceName(groupName)).toBe(true); - }); - }); - - test("should return false for invalid group resource names", () => { - const invalidGroupNames = [ - "", // Empty string - "groups/", // Missing ULID - "groups/invalid", // Invalid ULID - "01ARZ3NDEKTSV4YWVF8F5BH32Q", // Missing "groups/" prefix - "users/01ARZ3NDEKTSV4YWVF8F5BH32Q", // Wrong resource type - "Groups/01ARZ3NDEKTSV4YWVF8F5BH32Q", // Wrong case - "groups/01arz3ndektsv4ywvf8f5bh32q", // Lowercase ULID - "groups/01ARZ3NDEKTSV4YWVF8F5BH3", // Too short ULID - "groups/01ARZ3NDEKTSV4YWVF8F5BH321X", // Too long ULID - "groups/01ARZ3NDEKTSV4YWVF8F5BH3!", // Invalid character in ULID - "groups//01ARZ3NDEKTSV4YWVF8F5BH32Q", // Double slash - "/groups/01ARZ3NDEKTSV4YWVF8F5BH32Q", // Leading slash - "groups/01ARZ3NDEKTSV4YWVF8F5BH32Q/", // Trailing slash - ]; - - invalidGroupNames.forEach((groupName) => { - expect(isValidGroupResourceName(groupName)).toBe(false); - }); - }); - }); - - describe("extractULIDFromGroupName", () => { - test("should extract ULID from valid group resource names", () => { - validULIDs.forEach((ulid) => { - const groupName = `groups/${ulid}`; - expect(extractULIDFromGroupName(groupName)).toBe(ulid); - }); - }); - - test("should return null for invalid group resource names", () => { - const invalidGroupNames = [ - "", // Empty string - "groups/", // Missing ULID - "groups/invalid", // Invalid ULID - "01ARZ3NDEKTSV4YWVF8F5BH32", // Missing prefix - "users/01ARZ3NDEKTSV4YWVF8F5BH32", // Wrong resource type - ]; - - invalidGroupNames.forEach((groupName) => { - expect(extractULIDFromGroupName(groupName)).toBeNull(); - }); - }); - }); - - describe("createGroupResourceName", () => { - test("should create valid group resource names from valid ULIDs", () => { - validULIDs.forEach((ulid) => { - const expected = `groups/${ulid}`; - expect(createGroupResourceName(ulid)).toBe(expected); - }); - }); - - test("should return null for invalid ULIDs", () => { - invalidULIDs.forEach((ulid) => { - expect(createGroupResourceName(ulid)).toBeNull(); - }); - }); - }); - - describe("isValidResourceName", () => { - test("should validate group resource names", () => { - validULIDs.forEach((ulid) => { - expect(isValidResourceName(`groups/${ulid}`, "groups")).toBe(true); - }); - }); - - test("should validate api_users resource names", () => { - validULIDs.forEach((ulid) => { - expect(isValidResourceName(`api_users/${ulid}`, "api_users")).toBe( - true - ); - }); - }); - - test("should validate roles resource names", () => { - validULIDs.forEach((ulid) => { - expect(isValidResourceName(`roles/${ulid}`, "roles")).toBe(true); - }); - }); - - test("should return false for mismatched resource types", () => { - const ulid = "01ARZ3NDEKTSV4YWVF8F5BH32Q"; - expect(isValidResourceName(`groups/${ulid}`, "users")).toBe(false); - expect(isValidResourceName(`api_users/${ulid}`, "groups")).toBe(false); - expect(isValidResourceName(`roles/${ulid}`, "api_users")).toBe(false); - }); - - test("should return false for invalid ULIDs", () => { - invalidULIDs.forEach((ulid) => { - expect(isValidResourceName(`groups/${ulid}`, "groups")).toBe(false); - expect(isValidResourceName(`api_users/${ulid}`, "api_users")).toBe( - false - ); - }); - }); - - test("should handle special characters in resource type", () => { - const ulid = "01ARZ3NDEKTSV4YWVF8F5BH32Q"; - // Test with resource types that contain underscores - expect(isValidResourceName(`api_users/${ulid}`, "api_users")).toBe(true); - expect( - isValidResourceName(`some_resource/${ulid}`, "some_resource") - ).toBe(true); - }); - - test("should be case sensitive for resource types", () => { - const ulid = "01ARZ3NDEKTSV4YWVF8F5BH32Q"; - expect(isValidResourceName(`Groups/${ulid}`, "groups")).toBe(false); - expect(isValidResourceName(`groups/${ulid}`, "Groups")).toBe(false); - }); - }); - - describe("integration tests", () => { - test("should work together for complete workflow", () => { - const ulid = "01ARZ3NDEKTSV4YWVF8F5BH32Q"; - - // Validate the ULID - expect(isValidULID(ulid)).toBe(true); - - // Create a group resource name - const groupName = createGroupResourceName(ulid); - expect(groupName).toBe(`groups/${ulid}`); - - // Validate the group resource name - expect(isValidGroupResourceName(groupName!)).toBe(true); - - // Extract the ULID back - const extractedULID = extractULIDFromGroupName(groupName!); - expect(extractedULID).toBe(ulid); - - // Generic validation - expect(isValidResourceName(groupName!, "groups")).toBe(true); - }); - - test("should handle error cases in workflow", () => { - const invalidULID = "invalid"; - - // Invalid ULID should fail validation - expect(isValidULID(invalidULID)).toBe(false); - - // Creating group name should return null - const groupName = createGroupResourceName(invalidULID); - expect(groupName).toBeNull(); - - // Manual invalid group name should fail validation - const invalidGroupName = `groups/${invalidULID}`; - expect(isValidGroupResourceName(invalidGroupName)).toBe(false); - - // Extraction should return null - expect(extractULIDFromGroupName(invalidGroupName)).toBeNull(); - }); - }); -}); diff --git a/ts-web/src/meshtrade/common/validation.ts b/ts-web/src/meshtrade/common/validation.ts deleted file mode 100644 index b8cea400..00000000 --- a/ts-web/src/meshtrade/common/validation.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Generic validation utilities for Meshtrade API resource names and identifiers. - */ - -/** - * Validates if a string is a valid ULID (Universally Unique Lexicographically Sortable Identifier). - * - * Note: This implementation uses a simplified character set for ULIDs that includes - * all uppercase letters A-Z and digits 0-9, unlike the standard ULID specification - * which excludes certain ambiguous characters (I, L, O, U). - * - * ULIDs in this system are 26-character identifiers that are: - * - Lexicographically sortable - * - Uppercase alphanumeric only - * - Contain timestamp information for natural ordering - * - * @param ulid - The string to validate as a ULID - * @returns true if the string is a valid ULID format, false otherwise - * - * @example - * ```typescript - * isValidULID('01ARZ3NDEKTSV4YWVF8F5BH32'); // true - * isValidULID('invalid'); // false - * isValidULID('01arz3ndektsv4ywvf8f5bh32'); // false (lowercase) - * ``` - */ -export function isValidULID(ulid: string): boolean { - return /^[0-9A-Z]{26}$/.test(ulid); -} - -/** - * Validates if a resource name follows the groups/{ulid} format. - * - * Group resource names in the Meshtrade API follow the pattern "groups/{ulid}" - * where {ulid} is a 26-character ULID identifier. - * - * @param resourceName - The resource name string to validate - * @returns true if the resource name is a valid group resource name, false otherwise - * - * @example - * ```typescript - * isValidGroupResourceName('groups/01ARZ3NDEKTSV4YWVF8F5BH32'); // true - * isValidGroupResourceName('groups/invalid'); // false - * isValidGroupResourceName('users/01ARZ3NDEKTSV4YWVF8F5BH32'); // false - * isValidGroupResourceName('01ARZ3NDEKTSV4YWVF8F5BH32'); // false - * ``` - */ -export function isValidGroupResourceName(resourceName: string): boolean { - return /^groups\/[0-9A-Z]{26}$/.test(resourceName); -} - -/** - * Validates a protobuf request message before sending to the server. - * - * This function serves as a client-side validation hook that can be extended - * to include protovalidate integration or other validation logic. - * - * Currently performs basic null/undefined checks. Future enhancements may include - * protovalidate integration for comprehensive message validation. - * - * @param request - The protobuf request message to validate - * @throws {Error} If the request is null or undefined - * - * @example - * ```typescript - * validateRequest(myRequest); // Throws if request is invalid - * ``` - */ -export function validateRequest(request: unknown): void { - if (request === null || request === undefined) { - throw new Error("Request cannot be null or undefined"); - } - // Future: Integrate protovalidate for comprehensive message validation - // For now, basic validation is sufficient as the server also validates -} diff --git a/ts-web/src/meshtrade/config/index.ts b/ts-web/src/meshtrade/config/index.ts new file mode 100644 index 00000000..d13ac669 --- /dev/null +++ b/ts-web/src/meshtrade/config/index.ts @@ -0,0 +1,230 @@ +/** + * Configuration options for Meshtrade API clients using functional options pattern. + * + * Supports flexible authentication modes with optional group context: + * + * 1. **No Authentication** (public APIs): + * ```typescript + * const client = new ServiceNode( + * WithServerUrl("http://localhost:10000") + * ); + * ``` + * + * 2. **API Key Authentication** (backend services): + * ```typescript + * const client = new ServiceNode( + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"), + * WithServerUrl("https://api.example.com") + * ); + * ``` + * + * 3. **JWT Token Authentication** (Next.js backend with user session): + * ```typescript + * const client = new ServiceNode( + * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."), + * WithServerUrl("https://api.example.com") + * ); + * ``` + * + * 4. **JWT with Group Context** (user session with specific group): + * ```typescript + * const client = new ServiceNode( + * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"), + * WithServerUrl("https://api.example.com") + * ); + * ``` + */ + +/** + * Internal configuration class used to build client configuration. + */ +export class ClientConfig { + /** API server URL (default: production) */ + apiServerURL: string = "http://localhost:10000"; + + /** API key for service-to-service authentication */ + apiKey?: string; + + /** JWT token for user session authentication */ + jwtToken?: string; + + /** Group context in format "groups/{ulid}" */ + group?: string; + + /** + * Validates the configuration. + * @throws {Error} If both API key and JWT token are provided (mutually exclusive) + */ + validate(): void { + if (this.apiKey && this.jwtToken) { + throw new Error( + "API key and JWT token authentication are mutually exclusive. " + + "Please use WithAPIKey() OR WithJWTAccessToken(), not both." + ); + } + } +} + +/** + * Client option function type for functional options pattern. + * Each option function modifies the ClientConfig. + */ +export type ClientOption = (config: ClientConfig) => void; + +/** + * Configures the client with an API key for service-to-service authentication. + * + * **Mutually Exclusive**: Cannot be used with WithJWTAccessToken(). + * **Optional**: Can be combined with WithGroup() for group-specific operations. + * + * @param apiKey - The API key for authentication + * @returns A client option function + * + * @example + * ```typescript + * const client = new ServiceNode( + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * ``` + */ +export function WithAPIKey(apiKey: string): ClientOption { + return (config: ClientConfig) => { + if (!apiKey || apiKey.trim() === "") { + throw new Error("API key cannot be empty"); + } + if (config.jwtToken) { + throw new Error( + "Cannot use both WithAPIKey() and WithJWTAccessToken(). " + + "Please choose one authentication method." + ); + } + config.apiKey = apiKey; + }; +} + +/** + * Configures the client with a JWT access token for user session authentication. + * + * **Mutually Exclusive**: Cannot be used with WithAPIKey(). + * **Optional**: Can be combined with WithGroup() for group-specific operations. + * + * The JWT is injected as a cookie header (Cookie: AccessToken=) + * so the server can extract it from the request. + * + * @param token - The JWT access token from the user's session + * @returns A client option function + * + * @example + * ```typescript + * const client = new ServiceNode( + * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * ``` + */ +export function WithJWTAccessToken(token: string): ClientOption { + return (config: ClientConfig) => { + if (!token || token.trim() === "") { + throw new Error("JWT token cannot be empty"); + } + if (config.apiKey) { + throw new Error( + "Cannot use both WithJWTAccessToken() and WithAPIKey(). " + + "Please choose one authentication method." + ); + } + config.jwtToken = token; + }; +} + +/** + * Configures the client with a group context for operations. + * + * **Optional**: Can be used with WithAPIKey() or WithJWTAccessToken(). + * When used alone without authentication, adds group header to requests. + * + * @param group - The group resource name in format "groups/{ulid}" + * @returns A client option function + * + * @example + * ```typescript + * // With API Key + * const client = new ServiceNode( + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * + * // With JWT + * const client = new ServiceNode( + * WithJWTAccessToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * ``` + */ +export function WithGroup(group: string): ClientOption { + return (config: ClientConfig) => { + if (!group || group.trim() === "") { + throw new Error("Group cannot be empty"); + } + config.group = group; + }; +} + +/** + * Configures the client with a custom server URL. + * + * **Optional**: If not provided, defaults to localhost:10000. + * + * @param url - The API server URL + * @returns A client option function + * + * @example + * ```typescript + * const client = new ServiceNode( + * WithServerUrl("http://localhost:10000"), + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32") + * ); + * ``` + */ +export function WithServerUrl(url: string): ClientOption { + return (config: ClientConfig) => { + if (!url || url.trim() === "") { + throw new Error("Server URL cannot be empty"); + } + config.apiServerURL = url; + }; +} + +/** + * Builds client configuration from an array of option functions. + * + * @param opts - Variable number of option functions + * @returns A validated ClientConfig instance + * @throws {Error} If configuration is invalid (e.g., both API key and JWT provided) + * + * @example + * ```typescript + * const config = buildConfigFromOptions( + * WithAPIKey("your-api-key"), + * WithGroup("groups/01ARZ3NDEKTSV4YWVF8F5BH32"), + * WithServerUrl("https://api.example.com") + * ); + * ``` + */ +export function buildConfigFromOptions(...opts: ClientOption[]): ClientConfig { + const config = new ClientConfig(); + + // Apply each option + for (const opt of opts) { + opt(config); + } + + // Validate the final configuration + config.validate(); + + return config; +} diff --git a/ts-web/src/meshtrade/iam/api_user/v1/validation.integration.test.ts b/ts-web/src/meshtrade/iam/api_user/v1/validation.integration.test.ts new file mode 100644 index 00000000..57333a9d --- /dev/null +++ b/ts-web/src/meshtrade/iam/api_user/v1/validation.integration.test.ts @@ -0,0 +1,206 @@ +/** + * Integration tests for APIUserServiceWeb validation + * Tests buf.validate schema validation before network calls + */ + +import { create } from "@bufbuild/protobuf"; +import { + GetAPIUserRequestSchema, + GetAPIUserByKeyHashRequestSchema, + AssignRolesToAPIUserRequestSchema, +} from "./service_pb"; +import { APIUserServiceWeb } from "./service_web_meshts"; +import { WithServerUrl } from "../../../config"; + +describe("APIUserServiceWeb - Request validation (before network call)", () => { + let client: APIUserServiceWeb; + + beforeEach(() => { + // Create client with dummy server URL - validation happens before network call + client = new APIUserServiceWeb(WithServerUrl("http://localhost:9999")); + }); + + describe("GetAPIUserRequest validation", () => { + it("should pass validation with valid request and fail at network layer", async () => { + const request = create(GetAPIUserRequestSchema, { + name: "api_users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + }); + + try { + await client.getAPIUser(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for empty name", async () => { + const request = create(GetAPIUserRequestSchema, { + name: "", + }); + + try { + await client.getAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid ULID format", async () => { + const request = create(GetAPIUserRequestSchema, { + name: "api_users/invalid", + }); + + try { + await client.getAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for lowercase ULID", async () => { + const request = create(GetAPIUserRequestSchema, { + name: "api_users/01arz3ndektsv4ywvf8f5bh3ab", + }); + + try { + await client.getAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); + + describe("GetAPIUserByKeyHashRequest validation", () => { + it("should pass validation with valid base64 key hash and fail at network layer", async () => { + const request = create(GetAPIUserByKeyHashRequestSchema, { + // 44-character base64: 43 base64 chars + 1 '=' padding + keyHash: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ=", + }); + + try { + await client.getAPIUserByKeyHash(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid base64 format", async () => { + const request = create(GetAPIUserByKeyHashRequestSchema, { + keyHash: "not-valid-base64!@#$%", + }); + + try { + await client.getAPIUserByKeyHash(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for empty key hash", async () => { + const request = create(GetAPIUserByKeyHashRequestSchema, { + keyHash: "", + }); + + try { + await client.getAPIUserByKeyHash(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for key hash shorter than 44 characters", async () => { + const request = create(GetAPIUserByKeyHashRequestSchema, { + keyHash: "short", + }); + + try { + await client.getAPIUserByKeyHash(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for key hash longer than 44 characters", async () => { + const request = create(GetAPIUserByKeyHashRequestSchema, { + keyHash: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ=", + }); + + try { + await client.getAPIUserByKeyHash(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); + + describe("AssignRolesToAPIUserRequest validation", () => { + it("should pass validation with valid request and fail at network layer", async () => { + const request = create(AssignRolesToAPIUserRequestSchema, { + name: "api_users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: [ + "groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/1234567", + "groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/12345678", + ], + }); + + try { + await client.assignRolesToAPIUser(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for empty name", async () => { + const request = create(AssignRolesToAPIUserRequestSchema, { + name: "", + roles: ["groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/1234567"], + }); + + try { + await client.assignRolesToAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for empty roles array", async () => { + const request = create(AssignRolesToAPIUserRequestSchema, { + name: "api_users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: [], + }); + + try { + await client.assignRolesToAPIUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); +}); diff --git a/ts-web/src/meshtrade/iam/user/v1/validation.integration.test.ts b/ts-web/src/meshtrade/iam/user/v1/validation.integration.test.ts new file mode 100644 index 00000000..bfe1efca --- /dev/null +++ b/ts-web/src/meshtrade/iam/user/v1/validation.integration.test.ts @@ -0,0 +1,160 @@ +/** + * Integration tests for UserServiceWeb validation + * Tests buf.validate schema validation before network calls + */ + +import { create } from "@bufbuild/protobuf"; +import { + GetUserRequestSchema, + AssignRolesToUserRequestSchema, +} from "./service_pb"; +import { UserServiceWeb } from "./service_web_meshts"; +import { WithServerUrl } from "../../../config"; + +describe("UserServiceWeb - Request validation (before network call)", () => { + let client: UserServiceWeb; + + beforeEach(() => { + // Create client with dummy server URL - validation happens before network call + client = new UserServiceWeb(WithServerUrl("http://localhost:9999")); + }); + + describe("GetUserRequest validation", () => { + it("should pass validation with valid request and fail at network layer", async () => { + const request = create(GetUserRequestSchema, { + name: "users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + }); + + try { + await client.getUser(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for empty name", async () => { + const request = create(GetUserRequestSchema, { + name: "", + }); + + try { + await client.getUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid ULID format", async () => { + const request = create(GetUserRequestSchema, { + name: "users/invalid", + }); + + try { + await client.getUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for lowercase ULID", async () => { + const request = create(GetUserRequestSchema, { + name: "users/01arz3ndektsv4ywvf8f5bh3ab", + }); + + try { + await client.getUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for wrong resource type", async () => { + const request = create(GetUserRequestSchema, { + name: "api_users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + }); + + try { + await client.getUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); + + describe("AssignRolesToUserRequest validation", () => { + it("should pass validation with valid request and fail at network layer", async () => { + const request = create(AssignRolesToUserRequestSchema, { + name: "users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: [ + "groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/1234567", + "groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/12345678", + ], + }); + + try { + await client.assignRolesToUser(request); + fail("Expected network error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + // Validation passed - should get network error, not validation error + expect((error as Error).message).not.toContain("Validation failed"); + } + }); + + it("should throw validation error for empty name", async () => { + const request = create(AssignRolesToUserRequestSchema, { + name: "", + roles: ["groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/1234567"], + }); + + try { + await client.assignRolesToUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for empty roles array", async () => { + const request = create(AssignRolesToUserRequestSchema, { + name: "users/01ARZ3NDEKTSV4YWVF8F5BH3AB", + roles: [], + }); + + try { + await client.assignRolesToUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + + it("should throw validation error for invalid ULID in name", async () => { + const request = create(AssignRolesToUserRequestSchema, { + name: "users/invalid", + roles: ["groups/01ARZ3NDEKTSV4YWVF8F5BH3AB/roles/1234567"], + }); + + try { + await client.assignRolesToUser(request); + fail("Expected validation error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("Validation failed"); + } + }); + }); +}); diff --git a/ts-node/src/meshtrade/common/connectInterceptors.ts b/ts-web/src/meshtrade/interceptors/index.ts similarity index 60% rename from ts-node/src/meshtrade/common/connectInterceptors.ts rename to ts-web/src/meshtrade/interceptors/index.ts index 6fc19cbb..7531daa9 100644 --- a/ts-node/src/meshtrade/common/connectInterceptors.ts +++ b/ts-web/src/meshtrade/interceptors/index.ts @@ -7,7 +7,6 @@ */ import { Interceptor } from "@connectrpc/connect"; -import { isValidGroupResourceName } from "./validation"; /** * HTTP header names for authentication. @@ -18,6 +17,16 @@ const GROUP_HEADER = "x-group"; const COOKIE_HEADER = "cookie"; const ACCESS_TOKEN_COOKIE_NAME = "AccessToken"; +/** + * Validates if a resource name follows the groups/{ulid} format. + * + * @param resourceName - The resource name string to validate + * @returns true if the resource name is a valid group resource name, false otherwise + */ +function isValidGroupResourceName(resourceName: string): boolean { + return /^groups\/[0-9A-Z]{26}$/.test(resourceName); +} + /** * Creates a Connect-ES interceptor that injects operating group context * into API requests by adding an `x-group` header. @@ -62,10 +71,7 @@ export function createGroupInterceptor( // Create the interceptor function const interceptor: Interceptor = (next) => async (req) => { - // Add the x-group header to the request - req.header.set("x-group", group); - - // Call the next interceptor in the chain + req.header.set(GROUP_HEADER, group); return await next(req); }; @@ -75,129 +81,47 @@ export function createGroupInterceptor( } /** - * Creates a Connect-ES interceptor that injects API key authentication - * into API requests by adding `x-api-key` and `x-group` headers. - * - * This authentication mode is used for service-to-service communication - * where a backend service authenticates using an API key and operates - * within a specific group context. - * - * Both the API key and group are required and validated. The group must - * follow the resource name format: `groups/{ulid}` where {ulid} is a - * 26-character ULID. + * Creates a Connect-ES interceptor that injects API key authentication. * * @param apiKey - The API key for authentication - * @param group - The group resource name in format `groups/{ulid}` - * @returns An interceptor function that adds authentication headers to all requests - * @throws {Error} If apiKey is empty or group format is invalid - * - * @example - * ```typescript - * const authInterceptor = createApiKeyInterceptor( - * 'your-api-key', - * 'groups/01ARZ3NDEKTSV4YWVF8F5BH32' - * ); - * - * const transport = createGrpcTransport({ - * baseUrl: 'https://api.example.com', - * interceptors: [authInterceptor] - * }); - * ``` + * @returns An interceptor that adds x-api-key header + * @throws {Error} If apiKey is empty */ export function createApiKeyInterceptor( - apiKey: string, - group: string -): Interceptor & { apiKeyAuth: true; groupContext: string } { - // Validate inputs + apiKey: string +): Interceptor & { apiKeyAuth: true } { if (!apiKey || apiKey.trim() === "") { throw new Error("API key cannot be empty"); } - if (!isValidGroupResourceName(group)) { - throw new Error( - `Invalid group format: "${group}". Group must be in the format "groups/{ulid}" ` + - `where {ulid} is a 26-character ULID (e.g., "groups/01ARZ3NDEKTSV4YWVF8F5BH32").` - ); - } - - // Create the interceptor function const interceptor: Interceptor = (next) => async (req) => { - // Add authentication headers to the request req.header.set(API_KEY_HEADER, apiKey); - req.header.set(GROUP_HEADER, group); - - // Call the next interceptor in the chain return await next(req); }; - // Add marker properties for identification - return Object.assign(interceptor, { - apiKeyAuth: true as const, - groupContext: group, - }); + return Object.assign(interceptor, { apiKeyAuth: true as const }); } /** - * Creates a Connect-ES interceptor that injects JWT token authentication - * into API requests by adding a `Cookie` header with the AccessToken. - * - * This authentication mode is used in Next.js backends where the server - * has access to the user's JWT token from their browser session. The JWT - * is injected as a cookie so the server can extract it in the same way - * it would from a browser request. - * - * The JWT token is added as: `Cookie: AccessToken=` - * - * This allows the server-side authentication middleware to extract it as: - * ```go - * if cookieHeader := request.Attributes.Request.Http.Headers["cookie"]; cookieHeader != "" { - * cookies := parseHTTPCookies(cookieHeader) - * for _, cookie := range cookies { - * if cookie.Name == "AccessToken" && cookie.Value != "" { - * authContext.AccessToken = cookie.Value - * break - * } - * } - * } - * ``` + * Creates a Connect-ES interceptor that injects JWT token authentication. * * @param jwtToken - The JWT token from the user's session - * @returns An interceptor function that adds the JWT as a cookie header + * @returns An interceptor that adds AccessToken cookie * @throws {Error} If jwtToken is empty - * - * @example - * ```typescript - * // In a Next.js API route - * const authInterceptor = createJwtInterceptor( - * 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' - * ); - * - * const transport = createGrpcTransport({ - * baseUrl: 'https://api.example.com', - * interceptors: [authInterceptor] - * }); - * ``` */ export function createJwtInterceptor( jwtToken: string ): Interceptor & { jwtAuth: true } { - // Validate input if (!jwtToken || jwtToken.trim() === "") { throw new Error("JWT token cannot be empty"); } - // Create the interceptor function const interceptor: Interceptor = (next) => async (req) => { - // Add JWT as a cookie header - // Format: "Cookie: AccessToken=" const cookieValue = `${ACCESS_TOKEN_COOKIE_NAME}=${jwtToken}`; req.header.set(COOKIE_HEADER, cookieValue); - - // Call the next interceptor in the chain return await next(req); }; - // Add marker property for identification return Object.assign(interceptor, { jwtAuth: true as const }); } @@ -226,7 +150,7 @@ export function createLoggingInterceptor(): Interceptor { }); // Log the request - console.log(`[Connect] ${req.method.name} request:`, { + console.debug(`[Connect] ${req.method.name} request:`, { service: req.service.typeName, method: req.method.name, headers, @@ -237,7 +161,7 @@ export function createLoggingInterceptor(): Interceptor { const response = await next(req); // Log successful response - console.log(`[Connect] ${req.method.name} response:`, { + console.debug(`[Connect] ${req.method.name} response:`, { service: req.service.typeName, method: req.method.name, status: "success", diff --git a/ts-web/tsconfig.build.json b/ts-web/tsconfig.build.json new file mode 100644 index 00000000..ce1a9d17 --- /dev/null +++ b/ts-web/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "dist", + "**/*.test.ts", + "**/*.spec.ts" + ] +} diff --git a/ts-web/tsconfig.json b/ts-web/tsconfig.json index 590a2b78..cd289874 100644 --- a/ts-web/tsconfig.json +++ b/ts-web/tsconfig.json @@ -10,6 +10,8 @@ "ES2020", "DOM" ], + // Include Jest types for test file type checking in editors + "types": ["jest"], // --- Library Build Settings --- // Generates .d.ts files so other TypeScript projects can use your library. ESSENTIAL. "declaration": true, @@ -32,10 +34,9 @@ "include": [ "src/**/*.ts" ], - // Tells TypeScript to ignore its own output directory and test files. + // Tells TypeScript to ignore its own output directory. + // Note: Test files are included for editor type checking but excluded from build via separate config. "exclude": [ - "dist", - "**/*.test.ts", - "**/*.spec.ts" + "dist" ] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2e59bd8b..f85364f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1210,6 +1210,18 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e" integrity sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw== +"@bufbuild/cel-spec@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@bufbuild/cel-spec/-/cel-spec-0.3.0.tgz#05fa54cb6c8d1f6053aca40687ff976ba40e2f2f" + integrity sha512-mN669LGlXkYNco6NzSTpFoW52UwGb0h5UJNct43nkOjk9YrgUtzcBn9PfjrwbyAe3OlUtasvXAFf1Tjs3NQLOg== + +"@bufbuild/cel@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@bufbuild/cel/-/cel-0.3.0.tgz#0ebb8f4fdc03449de79b25767f45779e1586eec5" + integrity sha512-vIdcn0Ot6XDKakcDqEQvvlCtMlYwLlxc++SrVjjCmYIiZRH+tlr1GRYpe5R9kguSiTS3BLh7C+I7ZoektVPICQ== + dependencies: + "@bufbuild/cel-spec" "0.3.0" + "@bufbuild/protobuf@2.9.0": version "2.9.0" resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.9.0.tgz#ff8827be3d8e56d74a03530cff8b0e1952aa115e" @@ -1229,6 +1241,13 @@ "@typescript/vfs" "^1.5.2" typescript "5.4.5" +"@bufbuild/protovalidate@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@bufbuild/protovalidate/-/protovalidate-1.0.0.tgz#6d8660829b4f9c4f7127741f76a6a52423224910" + integrity sha512-ICGANMQXaPKdR5BJ+6/L3nySHOZQQEZQvvivSZCFb799138obPLjNk32rSIKOMrA/YHc4Y2W738e+SL3CbZXSg== + dependencies: + "@bufbuild/cel" "0.3.0" + "@chevrotain/cst-dts-gen@11.0.3": version "11.0.3" resolved "https://registry.yarnpkg.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz#5e0863cc57dc45e204ccfee6303225d15d9d4783"