Every RPC failure surfaced as a GatewayError with no way to tell a rejected key from a missing scope, though docs/ClientLibrariesDesign.md requires the two be treated distinctly. GatewayError now matches the new sentinels ErrUnauthenticated and ErrPermissionDenied via errors.Is (by gRPC status code only, so it works through the secret-redaction wrapper) and exposes Code(). Tests: errors_auth_test.go — sentinel matching per code, Code(), and a bufconn AcknowledgeAlarm round trip proving the match survives the wire and the password stays redacted (both behavioural tests fail without GatewayError.Is).
5.9 KiB
Go Client Detailed Design
Purpose
Provide an idiomatic Go client module for MXAccess Gateway, plus a test CLI and unit tests. The Go client should be suitable for services and command-line automation.
Follow the Go Style Guide for handwritten code and the Protobuf Style Guide for generated contract inputs.
Module Layout
Recommended layout:
clients/go/
go.mod
mxgateway/
client.go
session.go
options.go
auth.go
values.go
errors.go
internal/generated/
mxaccess_gateway.pb.go
mxaccess_gateway_grpc.pb.go
cmd/mxgw-go/
main.go
tests/
Generated code should come from protoc plus:
protoc-gen-goprotoc-gen-go-grpc
Library API
Suggested API:
type Client struct {
// owns grpc.ClientConn
}
type Options struct {
Endpoint string
APIKey string
Plaintext bool
CACertFile string
ServerNameOverride string
DialTimeout time.Duration
CallTimeout time.Duration
}
func Dial(ctx context.Context, opts Options) (*Client, error)
func (c *Client) OpenSession(ctx context.Context, opts OpenSessionOptions) (*Session, error)
func (c *Client) Invoke(ctx context.Context, req *pb.MxCommandRequest) (*pb.MxCommandReply, error)
func (c *Client) Close() error
Session:
type Session struct {
ID string
}
func (s *Session) Register(ctx context.Context, clientName string) (int32, error)
func (s *Session) Unregister(ctx context.Context, serverHandle int32) error
func (s *Session) AddItem(ctx context.Context, serverHandle int32, item string) (int32, error)
func (s *Session) AddItem2(ctx context.Context, serverHandle int32, item, context string) (int32, error)
func (s *Session) Advise(ctx context.Context, serverHandle, itemHandle int32) error
func (s *Session) AddItemBulk(ctx context.Context, serverHandle int32, tagAddresses []string) ([]*pb.SubscribeResult, error)
func (s *Session) AdviseItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
func (s *Session) RemoveItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
func (s *Session) UnAdviseItemBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
func (s *Session) SubscribeBulk(ctx context.Context, serverHandle int32, tagAddresses []string) ([]*pb.SubscribeResult, error)
func (s *Session) UnsubscribeBulk(ctx context.Context, serverHandle int32, itemHandles []int32) ([]*pb.SubscribeResult, error)
func (s *Session) Write(ctx context.Context, serverHandle, itemHandle int32, value Value, userID int32) error
func (s *Session) Events(ctx context.Context) (<-chan EventResult, error)
func (s *Session) Close(ctx context.Context) error
Authentication
Use a unary and stream interceptor to attach:
authorization: Bearer <api key>
The interceptor should use metadata.AppendToOutgoingContext or call options.
Do not print API keys in errors.
TLS
Support:
credentials/insecurefor local plaintext,credentials.NewClientTLSFromFile,- custom
tls.Configfor advanced callers.
Trust posture
The gateway can serve a self-signed certificate it generates itself (it has no
PKI). To make that usable, TLS is lenient by default: when Plaintext is
false and no CACertFile/TLSConfig/TransportCredentials is supplied,
buildCredentials dials with tls.Config{InsecureSkipVerify: true} (carrying
ServerNameOverride as the SNI when set), so the gateway's self-signed
certificate is accepted without verification.
To verify the gateway instead:
- set
CACertFileto pin a CA (full verification against that root), or - set
RequireCertificateValidation: trueto verify against the OS/system trust roots without pinning.
Pinning a CA always wins over the lenient default.
Streaming
Events(ctx) should return a receive channel of:
type EventResult struct {
Event *pb.MxEvent
Err error
}
The receive goroutine exits on stream end, context cancellation, or error. The channel should be closed exactly once. Do not reorder events.
Error Handling
Expose typed errors:
type GatewayError struct { ... }
type CommandError struct { ... }
type MxAccessError struct { ... }
Use errors.Is / errors.As support. Preserve raw protobuf replies on command
errors. GatewayError matches the sentinels ErrUnauthenticated and
ErrPermissionDenied through errors.Is by gRPC status code, so callers treat
the two authorization failures distinctly (docs/ClientLibrariesDesign.md)
without parsing messages.
Test CLI
Binary: mxgw-go.
Recommended commands:
mxgw-go version
mxgw-go smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestChildObject.TestInt
mxgw-go write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123
mxgw-go stream-events --session-id <id> --json
Recommended CLI library:
- standard
flagfor minimalism, or - Cobra if subcommand ergonomics matter.
Unit Tests
Use bufconn for in-memory gRPC tests.
Required tests:
- auth interceptor on unary calls,
- auth interceptor on streaming calls,
- plaintext and TLS dial options,
- command helper request construction,
- value conversion,
- status conversion,
- typed error wrapping,
- stream channel closes on cancellation,
- late stream error propagation,
- CLI JSON redaction.
Integration Tests
Use Go build tags or environment skip:
MXGATEWAY_INTEGRATION=1
Integration test should run OpenSession, Register, AddItem, Advise,
bounded StreamEvents, and CloseSession.