From 38e238286a292a3fb71aed60b9f4c31514bcc2b8 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Tue, 7 Jul 2026 14:13:24 +0200 Subject: [PATCH 1/2] Move errorutil into API. --- go.mod | 2 + go.sum | 8 ++ go/errorutil/errors.go | 244 ++++++++++++++++++++++++++++++++++ go/errorutil/errors_test.go | 257 ++++++++++++++++++++++++++++++++++++ 4 files changed, 511 insertions(+) create mode 100644 go/errorutil/errors.go create mode 100644 go/errorutil/errors_test.go diff --git a/go.mod b/go.mod index c142ed5f..9345be35 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/uuid v1.6.0 github.com/klauspost/connect-compress/v2 v2.1.1 github.com/stretchr/testify v1.11.1 + google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 ) @@ -28,6 +29,7 @@ require ( github.com/stretchr/objx v0.5.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect + golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.38.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect diff --git a/go.sum b/go.sum index 129d64be..039c591a 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.29.1 h1:8Gx3S5HPop5XPOHtZI8vkaGk+DBW/nn9fkpNPbh5dBk= github.com/google/cel-go v0.29.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -53,12 +55,18 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU= google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go/errorutil/errors.go b/go/errorutil/errors.go new file mode 100644 index 00000000..5a9988f8 --- /dev/null +++ b/go/errorutil/errors.go @@ -0,0 +1,244 @@ +package errorutil + +import ( + "errors" + "fmt" + "strings" + + "connectrpc.com/connect" + "google.golang.org/grpc/status" + + "github.com/google/go-cmp/cmp" +) + +// Convert compares the error and maps it to an appropriate connect.Error. +// If there is no backend-specific wrapped error given to this function, it will return an internal error. +// If there are multiple errors wrapped, this function only cares about the first found error in the error tree. +func Convert(err error) *connect.Error { + // Ipam or other connect errors + if connectErr, ok := errors.AsType[*connect.Error](err); ok { + // when the connect error is wrapped deeper a tree, connect.Error() calls the string function on + // the error and adds things like "internal: ..." + // so we replace the wrapped error message with the direct message + cleaned := strings.Replace(err.Error(), connectErr.Error(), connectErr.Message(), 1) + return connect.NewError(connectErr.Code(), errors.New(cleaned)) + } + + // for other pure grpc apis + if _, ok := status.FromError(err); ok { + // when the grpc error is wrapped deeper a tree, status.FromError calls the string function on + // the error and adds "rpc error: ..." + // so we unwrap the grpc status on our own and replace the error message with the direct message + type grpcstatus interface{ GRPCStatus() *status.Status } + var ( + iterErr = err + ) + + for { + st, ok := iterErr.(grpcstatus) + if ok { + cleaned := strings.Replace(err.Error(), st.GRPCStatus().String(), st.GRPCStatus().Message(), 1) + return connect.NewError(connect.Code(st.GRPCStatus().Code()), errors.New(cleaned)) + } + + iterErr = errors.Unwrap(iterErr) + if iterErr == nil { + // just a theoretical case + return connect.NewError(connect.CodeUnknown, err) + } + } + } + + return connect.NewError(connect.CodeInternal, err) +} + +// NotFound creates a new Not Found error (Connect Code 5) with a formatted error message. +func NotFound(format string, args ...any) error { + return connect.NewError(connect.CodeNotFound, fmt.Errorf(format, args...)) +} + +// Conflict creates a new Already Exists error (Connect Code 6) with a formatted error message. +func Conflict(format string, args ...any) error { + return connect.NewError(connect.CodeAlreadyExists, fmt.Errorf(format, args...)) +} + +// Internal creates a new Internal error (Connect Code 13) with a formatted error message. +func Internal(format string, args ...any) error { + return connect.NewError(connect.CodeInternal, fmt.Errorf(format, args...)) +} + +// Aborted creates a new Aborted error (Connect Code 10) with a formatted error message. +func Aborted(format string, args ...any) error { + return connect.NewError(connect.CodeAborted, fmt.Errorf(format, args...)) +} + +// InvalidArgument creates a new Invalid Argument error (Connect Code 3) with a formatted error message. +func InvalidArgument(format string, args ...any) error { + return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf(format, args...)) +} + +// FailedPrecondition creates a new Failed Precondition error (Connect Code 9) with a formatted error message. +func FailedPrecondition(format string, args ...any) error { + return connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf(format, args...)) +} + +// Unauthenticated creates a new Unauthenticated error (Connect Code 16) with a formatted error message. +func Unauthenticated(format string, args ...any) error { + return connect.NewError(connect.CodeUnauthenticated, fmt.Errorf(format, args...)) +} + +// ResourceExhausted creates a new Resource Exhausted error (Connect Code 8) with a formatted error message. +func ResourceExhausted(format string, args ...any) error { + return connect.NewError(connect.CodeResourceExhausted, fmt.Errorf(format, args...)) +} + +// PermissionDenied creates a new Permission Denied error (Connect Code 7) with a formatted error message. +func PermissionDenied(format string, args ...any) error { + return connect.NewError(connect.CodePermissionDenied, fmt.Errorf(format, args...)) +} + +// NewNotFound creates a new Not Found error (Connect Code 5) from an underlying error. +func NewNotFound(err error) error { + return connect.NewError(connect.CodeNotFound, err) +} + +// NewConflict creates a new Already Exists error (Connect Code 6) from an underlying error. +func NewConflict(err error) error { + return connect.NewError(connect.CodeAlreadyExists, err) +} + +// NewInternal creates a new Internal error (Connect Code 13) from an underlying error. +func NewInternal(err error) error { + return connect.NewError(connect.CodeInternal, err) +} + +// NewAborted creates a new Aborted error (Connect Code 10) from an underlying error. +func NewAborted(err error) error { + return connect.NewError(connect.CodeAborted, err) +} + +// NewInvalidArgument creates a new Invalid Argument error (Connect Code 3) from an underlying error. +func NewInvalidArgument(err error) error { + return connect.NewError(connect.CodeInvalidArgument, err) +} + +// NewFailedPrecondition creates a new Failed Precondition error (Connect Code 9) from an underlying error. +func NewFailedPrecondition(err error) error { + return connect.NewError(connect.CodeFailedPrecondition, err) +} + +// NewUnauthenticated creates a new Unauthenticated error (Connect Code 16) from an underlying error. +func NewUnauthenticated(err error) error { + return connect.NewError(connect.CodeUnauthenticated, err) +} + +// NewResourceExhausted creates a new Resource Exhausted error (Connect Code 8) from an underlying error. +func NewResourceExhausted(err error) error { + return connect.NewError(connect.CodeResourceExhausted, err) +} + +// NewPermissionDenied creates a new Permission Denied error (Connect Code 7) from an underlying error. +func NewPermissionDenied(err error) error { + return connect.NewError(connect.CodePermissionDenied, err) +} + +// IsNotFound reports whether the error is a Not Found error (Connect Code 5). +func IsNotFound(err error) bool { + connectErr := Convert(err) + return connectErr.Code() == connect.CodeNotFound +} + +// IsConflict reports whether the error is an Already Exists error (Connect Code 6). +func IsConflict(err error) bool { + connectErr := Convert(err) + return connectErr.Code() == connect.CodeAlreadyExists +} + +// IsInternal reports whether the error is an Internal error (Connect Code 13). +func IsInternal(err error) bool { + connectErr := Convert(err) + return connectErr.Code() == connect.CodeInternal +} + +// IsAborted reports whether the error is an Aborted error (Connect Code 10). +func IsAborted(err error) bool { + connectErr := Convert(err) + return connectErr.Code() == connect.CodeAborted +} + +// IsInvalidArgument reports whether the error is an Invalid Argument error (Connect Code 3). +func IsInvalidArgument(err error) bool { + connectErr := Convert(err) + return connectErr.Code() == connect.CodeInvalidArgument +} + +// IsFailedPrecondition reports whether the error is a Failed Precondition error (Connect Code 9). +func IsFailedPrecondition(err error) bool { + connectErr := Convert(err) + return connectErr.Code() == connect.CodeFailedPrecondition +} + +// IsUnauthenticated reports whether the error is an Unauthenticated error (Connect Code 16). +func IsUnauthenticated(err error) bool { + connectErr := Convert(err) + return connectErr.Code() == connect.CodeUnauthenticated +} + +// IsResourceExhausted reports whether the error is a Resource Exhausted error (Connect Code 8). +func IsResourceExhausted(err error) bool { + connectErr := Convert(err) + return connectErr.Code() == connect.CodeResourceExhausted +} + +// IsPermissionDenied reports whether the error is a Permission Denied error (Connect Code 7). +func IsPermissionDenied(err error) bool { + connectErr := Convert(err) + return connectErr.Code() == connect.CodePermissionDenied +} + +// WrapConnectErr wraps the error into a connect.Error with the given code if it is not already a connect.Error. +// If the error is already a *connect.Error, it returns it unchanged. +func WrapConnectErr(c connect.Code, underlying error) *connect.Error { + if connectErr, ok := errors.AsType[*connect.Error](underlying); ok { + return connectErr + } + + return connect.NewError(c, underlying) +} + +// ConnectErrorComparer returns a go-cmp Option for comparing *connect.Error values. +// Two errors are considered equal if they have the same error string and the same Connect code. +func ConnectErrorComparer() cmp.Option { + return cmp.Comparer(func(x, y *connect.Error) bool { + if x == nil && y == nil { + return true + } + if x == nil && y != nil { + return false + } + if x != nil && y == nil { + return false + } + if x.Error() != y.Error() { + return false + } + return x.Code() == y.Code() + }) +} + +// ErrorStringComparer returns a go-cmp Option for comparing error values. +// Two errors are considered equal if their Error() strings are identical. +func ErrorStringComparer() cmp.Option { + return cmp.Comparer(func(x, y error) bool { + if x == nil && y == nil { + return true + } + if x == nil && y != nil { + return false + } + if x != nil && y == nil { + return false + } + return x.Error() == y.Error() + }) +} diff --git a/go/errorutil/errors_test.go b/go/errorutil/errors_test.go new file mode 100644 index 00000000..6869cff0 --- /dev/null +++ b/go/errorutil/errors_test.go @@ -0,0 +1,257 @@ +package errorutil + +import ( + "errors" + "fmt" + "testing" + + "connectrpc.com/connect" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestNotFound(t *testing.T) { + tests := []struct { + name string + format string + args []any + wantErr error + }{ + { + name: "error gets created as expected", + format: "something went wrong: %s", + args: []any{"abc"}, + wantErr: errors.New("not_found: something went wrong: abc"), + }, + { + name: "error gets wrapped", + format: "something went wrong: %w", + args: []any{fmt.Errorf("abc")}, + wantErr: errors.New("not_found: something went wrong: abc"), + }, + { + name: "error gets wrapped", + format: "something went wrong: %w", + args: []any{InvalidArgument("abc")}, + wantErr: errors.New("not_found: something went wrong: invalid_argument: abc"), + }, + } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + err := NotFound(tt.format, tt.args...) + require.Error(t, err) + + assert.EqualError(t, tt.wantErr, err.Error()) + }) + } +} + +func TestIsErrFns(t *testing.T) { + for _, fns := range []struct { + name string + errFn func(err error) bool + code connect.Code + }{ + { + name: "not found", + errFn: IsNotFound, + code: connect.CodeNotFound, + }, + { + name: "conflict", + errFn: IsConflict, + code: connect.CodeAlreadyExists, + }, + { + name: "invalid", + errFn: IsInvalidArgument, + code: connect.CodeInvalidArgument, + }, + { + name: "unauthenticated", + errFn: IsUnauthenticated, + code: connect.CodeUnauthenticated, + }, + } { + tests := []struct { + name string + errorFn func(err error) bool + err error + want bool + }{ + { + name: "plain error", + errorFn: fns.errFn, + err: errors.New("Some other Error"), + want: false, + }, + { + name: "connect error", + errorFn: fns.errFn, + err: connect.NewError(fns.code, errors.New("")), + want: true, + }, + { + name: "nil", + errorFn: fns.errFn, + err: nil, + want: false, + }, + { + name: "wrapped", + errorFn: fns.errFn, + err: fmt.Errorf("wrapped: %w", connect.NewError(fns.code, errors.New(""))), + want: true, + }, + { + name: "grpc", + errorFn: fns.errFn, + err: fmt.Errorf("wrapped: %w", status.Error(codes.Code(fns.code), "")), + want: true, + }, + } + for i := range tests { + tt := tests[i] + t.Run(fns.name+" "+tt.name, func(t *testing.T) { + if got := tt.errorFn(tt.err); got != tt.want { + t.Errorf("%T = %v, want %v", tt.errorFn, got, tt.want) + } + }) + } + } +} + +func TestIsInternal(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + { + name: "plain error", + err: errors.New("Some other Error"), + want: true, // a plain error is interpreted as internal (see default of convert) + }, + { + name: "connect error", + err: connect.NewError(connect.CodeInternal, errors.New("")), + want: true, + }, + { + name: "nil", + err: nil, + want: false, + }, + { + name: "wrapped", + err: fmt.Errorf("wrapped: %w", connect.NewError(connect.CodeInternal, errors.New(""))), + want: true, + }, + { + name: "grpc", + err: fmt.Errorf("wrapped: %w", status.Error(codes.Internal, "")), + want: true, + }, + } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + if got := IsInternal(tt.err); got != tt.want { + t.Errorf("%v, want %v", got, tt.want) + } + }) + } +} + +func TestWrappedInternal(t *testing.T) { + tests := []struct { + name string + err error + wantErr error + }{ + { + name: "one error", + err: Internal("something went wrong"), + wantErr: connect.NewError(connect.CodeInternal, errors.New("something went wrong")), + }, + { + name: "two errors", + err: Internal("wrapping error: %w", fmt.Errorf("root error")), + wantErr: connect.NewError(connect.CodeInternal, fmt.Errorf("wrapping error: %w", errors.New("root error"))), + }, + { + name: "two errors upside down", + err: fmt.Errorf("wrapping error: %w", Internal("root error")), + wantErr: fmt.Errorf("wrapping error: %w", connect.NewError(connect.CodeInternal, errors.New("root error"))), + }, + } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + if diff := cmp.Diff(tt.wantErr, tt.err, ErrorStringComparer()); diff != "" { + t.Errorf("wrapping() %v", diff) + } + }) + } +} + +func TestConvert(t *testing.T) { + tests := []struct { + name string + err error + wantErr error + }{ + { + name: "non specific error", + err: fmt.Errorf("something went wrong"), + wantErr: connect.NewError(connect.CodeInternal, errors.New("something went wrong")), + }, + { + name: "passing connect error", + err: Internal("something went wrong"), + wantErr: connect.NewError(connect.CodeInternal, errors.New("something went wrong")), + }, + { + name: "wrapped connect error", + err: Internal("wrapping error: %w", fmt.Errorf("root error")), + wantErr: connect.NewError(connect.CodeInternal, fmt.Errorf("wrapping error: %w", errors.New("root error"))), + }, + { + name: "wrapped connect error gets unwrapped", + err: fmt.Errorf("wrapping error: %w", Internal("root error")), + wantErr: connect.NewError(connect.CodeInternal, fmt.Errorf("wrapping error: root error")), + }, + { + name: "doubly-wrapped connect error gets unwraps the first and maintains the last", + err: fmt.Errorf("wrapping error: %w", Internal("doubly-wrapped: %w", InvalidArgument("invalid"))), + wantErr: connect.NewError(connect.CodeInternal, fmt.Errorf("wrapping error: doubly-wrapped: invalid_argument: invalid")), + }, + { + name: "grpc error gets converted", + err: status.Errorf(codes.AlreadyExists, "project already exists"), // TODO: check if this is really returned by a grpc client like that + wantErr: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("project already exists")), + }, + { + name: "wrapped grpc error gets unwrapped", + err: fmt.Errorf("wrapping error: %w", status.Errorf(codes.AlreadyExists, "project already exists")), + wantErr: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("wrapping error: project already exists")), + }, + { + name: "doubly-wrapped grpc error unwraps the first and maintains the last", + err: fmt.Errorf("wrapping error: %w", status.Errorf(codes.AlreadyExists, "doubly-wrapped: %s", status.Errorf(codes.FailedPrecondition, "precondition"))), + wantErr: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("wrapping error: doubly-wrapped: rpc error: code = FailedPrecondition desc = precondition")), + }, + } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + if diff := cmp.Diff(tt.wantErr, Convert(tt.err), ErrorStringComparer()); diff != "" { + t.Errorf("wrapping() %v", diff) + } + }) + } +} From b5533350a5f3b09c82070c6862c4f652e7705c06 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Tue, 7 Jul 2026 14:26:52 +0200 Subject: [PATCH 2/2] Up --- go.mod | 4 ++-- go.sum | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 9345be35..4f757dcc 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/google/uuid v1.6.0 github.com/klauspost/connect-compress/v2 v2.1.1 github.com/stretchr/testify v1.11.1 - google.golang.org/grpc v1.79.3 + google.golang.org/grpc v1.82.0 google.golang.org/protobuf v1.36.11 ) @@ -29,7 +29,7 @@ require ( github.com/stretchr/objx v0.5.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.38.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect diff --git a/go.sum b/go.sum index 039c591a..8c9816a8 100644 --- a/go.sum +++ b/go.sum @@ -55,18 +55,18 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU= google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU= +google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=