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:
@@ -3,10 +3,68 @@ package mxgateway
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||
)
|
||||
|
||||
// redactedSecretMarker is the placeholder substituted for credential material in
|
||||
// surfaced error text. It matches the marker used by RedactAPIKey so the client
|
||||
// presents one consistent redaction shape everywhere secrets could otherwise
|
||||
// leak.
|
||||
const redactedSecretMarker = "<redacted>"
|
||||
|
||||
// secretRedactingError wraps a typed error so any occurrence of a known
|
||||
// credential in the underlying message is replaced with redactedSecretMarker in
|
||||
// the surfaced text. Unwrap still exposes the wrapped error, so errors.As /
|
||||
// errors.Is continue to reach the underlying MxAccessError, CommandError, or
|
||||
// GatewayError. This is the seam that keeps AuthenticateUser credentials and
|
||||
// WriteSecured/WriteSecured2 payload strings out of any error a caller might log,
|
||||
// even if a gateway diagnostic message were to echo them back.
|
||||
type secretRedactingError struct {
|
||||
err error
|
||||
secrets []string
|
||||
}
|
||||
|
||||
// Error returns the wrapped error's message with every non-empty secret redacted.
|
||||
func (e *secretRedactingError) Error() string {
|
||||
if e == nil || e.err == nil {
|
||||
return ""
|
||||
}
|
||||
message := e.err.Error()
|
||||
for _, secret := range e.secrets {
|
||||
if secret != "" {
|
||||
message = strings.ReplaceAll(message, secret, redactedSecretMarker)
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// Unwrap returns the wrapped error so typed-error inspection still works through
|
||||
// the redaction wrapper.
|
||||
func (e *secretRedactingError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.err
|
||||
}
|
||||
|
||||
// redactSecrets wraps err so any occurrence of a non-empty secret in the surfaced
|
||||
// message is redacted, while errors.As / errors.Is still reach the wrapped typed
|
||||
// error. It returns nil unchanged and skips wrapping when no non-empty secret is
|
||||
// supplied, so non-secret-bearing calls keep their original error verbatim.
|
||||
func redactSecrets(err error, secrets ...string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
if secret != "" {
|
||||
return &secretRedactingError{err: err, secrets: secrets}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrSlowConsumer is the terminal error sent on the Events/EventsAfter
|
||||
// (cancel-when-full) path when the buffered results channel overflows because
|
||||
// the consumer fell behind. It is delivered as the final EventResult.Err before
|
||||
|
||||
Reference in New Issue
Block a user