feat(clients): CLI-04 typed single-item command parity (+CLI-30 unregister) — 4/5 clients
Every parity-critical single-item MXAccess command now has a typed session helper instead of only a raw-Invoke escape hatch. Added per client: - Phase 1: AdviseSupervisory, WriteSecured, WriteSecured2, AuthenticateUser, ArchestrAUserToId - Phase 2: AddBufferedItem, SetBufferedUpdateInterval, Suspend, Activate - CLI-30: Unregister (Rust + .NET; Go/Python already had it) Each wraps the existing raw-command machinery (no new wire surface) and runs the same MXAccess-level reply validation (hresult < 0 + MxStatusProxy). MXAccess parity preserved: WriteSecured before AuthenticateUser+AdviseSupervisory surfaces the native failure unchanged (not pre-validated/reordered). Credentials (AuthenticateUser password, WriteSecured payloads) route through each client's secret-redaction seam and never reach logs/exceptions/ToString/Debug/Display; each suite asserts a distinctive credential is absent from surfaced errors. New CLI subcommands source credentials via flag/env, never echoed. - .NET: 21 helpers (validated + Raw), CLI subcommands, multi-secret CLI redactor. Build clean (0 warn), 102 passed. - Go: 9 helpers + *Raw variants, redactSecrets seam, promoted CLI advise-supervisory to typed. gofmt/vet/build/test clean. - Rust: 10 helpers incl. unregister; verified ensure_mxaccess_success runs on secured paths; error.rs credential scrub. fmt/check/test/clippy clean. - Python: 9 async helpers, redact_secret seam + _invoke_redacted, CLI commands. 145 passed. - Shared doc: ClientLibrariesDesign "Typed Command Parity" section. Java client typed parity is batched to windev (no local JRE); CLI-04 + CLI-30 stay open until it lands. Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
This commit is contained in:
@@ -706,6 +706,277 @@ func (s *Session) Write2Raw(ctx context.Context, serverHandle, itemHandle int32,
|
||||
})
|
||||
}
|
||||
|
||||
// AdviseSupervisory invokes MXAccess AdviseSupervisory, advising an item on the
|
||||
// supervisory (as opposed to runtime) data path.
|
||||
func (s *Session) AdviseSupervisory(ctx context.Context, serverHandle, itemHandle int32) error {
|
||||
_, err := s.AdviseSupervisoryRaw(ctx, serverHandle, itemHandle)
|
||||
return err
|
||||
}
|
||||
|
||||
// AdviseSupervisoryRaw invokes MXAccess AdviseSupervisory and returns the raw reply.
|
||||
func (s *Session) AdviseSupervisoryRaw(ctx context.Context, serverHandle, itemHandle int32) (*MxCommandReply, error) {
|
||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY,
|
||||
Payload: &pb.MxCommand_AdviseSupervisory{
|
||||
AdviseSupervisory: &pb.AdviseSupervisoryCommand{
|
||||
ServerHandle: serverHandle,
|
||||
ItemHandle: itemHandle,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// WriteSecured invokes MXAccess WriteSecured (secured single-item write).
|
||||
//
|
||||
// The value is credential-sensitive: callers must not log it, and any error this
|
||||
// call surfaces has the value's string content redacted (see WriteSecuredRaw).
|
||||
// MXAccess parity is preserved — WriteSecured legitimately fails when it is not
|
||||
// preceded by a matching AuthenticateUser + AdviseSupervisory or when the body
|
||||
// carries no value; that native failure is surfaced as-is, never pre-empted or
|
||||
// reordered by this client.
|
||||
func (s *Session) WriteSecured(ctx context.Context, serverHandle, itemHandle, currentUserID, verifierUserID int32, value *MxValue) error {
|
||||
_, err := s.WriteSecuredRaw(ctx, serverHandle, itemHandle, currentUserID, verifierUserID, value)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteSecuredRaw invokes MXAccess WriteSecured and returns the raw reply. Any
|
||||
// surfaced error is routed through the client's secret-redaction seam so a string
|
||||
// write value never appears in error text.
|
||||
func (s *Session) WriteSecuredRaw(ctx context.Context, serverHandle, itemHandle, currentUserID, verifierUserID int32, value *MxValue) (*MxCommandReply, error) {
|
||||
if value == nil {
|
||||
return nil, errors.New("mxgateway: write-secured value is required")
|
||||
}
|
||||
|
||||
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
|
||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE_SECURED,
|
||||
Payload: &pb.MxCommand_WriteSecured{
|
||||
WriteSecured: &pb.WriteSecuredCommand{
|
||||
ServerHandle: serverHandle,
|
||||
ItemHandle: itemHandle,
|
||||
CurrentUserId: currentUserID,
|
||||
VerifierUserId: verifierUserID,
|
||||
Value: value,
|
||||
},
|
||||
},
|
||||
})
|
||||
return reply, redactSecrets(err, stringSecrets(value)...)
|
||||
}
|
||||
|
||||
// WriteSecured2 invokes MXAccess WriteSecured2 (secured, timestamped single-item write).
|
||||
//
|
||||
// Like WriteSecured, the value is credential-sensitive and its string content is
|
||||
// scrubbed from any surfaced error. Native parity failures (missing prior
|
||||
// AuthenticateUser/AdviseSupervisory, value-less body) are surfaced unchanged.
|
||||
func (s *Session) WriteSecured2(ctx context.Context, serverHandle, itemHandle, currentUserID, verifierUserID int32, value, timestampValue *MxValue) error {
|
||||
_, err := s.WriteSecured2Raw(ctx, serverHandle, itemHandle, currentUserID, verifierUserID, value, timestampValue)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteSecured2Raw invokes MXAccess WriteSecured2 and returns the raw reply. Any
|
||||
// surfaced error is routed through the client's secret-redaction seam.
|
||||
func (s *Session) WriteSecured2Raw(ctx context.Context, serverHandle, itemHandle, currentUserID, verifierUserID int32, value, timestampValue *MxValue) (*MxCommandReply, error) {
|
||||
if value == nil {
|
||||
return nil, errors.New("mxgateway: write-secured2 value is required")
|
||||
}
|
||||
if timestampValue == nil {
|
||||
return nil, errors.New("mxgateway: write-secured2 timestamp value is required")
|
||||
}
|
||||
|
||||
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
|
||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE_SECURED2,
|
||||
Payload: &pb.MxCommand_WriteSecured2{
|
||||
WriteSecured2: &pb.WriteSecured2Command{
|
||||
ServerHandle: serverHandle,
|
||||
ItemHandle: itemHandle,
|
||||
CurrentUserId: currentUserID,
|
||||
VerifierUserId: verifierUserID,
|
||||
Value: value,
|
||||
TimestampValue: timestampValue,
|
||||
},
|
||||
},
|
||||
})
|
||||
return reply, redactSecrets(err, stringSecrets(value)...)
|
||||
}
|
||||
|
||||
// AuthenticateUser invokes MXAccess AuthenticateUser and returns the resolved
|
||||
// MXAccess user id.
|
||||
//
|
||||
// verifyUserPassword is a raw MXAccess credential: this client never logs it and
|
||||
// scrubs it from any error it surfaces (see AuthenticateUserRaw). Callers must
|
||||
// likewise keep it out of their own logs, metrics, and diagnostics.
|
||||
func (s *Session) AuthenticateUser(ctx context.Context, serverHandle int32, verifyUser, verifyUserPassword string) (int32, error) {
|
||||
reply, err := s.AuthenticateUserRaw(ctx, serverHandle, verifyUser, verifyUserPassword)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if reply.GetAuthenticateUser() != nil {
|
||||
return reply.GetAuthenticateUser().GetUserId(), nil
|
||||
}
|
||||
return reply.GetReturnValue().GetInt32Value(), nil
|
||||
}
|
||||
|
||||
// AuthenticateUserRaw invokes MXAccess AuthenticateUser and returns the raw
|
||||
// reply. The credential is scrubbed from any surfaced error via the client's
|
||||
// secret-redaction seam, so even a gateway diagnostic echoing the password back
|
||||
// cannot leak it through this call's error.
|
||||
func (s *Session) AuthenticateUserRaw(ctx context.Context, serverHandle int32, verifyUser, verifyUserPassword string) (*MxCommandReply, error) {
|
||||
if verifyUser == "" {
|
||||
return nil, errors.New("mxgateway: verify user is required")
|
||||
}
|
||||
|
||||
reply, err := s.invokeCommand(ctx, &pb.MxCommand{
|
||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_AUTHENTICATE_USER,
|
||||
Payload: &pb.MxCommand_AuthenticateUser{
|
||||
AuthenticateUser: &pb.AuthenticateUserCommand{
|
||||
ServerHandle: serverHandle,
|
||||
VerifyUser: verifyUser,
|
||||
VerifyUserPassword: verifyUserPassword,
|
||||
},
|
||||
},
|
||||
})
|
||||
return reply, redactSecrets(err, verifyUserPassword)
|
||||
}
|
||||
|
||||
// ArchestrAUserToId invokes MXAccess ArchestrAUserToId, resolving an ArchestrA
|
||||
// user GUID to its MXAccess integer user id.
|
||||
func (s *Session) ArchestrAUserToId(ctx context.Context, serverHandle int32, userIDGuid string) (int32, error) {
|
||||
reply, err := s.ArchestrAUserToIdRaw(ctx, serverHandle, userIDGuid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if reply.GetArchestraUserToId() != nil {
|
||||
return reply.GetArchestraUserToId().GetUserId(), nil
|
||||
}
|
||||
return reply.GetReturnValue().GetInt32Value(), nil
|
||||
}
|
||||
|
||||
// ArchestrAUserToIdRaw invokes MXAccess ArchestrAUserToId and returns the raw reply.
|
||||
func (s *Session) ArchestrAUserToIdRaw(ctx context.Context, serverHandle int32, userIDGuid string) (*MxCommandReply, error) {
|
||||
if userIDGuid == "" {
|
||||
return nil, errors.New("mxgateway: user id GUID is required")
|
||||
}
|
||||
|
||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ARCHESTRA_USER_TO_ID,
|
||||
Payload: &pb.MxCommand_ArchestraUserToId{
|
||||
ArchestraUserToId: &pb.ArchestrAUserToIdCommand{
|
||||
ServerHandle: serverHandle,
|
||||
UserIdGuid: userIDGuid,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// AddBufferedItem invokes MXAccess AddBufferedItem and returns the item handle.
|
||||
func (s *Session) AddBufferedItem(ctx context.Context, serverHandle int32, itemDefinition, itemContext string) (int32, error) {
|
||||
reply, err := s.AddBufferedItemRaw(ctx, serverHandle, itemDefinition, itemContext)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if reply.GetAddBufferedItem() != nil {
|
||||
return reply.GetAddBufferedItem().GetItemHandle(), nil
|
||||
}
|
||||
return reply.GetReturnValue().GetInt32Value(), nil
|
||||
}
|
||||
|
||||
// AddBufferedItemRaw invokes MXAccess AddBufferedItem and returns the raw reply.
|
||||
func (s *Session) AddBufferedItemRaw(ctx context.Context, serverHandle int32, itemDefinition, itemContext string) (*MxCommandReply, error) {
|
||||
if itemDefinition == "" {
|
||||
return nil, errors.New("mxgateway: item definition is required")
|
||||
}
|
||||
|
||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADD_BUFFERED_ITEM,
|
||||
Payload: &pb.MxCommand_AddBufferedItem{
|
||||
AddBufferedItem: &pb.AddBufferedItemCommand{
|
||||
ServerHandle: serverHandle,
|
||||
ItemDefinition: itemDefinition,
|
||||
ItemContext: itemContext,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SetBufferedUpdateInterval invokes MXAccess SetBufferedUpdateInterval.
|
||||
func (s *Session) SetBufferedUpdateInterval(ctx context.Context, serverHandle, updateIntervalMilliseconds int32) error {
|
||||
_, err := s.SetBufferedUpdateIntervalRaw(ctx, serverHandle, updateIntervalMilliseconds)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetBufferedUpdateIntervalRaw invokes MXAccess SetBufferedUpdateInterval and returns the raw reply.
|
||||
func (s *Session) SetBufferedUpdateIntervalRaw(ctx context.Context, serverHandle, updateIntervalMilliseconds int32) (*MxCommandReply, error) {
|
||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_SET_BUFFERED_UPDATE_INTERVAL,
|
||||
Payload: &pb.MxCommand_SetBufferedUpdateInterval{
|
||||
SetBufferedUpdateInterval: &pb.SetBufferedUpdateIntervalCommand{
|
||||
ServerHandle: serverHandle,
|
||||
UpdateIntervalMilliseconds: updateIntervalMilliseconds,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Suspend invokes MXAccess Suspend and returns the resulting item status.
|
||||
func (s *Session) Suspend(ctx context.Context, serverHandle, itemHandle int32) (*MxStatusProxy, error) {
|
||||
reply, err := s.SuspendRaw(ctx, serverHandle, itemHandle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reply.GetSuspend().GetStatus(), nil
|
||||
}
|
||||
|
||||
// SuspendRaw invokes MXAccess Suspend and returns the raw reply.
|
||||
func (s *Session) SuspendRaw(ctx context.Context, serverHandle, itemHandle int32) (*MxCommandReply, error) {
|
||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_SUSPEND,
|
||||
Payload: &pb.MxCommand_Suspend{
|
||||
Suspend: &pb.SuspendCommand{
|
||||
ServerHandle: serverHandle,
|
||||
ItemHandle: itemHandle,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Activate invokes MXAccess Activate and returns the resulting item status.
|
||||
func (s *Session) Activate(ctx context.Context, serverHandle, itemHandle int32) (*MxStatusProxy, error) {
|
||||
reply, err := s.ActivateRaw(ctx, serverHandle, itemHandle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reply.GetActivate().GetStatus(), nil
|
||||
}
|
||||
|
||||
// ActivateRaw invokes MXAccess Activate and returns the raw reply.
|
||||
func (s *Session) ActivateRaw(ctx context.Context, serverHandle, itemHandle int32) (*MxCommandReply, error) {
|
||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ACTIVATE,
|
||||
Payload: &pb.MxCommand_Activate{
|
||||
Activate: &pb.ActivateCommand{
|
||||
ServerHandle: serverHandle,
|
||||
ItemHandle: itemHandle,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// stringSecrets collects the non-empty string content of the given values so it
|
||||
// can be scrubbed from surfaced errors. Only string-typed MxValues carry
|
||||
// scrubable text; non-string values (numbers, timestamps, arrays) contribute
|
||||
// nothing.
|
||||
func stringSecrets(values ...*MxValue) []string {
|
||||
var secrets []string
|
||||
for _, value := range values {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
if stringValue, ok := value.GetKind().(*pb.MxValue_StringValue); ok && stringValue.StringValue != "" {
|
||||
secrets = append(secrets, stringValue.StringValue)
|
||||
}
|
||||
}
|
||||
return secrets
|
||||
}
|
||||
|
||||
// Events streams ordered session events until the server ends the stream,
|
||||
// context cancellation stops Recv, or a terminal error is sent.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user