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"