From b8f53a5afe05757af25d1299ddbc5003fd88f270 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:04:27 -0400 Subject: [PATCH 01/12] feat(datasource): prepare declared-operation facade and receipt outcomes --- CHANGELOG.md | 16 ++ README.md | 84 ++++++ datasource/client.go | 19 -- datasource/doc.go | 25 ++ datasource/doc_test.go | 41 +++ datasource/errors.go | 144 +++++++++++ datasource/errors_test.go | 125 +++++++++ datasource/invoke_options.go | 23 ++ datasource/json.go | 37 +++ datasource/json_test.go | 47 ++++ datasource/operations.go | 176 +++++++++++++ datasource/operations_test.go | 463 ++++++++++++++++++++++++++++++++++ datasource/types.go | 54 +++- go.mod | 5 +- go.sum | 4 +- 15 files changed, 1239 insertions(+), 24 deletions(-) create mode 100644 datasource/doc.go create mode 100644 datasource/doc_test.go create mode 100644 datasource/errors.go create mode 100644 datasource/errors_test.go create mode 100644 datasource/invoke_options.go create mode 100644 datasource/json.go create mode 100644 datasource/json_test.go create mode 100644 datasource/operations.go create mode 100644 datasource/operations_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 81965c5..7a6aabb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Unreleased — callable sources (awaits host #1049) + +- Prepare `datasource.Client.Invoke`, `Lookup`, `DeclareOperations` and + `ListOperations` on the existing gateway-bound Connect client, matching the + Python facade. Add operations, JSON output, receipts, UUIDv7 effect IDs and + per-call deadlines; the host alone owns replay and provider dispatch. +- Add `ErrNotPermitted`, `ErrUnknownOperation`, `InputError`, `RateLimited`, + `ProviderRefused` and `ErrOutcomeUnknown`. Unknown retains the effect ID and + never exposes a retryable transport error. Host messages remain unchanged. +- Document the call and retry contracts in `README.md` and package examples. +- Update `golang.org/x/tools` so the API boundary gate can read Go 1.27 export + data; the gate and its fixtures are unchanged. +- The RPC integration is awaiting the upstream proto. `SOURCE.txt` continues + to describe the existing generated tree until regeneration from the host + PR head. This SDK must not release until the host tag exists and is consumed. + ## Unreleased — current installation identity - Added `moduleauthority.Client.GetCurrentInstallation` with independent module diff --git a/README.md b/README.md index a95b60e..30c85b6 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,90 @@ The SDK surfaces are: }) _, err = ds.Sync(ctx, org, src.GetId()) ``` + + The **call** half uses that same gateway and client to invoke a declared + operation of a connected API as the signed-in person: + + ```go + result, err := ds.Invoke(ctx, orgID, sourceID, "list_invoices", + map[string]any{"limit": 20}, datasource.WithDeadline(time.Now().Add(10*time.Second))) + if errors.Is(err, datasource.ErrOutcomeUnknown) { + // Preserve result.Receipt.EffectID. Use Lookup with a live context; + // an unknown outcome does not mean the effect failed. + return err + } + if err != nil { + return err + } + output := result.Output // json.RawMessage + receipt := result.Receipt + ``` + + `Invoke` marshals a JSON object (a map, struct with JSON tags, or + `json.RawMessage`) to protobuf `Struct`. `WithDeadline(time.Time)` bounds + only this call; an earlier context deadline still wins. `WithEffectID(id)` + supplies an effect ID; when absent or empty, the SDK mints a UUIDv7. Once a + call is attempted, even an error returns a non-nil `Result` containing that + ID in `Receipt.EffectID`. Persist a caller-owned ID **before** invoking a + mutation if it must survive a process crash. + + Repeating the same effect ID and input asks the **host** to replay its + receipt. Different input under the same ID is refused. Every explicit + `Invoke` reaches the host: the SDK has no retry loop, deduplication or + receipt cache. Do not change the ID to recover an uncertain mutation. + + `Lookup(ctx, orgID, sourceID, effectID)` returns the receipt without + invoking the provider. An unknown receipt is returned alongside + `ErrOutcomeUnknown`; check it with `errors.Is`. **Unknown is an outcome, + not evidence of failure or permission to repeat an effect.** Invoke also + reports transport loss or deadline expiry conservatively as unknown, since + it cannot prove the operation was read-only. That outcome does not unwrap + to a retryable Connect transport error. An unavailable lookup also returns + an unknown receipt: it cannot establish what happened to the effect. Use a + fresh bounded context to look up an effect after the original context has + expired. + + `DeclareOperations(ctx, orgID, sourceID, []datasource.Operation)` replaces + the source's declaration set atomically under the host's administrator + check. `ListOperations(ctx, orgID, sourceID)` reads it. For example, an API + connected to `https://example.com` could declare: + + ```go + err := ds.DeclareOperations(ctx, orgID, sourceID, []datasource.Operation{{ + Name: "list_invoices", Method: "GET", Path: "/invoices", Query: []string{"limit"}, + Input: json.RawMessage(`{"type":"object","properties":{"limit":{"type":"integer"}},"additionalProperties":false}`), + Output: json.RawMessage(`{"type":"object"}`), + Effect: datasource.EffectReadOnly, MaxOutputBytes: 65536, + }}) + ``` + + The host admits the schemas, route, effect and output cap. Invocation never + accepts a provider URL or credential. The gateway's HTTP client carries + the person's Work Context unchanged; this facade adds only + `x-codefly-effect-id` to the normal Connect request, matching the request's + effect ID. It never mints, replaces or sets a Work Context header. + + | Host response | Go result | + | --- | --- | + | PermissionDenied | `ErrNotPermitted` (`errors.Is`) | + | NotFound | `ErrUnknownOperation` (`errors.Is`) | + | InvalidArgument + BadRequest | `*InputError`, with the first field violation's JSON `Pointer` | + | ResourceExhausted + DATASOURCE_RATE_LIMITED | `*RateLimited`, with `ResetAt` from `reset_at` or `RetryInfo` | + | FailedPrecondition | `*ProviderRefused`, with the host's `provider_status` as `Status` (zero if absent) | + | Ambiguous Invoke / unknown receipt | receipt plus `ErrOutcomeUnknown` (`errors.Is`) | + + Other errors retain their Connect identity. Host diagnostic messages are + preserved, including credential-looking text: **the host is the disclosure + guard**. The SDK neither redacts messages nor treats their prose as status + metadata. These names mirror the Python facade's `invoke`, `lookup`, + `declare_operations`, `list_operations`, `Operation`, `Result`, `Receipt`, + `NotPermitted`, `UnknownOperation`, `InputError`, `RateLimited`, + `ProviderRefused` and `OutcomeUnknown`. + + This call surface is unreleased pending the four host RPCs from + [module-saas-starter#1049](https://github.com/codefly-dev/module-saas-starter/issues/1049). + Development regenerates the complete bindings from that PR's exact head; + release waits for its host tag and regenerates from the tagged commit. - **`moduleauthority/`** — the module-principal side of installed operation authority. A long-running module supplies its Codefly-projected registration credential once; the client mints and refreshes the module's short-lived Work diff --git a/datasource/client.go b/datasource/client.go index a75b2ef..88cfba0 100644 --- a/datasource/client.go +++ b/datasource/client.go @@ -1,22 +1,3 @@ -// Package datasource is the typed, gateway-bound client facade for the saas -// DatasourceService — the "connection" half of datasource ingestion. It is the -// syntactic-sugar layer over the generated Connect stubs, mirroring the accounts -// facade: it binds the generated client to a solution-runtime Gateway and -// unwraps the connect.Request/Response envelope so a solution declares a source -// in a few lines: -// -// ds := datasource.New(gw) -// src, err := ds.AddGitHubSource(ctx, datasource.GitHubSource{ -// OrgID: org, -// Repo: "codefly-dev/module-saas-starter", -// Paths: []string{"docs"}, -// Collection: "handbook", -// AccessToken: token, -// }) -// _, err = ds.Sync(ctx, org, src.GetId()) -// -// The access token is sent once; the connection side encrypts it through the -// SecretCipher and persists only a secret reference — no read ever returns it. package datasource import ( diff --git a/datasource/doc.go b/datasource/doc.go new file mode 100644 index 0000000..722300b --- /dev/null +++ b/datasource/doc.go @@ -0,0 +1,25 @@ +// Package datasource connects sources and calls their declared operations +// through the host's DatasourceService. New binds the generated Connect client +// to an existing Gateway; the gateway supplies the person's Work Context. +// The host owns credentials, provider routes, receipts, replay and audit. +// +// AddGitHubSource, ListSources and Sync are the connection and ingestion half. +// A creation credential is sent once and sealed by the host; source reads +// never return it. Invoke, Lookup, DeclareOperations and ListOperations are +// the call half. A caller supplies JSON input and an operation name, never a +// provider URL or credential. +// +// result, err := datasource.New(gw).Invoke(ctx, orgID, sourceID, +// "list_invoices", map[string]any{"limit": 20}) +// +// Invoke mints a UUIDv7 when WithEffectID is absent. Once a call is attempted, +// Result.Receipt.EffectID remains available even on error. Persist a caller-owned +// ID before a mutation if recovery must survive a process crash. The SDK never +// retries or deduplicates: the host alone replays identical input under the same +// ID and refuses different input. WithDeadline bounds the call, not the effect. +// +// ErrOutcomeUnknown is a typed outcome: the provider may have acted. Lookup +// returns the receipt alongside that sentinel, tested with errors.Is. Retain the +// ID and look it up with a live context; do not infer a failed effect or mint a +// new ID to retry. See Example_invoke for the complete compiled example. +package datasource diff --git a/datasource/doc_test.go b/datasource/doc_test.go new file mode 100644 index 0000000..24e254b --- /dev/null +++ b/datasource/doc_test.go @@ -0,0 +1,41 @@ +package datasource_test + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/codefly-dev/saas-sdk-go/datasource" +) + +// This example is compile-only: gw and ctx come from the serving runtime. +func Example_invoke() { + var gw datasource.Gateway + var ctx context.Context + ds := datasource.New(gw) + result, err := ds.Invoke(ctx, "org-id", "source-id", "list_invoices", + map[string]any{"limit": 20}, datasource.WithDeadline(time.Now().Add(5*time.Second))) + if errors.Is(err, datasource.ErrOutcomeUnknown) { + // Keep the original request values (including the person's Work Context) + // while replacing its expired cancellation/deadline for this lookup. + lookupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) + defer cancel() + receipt, lookupErr := ds.Lookup(lookupCtx, "org-id", "source-id", result.Receipt.EffectID) + if errors.Is(lookupErr, datasource.ErrOutcomeUnknown) { + fmt.Println("outcome still unknown", receipt.EffectID) + return // no repeated effect, and no newly minted ID + } + if lookupErr != nil { + fmt.Println(lookupErr) + return + } + fmt.Println(receipt.Status) + return + } + if err != nil { + fmt.Println(err) + return + } + fmt.Println(string(result.Output), result.Receipt.EffectID) +} diff --git a/datasource/errors.go b/datasource/errors.go new file mode 100644 index 0000000..73f2979 --- /dev/null +++ b/datasource/errors.go @@ -0,0 +1,144 @@ +package datasource + +import ( + "errors" + "strconv" + "time" + + "connectrpc.com/connect" + "google.golang.org/genproto/googleapis/rpc/errdetails" +) + +var ( + ErrNotPermitted = errors.New("datasource: not permitted") + ErrUnknownOperation = errors.New("datasource: unknown operation") + // ErrOutcomeUnknown means the effect may have happened. Inspect the receipt + // with Lookup; never interpret this outcome as permission to repeat it. + ErrOutcomeUnknown = errors.New("datasource: outcome unknown; look up the effect") +) + +// InputError reports a rejected input. Pointer is the host's JSON Pointer from +// BadRequest.FieldViolation.Field, or empty for a local JSON encoding failure. +type InputError struct { + Pointer string + cause error +} + +func (e *InputError) Error() string { + if e.cause == nil { + return "datasource: invalid input at " + e.Pointer + } + return e.cause.Error() +} +func (e *InputError) Unwrap() error { return e.cause } + +// RateLimited reports the datasource budget's next reset, without scheduling a +// retry. ResetAt is zero when the host supplies no valid reset or retry delay. +type RateLimited struct { + ResetAt time.Time + cause error +} + +func (e *RateLimited) Error() string { + if e.cause == nil { + return "datasource: rate limited" + } + return e.cause.Error() +} +func (e *RateLimited) Unwrap() error { return e.cause } + +// ProviderRefused reports a provider refusal. Status is the provider's HTTP +// status, or zero when the host supplies none. The original message is retained. +type ProviderRefused struct { + Status int + cause error +} + +func (e *ProviderRefused) Error() string { + if e.cause == nil { + return "datasource: provider refused" + } + return e.cause.Error() +} +func (e *ProviderRefused) Unwrap() error { return e.cause } + +type hostError struct { + kind error + cause error +} + +func (e *hostError) Error() string { return e.cause.Error() } +func (e *hostError) Unwrap() error { return e.cause } +func (e *hostError) Is(target error) bool { return target == e.kind } + +// An unknown outcome deliberately does not unwrap to a retryable Connect +// Unavailable/DeadlineExceeded error. Its diagnostic message remains unchanged. +type unknownOutcome struct{ cause error } + +func (e *unknownOutcome) Error() string { return e.cause.Error() } +func (e *unknownOutcome) Is(target error) bool { return target == ErrOutcomeUnknown } + +// mapHostError does not infer provider status or retry policy from free text. +// A transport loss during Invoke is conservatively unknown: this client does +// not cache declarations and cannot prove that an operation is read-only. +func mapHostError(err error, effectOutcome bool, now time.Time) error { + if err == nil { + return nil + } + var ce *connect.Error + if !errors.As(err, &ce) { + return err + } + var info *errdetails.ErrorInfo + var retry *errdetails.RetryInfo + var bad *errdetails.BadRequest + for _, detail := range ce.Details() { + value, detailErr := detail.Value() + if detailErr != nil { + continue + } + switch value := value.(type) { + case *errdetails.ErrorInfo: + info = value + case *errdetails.RetryInfo: + retry = value + case *errdetails.BadRequest: + bad = value + } + } + switch ce.Code() { + case connect.CodePermissionDenied: + return &hostError{kind: ErrNotPermitted, cause: err} + case connect.CodeNotFound: + return &hostError{kind: ErrUnknownOperation, cause: err} + case connect.CodeInvalidArgument: + pointer := "" + if fields := bad.GetFieldViolations(); len(fields) > 0 { + pointer = fields[0].GetField() + } + return &InputError{Pointer: pointer, cause: err} + case connect.CodeResourceExhausted: + if info.GetReason() != "DATASOURCE_RATE_LIMITED" { + return err + } + reset, parseErr := time.Parse(time.RFC3339Nano, info.GetMetadata()["reset_at"]) + if parseErr != nil { + reset = time.Time{} + if delay := retry.GetRetryDelay(); delay != nil && delay.CheckValid() == nil && delay.AsDuration() >= 0 { + reset = now.Add(delay.AsDuration()) + } + } + return &RateLimited{ResetAt: reset, cause: err} + case connect.CodeFailedPrecondition: + status, parseErr := strconv.Atoi(info.GetMetadata()["provider_status"]) + if parseErr != nil || status < 100 || status > 599 { + status = 0 + } + return &ProviderRefused{Status: status, cause: err} + case connect.CodeUnavailable, connect.CodeDeadlineExceeded, connect.CodeCanceled: + if effectOutcome { + return &unknownOutcome{cause: err} + } + } + return err +} diff --git a/datasource/errors_test.go b/datasource/errors_test.go new file mode 100644 index 0000000..86f7e99 --- /dev/null +++ b/datasource/errors_test.go @@ -0,0 +1,125 @@ +package datasource + +import ( + "errors" + "testing" + "time" + + "connectrpc.com/connect" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" +) + +func detailedError(t *testing.T, code connect.Code, details ...proto.Message) *connect.Error { + t.Helper() + err := connect.NewError(code, errors.New("provider said credential-looking-example-secret")) + for _, message := range details { + detail, detailErr := connect.NewErrorDetail(message) + if detailErr != nil { + t.Fatal(detailErr) + } + err.AddDetail(detail) + } + return err +} + +func TestHostErrorMapping(t *testing.T) { + now := time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC) + tests := []struct { + name string + err *connect.Error + want error + }{ + {"permission", detailedError(t, connect.CodePermissionDenied), ErrNotPermitted}, + {"operation", detailedError(t, connect.CodeNotFound), ErrUnknownOperation}, + {"unavailable", detailedError(t, connect.CodeUnavailable), ErrOutcomeUnknown}, + {"deadline", detailedError(t, connect.CodeDeadlineExceeded), ErrOutcomeUnknown}, + {"canceled", detailedError(t, connect.CodeCanceled), ErrOutcomeUnknown}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := mapHostError(tt.err, true, now) + if !errors.Is(got, tt.want) { + t.Fatalf("error = %v, want %v", got, tt.want) + } + if got.Error() != tt.err.Error() { + t.Fatalf("host diagnostic was changed: %q", got.Error()) + } + if tt.want == ErrOutcomeUnknown { + var retryable *connect.Error + if errors.As(got, &retryable) { + t.Fatal("unknown outcome exposes a retryable transport error") + } + } else if !errors.Is(got, tt.err) { + t.Fatal("host error was lost") + } + }) + } + + t.Run("input pointer", func(t *testing.T) { + err := detailedError(t, connect.CodeInvalidArgument, &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{{Field: "/invoice/items/0/id"}}, + }) + var input *InputError + if !errors.As(mapHostError(err, true, now), &input) || input.Pointer != "/invoice/items/0/id" || input.Error() != err.Error() { + t.Fatalf("input = %#v", input) + } + }) + t.Run("provider status", func(t *testing.T) { + err := detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{ + Reason: "DATASOURCE_PROVIDER_REFUSED", Metadata: map[string]string{"provider_status": "403"}, + }) + var refused *ProviderRefused + if !errors.As(mapHostError(err, true, now), &refused) || refused.Status != 403 || refused.Error() != err.Error() { + t.Fatalf("refusal = %#v", refused) + } + }) + for _, code := range []connect.Code{connect.CodeUnknown, connect.CodeUnauthenticated, connect.CodeAlreadyExists, + connect.CodeAborted, connect.CodeOutOfRange, connect.CodeUnimplemented, connect.CodeInternal, + connect.CodeDataLoss, connect.CodeResourceExhausted} { + err := detailedError(t, code) + if got := mapHostError(err, true, now); got != err { + t.Errorf("unmapped %v changed to %v", code, got) + } + } + for _, code := range []connect.Code{connect.CodeUnavailable, connect.CodeDeadlineExceeded, connect.CodeCanceled} { + err := detailedError(t, code) + if got := mapHostError(err, false, now); got != err { + t.Errorf("read transport failure %v was presented as an effect outcome: %v", err, got) + } + } + plain := errors.New("local error") + if mapHostError(plain, true, now) != plain || mapHostError(nil, true, now) != nil { + t.Fatal("non-Connect error changed") + } +} + +func TestRateLimitReset(t *testing.T) { + now := time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC) + for _, tt := range []struct { + name string + metadata map[string]string + retry *errdetails.RetryInfo + want time.Time + }{ + {"retry delay", nil, &errdetails.RetryInfo{RetryDelay: durationpb.New(3 * time.Second)}, now.Add(3 * time.Second)}, + {"absolute wins", map[string]string{"reset_at": now.Add(time.Minute).Format(time.RFC3339)}, &errdetails.RetryInfo{RetryDelay: durationpb.New(time.Second)}, now.Add(time.Minute)}, + {"invalid absolute falls back", map[string]string{"reset_at": "invalid"}, &errdetails.RetryInfo{RetryDelay: durationpb.New(time.Second)}, now.Add(time.Second)}, + {"missing", nil, &errdetails.RetryInfo{}, time.Time{}}, + {"negative", nil, &errdetails.RetryInfo{RetryDelay: durationpb.New(-time.Second)}, time.Time{}}, + {"invalid duration", nil, &errdetails.RetryInfo{RetryDelay: &durationpb.Duration{Seconds: 1, Nanos: -1}}, time.Time{}}, + } { + t.Run(tt.name, func(t *testing.T) { + err := detailedError(t, connect.CodeResourceExhausted, + &errdetails.ErrorInfo{Reason: "DATASOURCE_RATE_LIMITED", Metadata: tt.metadata}, tt.retry) + var limited *RateLimited + if !errors.As(mapHostError(err, true, now), &limited) || !limited.ResetAt.Equal(tt.want) { + t.Fatalf("rate limit = %#v, want %v", limited, tt.want) + } + if limited.Error() != err.Error() || !errors.Is(limited, err) { + t.Fatal("rate limit lost host diagnostic") + } + }) + } +} diff --git a/datasource/invoke_options.go b/datasource/invoke_options.go new file mode 100644 index 0000000..99894d2 --- /dev/null +++ b/datasource/invoke_options.go @@ -0,0 +1,23 @@ +package datasource + +import "time" + +// InvokeOption configures one call; the client retains no per-effect state. +type InvokeOption func(*invokeOptions) + +type invokeOptions struct { + effectID string + deadline time.Time +} + +// WithEffectID reuses a caller-owned effect ID. The host replays an identical +// request and refuses different input under the same ID. An empty ID mints one. +func WithEffectID(id string) InvokeOption { + return func(o *invokeOptions) { o.effectID = id } +} + +// WithDeadline bounds the call by an absolute deadline. An earlier deadline on +// ctx still wins. Expiry does not prove that a provider mutation did not happen. +func WithDeadline(deadline time.Time) InvokeOption { + return func(o *invokeOptions) { o.deadline = deadline } +} diff --git a/datasource/json.go b/datasource/json.go new file mode 100644 index 0000000..ec7f1d4 --- /dev/null +++ b/datasource/json.go @@ -0,0 +1,37 @@ +package datasource + +import ( + "bytes" + "encoding/json" + "errors" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" +) + +func inputStruct(input any) (*structpb.Struct, error) { + data, err := json.Marshal(input) + if err != nil { + return nil, &InputError{cause: err} + } + return jsonStruct(data) +} + +func jsonStruct(data []byte) (*structpb.Struct, error) { + if trimmed := bytes.TrimSpace(data); len(trimmed) == 0 || trimmed[0] != '{' { + return nil, &InputError{cause: errors.New("datasource: input must be a JSON object")} + } + value := new(structpb.Struct) + if err := protojson.Unmarshal(data, value); err != nil { + return nil, &InputError{cause: err} + } + return value, nil +} + +func structJSON(value *structpb.Struct) (json.RawMessage, error) { + if value == nil { + return nil, nil + } + data, err := protojson.Marshal(value) + return json.RawMessage(data), err +} diff --git a/datasource/json_test.go b/datasource/json_test.go new file mode 100644 index 0000000..b7384c0 --- /dev/null +++ b/datasource/json_test.go @@ -0,0 +1,47 @@ +package datasource + +import ( + "encoding/json" + "errors" + "math" + "reflect" + "testing" +) + +func TestInputJSONObject(t *testing.T) { + for _, input := range []any{ + map[string]any{"nested": map[string]any{"items": []any{"one", nil, true}}}, + struct { + Value string `json:"value"` + }{Value: "example"}, + json.RawMessage(`{"count":2,"empty":{},"nothing":null}`), + } { + encoded, err := inputStruct(input) + if err != nil { + t.Fatal(err) + } + output, err := structJSON(encoded) + if err != nil { + t.Fatal(err) + } + original, _ := json.Marshal(input) + var want, got any + if err := json.Unmarshal(original, &want); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(output, &got); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("round trip = %s, want %s", output, original) + } + } + for _, input := range []any{nil, "text", 42, []string{}, make(chan int), + map[string]any{"bad": math.Inf(1)}, json.RawMessage(`{"broken"`)} { + _, err := inputStruct(input) + var invalid *InputError + if !errors.As(err, &invalid) { + t.Errorf("%T: error = %v, want InputError", input, err) + } + } +} diff --git a/datasource/operations.go b/datasource/operations.go new file mode 100644 index 0000000..a38a3b9 --- /dev/null +++ b/datasource/operations.go @@ -0,0 +1,176 @@ +package datasource + +import ( + "context" + "errors" + "fmt" + "time" + + "connectrpc.com/connect" + "github.com/google/uuid" + + v1 "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1" +) + +// Invoke calls one host-declared operation once, through the gateway as the +// person. The host owns provider credentials, routing, replay and the receipt. +// On an RPC error the returned Result still carries the effect ID. An unknown +// outcome must be looked up, not interpreted as permission to repeat an effect. +func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, input any, opts ...InvokeOption) (*Result, error) { + value, err := inputStruct(input) + if err != nil { + return nil, err + } + var options invokeOptions + for _, option := range opts { + option(&options) + } + if options.effectID == "" { + id, err := uuid.NewV7() + if err != nil { + return nil, fmt.Errorf("datasource: mint effect ID: %w", err) + } + options.effectID = id.String() + } + if !options.deadline.IsZero() { + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(ctx, options.deadline) + defer cancel() + } + result := &Result{Receipt: Receipt{EffectID: options.effectID, Status: ReceiptStatusUnknown}} + request := connect.NewRequest(&v1.InvokeSourceOperationRequest{ + OrgId: orgID, SourceId: sourceID, Operation: operation, Input: value, EffectId: options.effectID, + }) + request.Header().Set("x-codefly-effect-id", options.effectID) + response, err := c.inner.InvokeSourceOperation(ctx, request) + if err != nil { + return result, mapHostError(err, true, time.Now()) + } + receipt, err := operationReceipt(response.Msg.GetReceipt(), options.effectID) + if err != nil { + return result, &unknownOutcome{cause: err} + } + result.Receipt = *receipt + if receipt.Status == ReceiptStatusUnknown { + return result, ErrOutcomeUnknown + } + result.Output, err = structJSON(response.Msg.GetOutput()) + return result, err +} + +// Lookup reads the host's receipt without invoking the provider. A receipt with +// unknown outcome is returned alongside ErrOutcomeUnknown (errors.Is works). +// An unavailable lookup also leaves the outcome unknown; it is never a reason +// to dispatch the effect again. +func (c *Client) Lookup(ctx context.Context, orgID, sourceID, effectID string) (*Receipt, error) { + response, err := c.inner.LookupInvokeSourceOperation(ctx, connect.NewRequest(&v1.LookupInvokeSourceOperationRequest{ + OrgId: orgID, SourceId: sourceID, EffectId: effectID, + })) + if err != nil { + mapped := mapHostError(err, connect.CodeOf(err) == connect.CodeUnavailable, time.Now()) + if errors.Is(mapped, ErrOutcomeUnknown) { + return &Receipt{EffectID: effectID, Status: ReceiptStatusUnknown}, mapped + } + return nil, mapped + } + receipt, err := operationReceipt(response.Msg.GetReceipt(), effectID) + if err != nil { + return nil, err + } + if receipt.Status == ReceiptStatusUnknown { + return receipt, ErrOutcomeUnknown + } + return receipt, nil +} + +// DeclareOperations asks the host to replace the source's declarations +// atomically. The host validates the set and enforces administrator permission. +func (c *Client) DeclareOperations(ctx context.Context, orgID, sourceID string, operations []Operation) error { + declared := make([]*v1.SourceOperation, 0, len(operations)) + for _, operation := range operations { + input, err := jsonStruct(operation.Input) + if err != nil { + return err + } + output, err := jsonStruct(operation.Output) + if err != nil { + return err + } + var effect v1.SourceOperationEffect + switch operation.Effect { + case EffectReadOnly: + effect = v1.SourceOperationEffect_SOURCE_OPERATION_EFFECT_READ_ONLY + case EffectMutation: + effect = v1.SourceOperationEffect_SOURCE_OPERATION_EFFECT_MUTATION + default: + return &InputError{cause: fmt.Errorf("datasource: invalid operation effect %q", operation.Effect)} + } + declared = append(declared, &v1.SourceOperation{ + Name: operation.Name, Method: operation.Method, Path: operation.Path, Query: operation.Query, + InputSchema: input, OutputSchema: output, Effect: effect, MaxOutputBytes: operation.MaxOutputBytes, + }) + } + _, err := c.inner.DeclareSourceOperations(ctx, connect.NewRequest(&v1.DeclareSourceOperationsRequest{ + OrgId: orgID, SourceId: sourceID, Operations: declared, + })) + return mapHostError(err, false, time.Now()) +} + +// ListOperations reads the host's admitted set without retaining a local copy. +func (c *Client) ListOperations(ctx context.Context, orgID, sourceID string) ([]Operation, error) { + response, err := c.inner.ListSourceOperations(ctx, connect.NewRequest(&v1.ListSourceOperationsRequest{ + OrgId: orgID, SourceId: sourceID, + })) + if err != nil { + return nil, mapHostError(err, false, time.Now()) + } + operations := make([]Operation, 0, len(response.Msg.GetOperations())) + for _, value := range response.Msg.GetOperations() { + input, err := structJSON(value.GetInputSchema()) + if err != nil { + return nil, err + } + output, err := structJSON(value.GetOutputSchema()) + if err != nil { + return nil, err + } + var effect Effect + switch value.GetEffect() { + case v1.SourceOperationEffect_SOURCE_OPERATION_EFFECT_READ_ONLY: + effect = EffectReadOnly + case v1.SourceOperationEffect_SOURCE_OPERATION_EFFECT_MUTATION: + effect = EffectMutation + default: + return nil, fmt.Errorf("datasource: unsupported host operation effect %v", value.GetEffect()) + } + operations = append(operations, Operation{ + Name: value.GetName(), Method: value.GetMethod(), Path: value.GetPath(), Query: value.GetQuery(), + Input: input, Output: output, Effect: effect, MaxOutputBytes: value.GetMaxOutputBytes(), + }) + } + return operations, nil +} + +func operationReceipt(value *v1.SourceOperationReceipt, effectID string) (*Receipt, error) { + if value == nil || value.GetEffectId() != effectID { + return nil, errors.New("datasource: host returned a missing or mismatched receipt") + } + receipt := &Receipt{EffectID: effectID, ProviderStatus: int(value.GetProviderStatus())} + switch value.GetStatus() { + case v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_COMMITTED: + receipt.Status = ReceiptStatusCommitted + case v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_REFUSED: + receipt.Status = ReceiptStatusRefused + case v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_UNKNOWN: + receipt.Status = ReceiptStatusUnknown + default: + return nil, fmt.Errorf("datasource: unsupported host receipt status %v", value.GetStatus()) + } + if committed := value.GetCommittedAt(); committed != nil { + if err := committed.CheckValid(); err != nil { + return nil, fmt.Errorf("datasource: invalid receipt commit time: %w", err) + } + receipt.CommittedAt = committed.AsTime() + } + return receipt, nil +} diff --git a/datasource/operations_test.go b/datasource/operations_test.go new file mode 100644 index 0000000..fdbb23a --- /dev/null +++ b/datasource/operations_test.go @@ -0,0 +1,463 @@ +package datasource_test + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "sync/atomic" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/google/uuid" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/codefly-dev/saas-sdk-go/datasource" + v1 "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1" + "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1/accountsv1connect" +) + +type operationsHandler struct { + accountsv1connect.UnimplementedDatasourceServiceHandler + invoke func(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) + lookup func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.LookupInvokeSourceOperationResponse], error) + declare func(context.Context, *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) + list func(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) +} + +func (h *operationsHandler) InvokeSourceOperation(ctx context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return h.invoke(ctx, req) +} + +func (h *operationsHandler) LookupInvokeSourceOperation(ctx context.Context, req *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.LookupInvokeSourceOperationResponse], error) { + return h.lookup(ctx, req) +} + +func (h *operationsHandler) DeclareSourceOperations(ctx context.Context, req *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) { + return h.declare(ctx, req) +} + +func (h *operationsHandler) ListSourceOperations(ctx context.Context, req *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) { + return h.list(ctx, req) +} + +type personContextKey struct{} + +type gatewayTransport struct { + testing *testing.T + inner http.RoundTripper +} + +func (g gatewayTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // No SDK-owned identity or routing headers may reach the gateway. The + // gateway, and only the gateway, forwards the original person's token. + for key := range req.Header { + if strings.HasPrefix(strings.ToLower(key), "x-codefly-") && !strings.EqualFold(key, "x-codefly-effect-id") { + g.testing.Errorf("SDK set gateway-owned header %q", key) + } + } + cloned := req.Clone(req.Context()) + if token, ok := req.Context().Value(personContextKey{}).(string); ok { + cloned.Header.Set("x-codefly-work-context", token) + } + return g.inner.RoundTrip(cloned) +} + +func newOperationsClient(t *testing.T, h *operationsHandler, opts ...connect.ClientOption) *datasource.Client { + t.Helper() + return datasource.New(newOperationsGateway(t, h), opts...) +} + +func newOperationsGateway(t *testing.T, h *operationsHandler) gw { + t.Helper() + mux := http.NewServeMux() + mux.Handle(accountsv1connect.NewDatasourceServiceHandler(h)) + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + client := server.Client() + client.Transport = gatewayTransport{testing: t, inner: client.Transport} + return gw{base: server.URL, client: client} +} + +func object(t *testing.T, value map[string]any) *structpb.Struct { + t.Helper() + result, err := structpb.NewStruct(value) + if err != nil { + t.Fatal(err) + } + return result +} + +func committedReceipt(id string) *v1.SourceOperationReceipt { + return &v1.SourceOperationReceipt{ + EffectId: id, Status: v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_COMMITTED, + CommittedAt: timestamppb.New(time.Unix(1_791_540_000, 123)), ProviderStatus: 200, + } +} + +func TestInvokeForwardsGatewayContextMintsEffectAndNeverDeduplicates(t *testing.T) { + const token = "opaque.original.person.work-context" + var calls atomic.Int32 + var first *v1.InvokeSourceOperationRequest + h := &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + calls.Add(1) + if req.Header().Get("x-codefly-work-context") != token { + t.Error("gateway's Work Context was changed or lost") + } + for key := range req.Header() { + if strings.HasPrefix(strings.ToLower(key), "x-codefly-") && + !strings.EqualFold(key, "x-codefly-work-context") && !strings.EqualFold(key, "x-codefly-effect-id") { + t.Errorf("unexpected SDK header %q", key) + } + } + if req.Msg.GetOrgId() != "org-1" || req.Msg.GetSourceId() != "source-1" || req.Msg.GetOperation() != "list_invoices" { + t.Errorf("selectors = %v", req.Msg) + } + if req.Header().Get("x-codefly-effect-id") != req.Msg.GetEffectId() { + t.Error("header and request effect IDs differ") + } + id, err := uuid.Parse(req.Msg.GetEffectId()) + if err != nil || id.Version() != 7 || id.Variant() != uuid.RFC4122 { + t.Errorf("effect ID is not UUIDv7: %q", req.Msg.GetEffectId()) + } + if first == nil { + first = proto.Clone(req.Msg).(*v1.InvokeSourceOperationRequest) + } else if !proto.Equal(first, req.Msg) { + t.Error("same-effect replay changed input") + } + return connect.NewResponse(&v1.InvokeSourceOperationResponse{ + Output: object(t, map[string]any{"count": float64(2)}), Receipt: committedReceipt(req.Msg.GetEffectId()), + }), nil + }} + c := newOperationsClient(t, h) + ctx := context.WithValue(context.Background(), personContextKey{}, token) + result, err := c.Invoke(ctx, "org-1", "source-1", "list_invoices", struct { + Limit int `json:"limit"` + }{Limit: 20}, datasource.WithDeadline(time.Now().Add(time.Minute))) + if err != nil { + t.Fatal(err) + } + if result.Receipt.EffectID != first.GetEffectId() || result.Receipt.Status != datasource.ReceiptStatusCommitted || + !result.Receipt.CommittedAt.Equal(committedReceipt("").GetCommittedAt().AsTime()) || result.Receipt.ProviderStatus != 200 { + t.Fatalf("receipt = %+v", result.Receipt) + } + var output map[string]int + if err := json.Unmarshal(result.Output, &output); err != nil || output["count"] != 2 { + t.Fatalf("output = %s, error = %v", result.Output, err) + } + second, err := c.Invoke(ctx, "org-1", "source-1", "list_invoices", map[string]any{"limit": 20}, datasource.WithEffectID(result.Receipt.EffectID)) + if err != nil || !reflect.DeepEqual(result, second) { + t.Fatalf("replay = %+v, %v", second, err) + } + if calls.Load() != 2 { + t.Fatalf("host got %d invokes, want 2 (no local deduplication)", calls.Load()) + } +} + +func TestInvokeChangedInputReachesHostAndIsRefused(t *testing.T) { + var calls atomic.Int32 + var first *structpb.Struct + h := &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + calls.Add(1) + if first != nil && !proto.Equal(first, req.Msg.GetInput()) { + return nil, connect.NewError(connect.CodeFailedPrecondition, errors.New("effect ID has different input")) + } + first = req.Msg.GetInput() + return connect.NewResponse(&v1.InvokeSourceOperationResponse{Receipt: committedReceipt(req.Msg.GetEffectId())}), nil + }} + c := newOperationsClient(t, h) + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{"amount": 1}) + if err != nil { + t.Fatal(err) + } + _, err = c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{"amount": 2}, datasource.WithEffectID(result.Receipt.EffectID)) + var refusal *datasource.ProviderRefused + if !errors.As(err, &refusal) || calls.Load() != 2 { + t.Fatalf("refusal = %v, host calls = %d", err, calls.Load()) + } +} + +func connectError(t *testing.T, code connect.Code, details ...proto.Message) error { + t.Helper() + err := connect.NewError(code, errors.New("credential-looking-example-secret")) + for _, message := range details { + detail, detailErr := connect.NewErrorDetail(message) + if detailErr != nil { + t.Fatal(detailErr) + } + err.AddDetail(detail) + } + return err +} + +func TestInvokeHostErrorsOverConnect(t *testing.T) { + for _, tt := range []struct { + name string + err error + check func(*testing.T, error, time.Time, time.Time) + }{ + {"permission", connectError(t, connect.CodePermissionDenied), checkSentinel(datasource.ErrNotPermitted)}, + {"operation", connectError(t, connect.CodeNotFound), checkSentinel(datasource.ErrUnknownOperation)}, + {"unknown", connectError(t, connect.CodeUnavailable), checkSentinel(datasource.ErrOutcomeUnknown)}, + {"input", connectError(t, connect.CodeInvalidArgument, &errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{{Field: "/invoice/id"}}}), + func(t *testing.T, err error, _, _ time.Time) { + var value *datasource.InputError + if !errors.As(err, &value) || value.Pointer != "/invoice/id" { + t.Errorf("input error = %v", err) + } + }}, + {"rate limit", connectError(t, connect.CodeResourceExhausted, &errdetails.ErrorInfo{Reason: "DATASOURCE_RATE_LIMITED"}, &errdetails.RetryInfo{RetryDelay: durationpb.New(5 * time.Second)}), + func(t *testing.T, err error, before, after time.Time) { + var value *datasource.RateLimited + if !errors.As(err, &value) || value.ResetAt.Before(before.Add(5*time.Second)) || value.ResetAt.After(after.Add(5*time.Second)) { + t.Errorf("rate limit = %#v", value) + } + }}, + {"provider", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "DATASOURCE_PROVIDER_REFUSED", Metadata: map[string]string{"provider_status": "401"}}), + func(t *testing.T, err error, _, _ time.Time) { + var value *datasource.ProviderRefused + if !errors.As(err, &value) || value.Status != 401 { + t.Errorf("provider refusal = %v", err) + } + }}, + } { + t.Run(tt.name, func(t *testing.T) { + var calls atomic.Int32 + c := newOperationsClient(t, &operationsHandler{invoke: func(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + calls.Add(1) + return nil, tt.err + }}) + before := time.Now() + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{}) + tt.check(t, err, before, time.Now()) + if result == nil || result.Receipt.EffectID == "" { + t.Fatal("RPC error lost the minted effect ID") + } + if err.Error() != tt.err.Error() { + t.Errorf("host error message was changed: %q", err) + } + if calls.Load() != 1 { + t.Errorf("SDK retried the effect: %d calls", calls.Load()) + } + }) + } +} + +func checkSentinel(want error) func(*testing.T, error, time.Time, time.Time) { + return func(t *testing.T, err error, _, _ time.Time) { + t.Helper() + if !errors.Is(err, want) { + t.Errorf("error = %v, want %v", err, want) + } + } +} + +func TestLookupUnknownIsReceiptAndNeverInvokes(t *testing.T) { + for _, unknown := range []bool{false, true} { + c := newOperationsClient(t, &operationsHandler{lookup: func(_ context.Context, req *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.LookupInvokeSourceOperationResponse], error) { + if req.Msg.GetOrgId() != "org" || req.Msg.GetSourceId() != "source" || req.Msg.GetEffectId() != "effect" { + t.Errorf("lookup selectors = %v", req.Msg) + } + if req.Header().Get("x-codefly-effect-id") != "" { + t.Error("lookup introduced an effect header") + } + receipt := committedReceipt("effect") + if unknown { + receipt.Status = v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_UNKNOWN + receipt.CommittedAt = nil + receipt.ProviderStatus = 0 + } + return connect.NewResponse(&v1.LookupInvokeSourceOperationResponse{Receipt: receipt}), nil + }}) + receipt, err := c.Lookup(context.Background(), "org", "source", "effect") + if unknown != errors.Is(err, datasource.ErrOutcomeUnknown) || receipt == nil || receipt.EffectID != "effect" { + t.Fatalf("lookup = %+v, %v", receipt, err) + } + if unknown && (receipt.Status != datasource.ReceiptStatusUnknown || !receipt.CommittedAt.IsZero() || receipt.ProviderStatus != 0) { + t.Fatalf("unknown receipt = %+v", receipt) + } + } +} + +func TestLookupUnavailableRetainsUnknownReceipt(t *testing.T) { + c := newOperationsClient(t, &operationsHandler{lookup: func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.LookupInvokeSourceOperationResponse], error) { + return nil, connect.NewError(connect.CodeUnavailable, errors.New("host unreachable")) + }}) + receipt, err := c.Lookup(context.Background(), "org", "source", "effect") + if receipt == nil || receipt.EffectID != "effect" || receipt.Status != datasource.ReceiptStatusUnknown || !errors.Is(err, datasource.ErrOutcomeUnknown) { + t.Fatalf("unavailable lookup = %+v, %v", receipt, err) + } +} + +func TestOperationDeclarationsRoundTrip(t *testing.T) { + var declared []*v1.SourceOperation + const token = "original.person" + check := func(orgID, sourceID string, header http.Header) { + if orgID != "org" || sourceID != "source" || header.Get("x-codefly-work-context") != token { + t.Error("selectors or gateway context were lost") + } + } + c := newOperationsClient(t, &operationsHandler{ + declare: func(_ context.Context, req *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) { + check(req.Msg.GetOrgId(), req.Msg.GetSourceId(), req.Header()) + declared = req.Msg.GetOperations() + return connect.NewResponse(&v1.DeclareSourceOperationsResponse{}), nil + }, + list: func(_ context.Context, req *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) { + check(req.Msg.GetOrgId(), req.Msg.GetSourceId(), req.Header()) + return connect.NewResponse(&v1.ListSourceOperationsResponse{Operations: declared}), nil + }, + }) + ctx := context.WithValue(context.Background(), personContextKey{}, token) + for _, effect := range []datasource.Effect{datasource.EffectReadOnly, datasource.EffectMutation} { + want := datasource.Operation{ + Name: "list_invoices", Method: "GET", Path: "/accounts/{id}/invoices", Query: []string{"limit"}, + Input: json.RawMessage(`{"type":"object","additionalProperties":false}`), Output: json.RawMessage(`{"type":"object"}`), + Effect: effect, MaxOutputBytes: 65536, + } + if err := c.DeclareOperations(ctx, "org", "source", []datasource.Operation{want}); err != nil { + t.Fatal(err) + } + got, err := c.ListOperations(ctx, "org", "source") + if err != nil || len(got) != 1 { + t.Fatalf("list = %+v, %v", got, err) + } + for _, pair := range [][2]json.RawMessage{{want.Input, got[0].Input}, {want.Output, got[0].Output}} { + var left, right any + if err := json.Unmarshal(pair[0], &left); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(pair[1], &right); err != nil || !reflect.DeepEqual(left, right) { + t.Errorf("schema changed: %s -> %s", pair[0], pair[1]) + } + } + got[0].Input, got[0].Output = want.Input, want.Output + if !reflect.DeepEqual(got[0], want) { + t.Errorf("operation = %+v, want %+v", got[0], want) + } + } + if err := c.DeclareOperations(ctx, "org", "source", nil); err != nil { + t.Fatal(err) + } + got, err := c.ListOperations(ctx, "org", "source") + if err != nil || len(got) != 0 { + t.Fatalf("empty replacement = %+v, %v", got, err) + } +} + +func TestInvokeDeadlineRetainsEffectID(t *testing.T) { + var calls atomic.Int32 + c := newOperationsClient(t, &operationsHandler{invoke: func(ctx context.Context, _ *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + calls.Add(1) + <-ctx.Done() + return nil, ctx.Err() + }}) + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{}, + datasource.WithDeadline(time.Now().Add(100*time.Millisecond))) + if !errors.Is(err, datasource.ErrOutcomeUnknown) || result == nil || result.Receipt.EffectID == "" || calls.Load() != 1 { + t.Fatalf("deadline = %+v, %v, calls = %d", result, err, calls.Load()) + } +} + +func TestInvokeUnknownAndMalformedReceiptsRetainOriginalEffectID(t *testing.T) { + for _, tt := range []struct { + name string + receipt *v1.SourceOperationReceipt + }{ + {"unknown", &v1.SourceOperationReceipt{EffectId: "effect", Status: v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_UNKNOWN}}, + {"missing", nil}, + {"mismatch", committedReceipt("another-effect")}, + {"unspecified", &v1.SourceOperationReceipt{EffectId: "effect"}}, + {"invalid timestamp", &v1.SourceOperationReceipt{EffectId: "effect", Status: v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_COMMITTED, CommittedAt: ×tamppb.Timestamp{Nanos: -1}}}, + } { + t.Run(tt.name, func(t *testing.T) { + var calls atomic.Int32 + c := newOperationsClient(t, &operationsHandler{invoke: func(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + calls.Add(1) + return connect.NewResponse(&v1.InvokeSourceOperationResponse{Receipt: tt.receipt}), nil + }}) + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{}, datasource.WithEffectID("effect")) + if !errors.Is(err, datasource.ErrOutcomeUnknown) || result == nil || result.Receipt.EffectID != "effect" || calls.Load() != 1 { + t.Fatalf("result = %+v, %v, host calls = %d", result, err, calls.Load()) + } + }) + } +} + +func TestInvokeInvalidJSONDoesNotReachHost(t *testing.T) { + var calls atomic.Int32 + c := newOperationsClient(t, &operationsHandler{invoke: func(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + calls.Add(1) + return nil, connect.NewError(connect.CodeInternal, errors.New("unexpected request")) + }}) + for _, input := range []any{nil, []int{1}, make(chan int)} { + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", input) + var invalid *datasource.InputError + if result != nil || !errors.As(err, &invalid) { + t.Fatalf("invalid input = %+v, %v", result, err) + } + } + if calls.Load() != 0 { + t.Fatalf("invalid input reached host %d times", calls.Load()) + } +} + +func TestInvokeAddsOnlyEffectHeaderToGeneratedClient(t *testing.T) { + gateway := newOperationsGateway(t, &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return connect.NewResponse(&v1.InvokeSourceOperationResponse{Receipt: committedReceipt(req.Msg.GetEffectId())}), nil + }}) + var headers []http.Header + interceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + headers = append(headers, req.Header().Clone()) + return next(ctx, req) + } + }) + option := connect.WithInterceptors(interceptor) + raw := accountsv1connect.NewDatasourceServiceClient(gateway.HTTPClient(), gateway.BaseURL(), option) + _, err := raw.InvokeSourceOperation(context.Background(), connect.NewRequest(&v1.InvokeSourceOperationRequest{ + OrgId: "org", SourceId: "source", Operation: "list_invoices", EffectId: "effect", Input: object(t, map[string]any{}), + })) + if err != nil { + t.Fatal(err) + } + _, err = datasource.New(gateway, option).Invoke(context.Background(), "org", "source", "list_invoices", map[string]any{}, datasource.WithEffectID("effect")) + if err != nil { + t.Fatal(err) + } + if len(headers) != 2 || headers[1].Get("x-codefly-effect-id") != "effect" { + t.Fatalf("recorded headers = %v", headers) + } + headers[1].Del("x-codefly-effect-id") + if !reflect.DeepEqual(headers[0], headers[1]) { + t.Fatalf("facade added headers beyond effect ID: generated %v, facade %v", headers[0], headers[1]) + } +} + +func TestWithDeadlineKeepsEarlierParentDeadline(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + want, _ := ctx.Deadline() + interceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if got, ok := ctx.Deadline(); !ok || !got.Equal(want) { + t.Errorf("deadline = %v, want parent %v", got, want) + } + return next(ctx, req) + } + }) + c := newOperationsClient(t, &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return connect.NewResponse(&v1.InvokeSourceOperationResponse{Receipt: committedReceipt(req.Msg.GetEffectId())}), nil + }}, connect.WithInterceptors(interceptor)) + if _, err := c.Invoke(ctx, "org", "source", "list_invoices", map[string]any{}, datasource.WithDeadline(time.Now().Add(time.Hour))); err != nil { + t.Fatal(err) + } +} diff --git a/datasource/types.go b/datasource/types.go index 6c8d762..33d4ba0 100644 --- a/datasource/types.go +++ b/datasource/types.go @@ -1,6 +1,58 @@ package datasource -import v1 "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1" +import ( + "encoding/json" + "time" + + v1 "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1" +) + +// Effect describes whether a declared operation may change the provider. +type Effect string + +const ( + EffectReadOnly Effect = "READ_ONLY" + EffectMutation Effect = "MUTATION" +) + +// Operation declares a route relative to a connected source. The host owns +// admission, JSON Schema validation, routing, credentials and output limits. +type Operation struct { + Name string + Method string + Path string + Query []string + Input json.RawMessage + Output json.RawMessage + Effect Effect + MaxOutputBytes int64 +} + +// ReceiptStatus is the host's observation of an effect, not a retry decision. +type ReceiptStatus string + +const ( + ReceiptStatusCommitted ReceiptStatus = "COMMITTED" + ReceiptStatusRefused ReceiptStatus = "REFUSED" + ReceiptStatusUnknown ReceiptStatus = "UNKNOWN" +) + +// Receipt identifies an effect for Lookup. Unknown means its outcome is not +// known; it does not mean that the provider did nothing. CommittedAt is zero +// when the host has not reported a commit; ProviderStatus is zero when absent. +type Receipt struct { + EffectID string + Status ReceiptStatus + CommittedAt time.Time + ProviderStatus int +} + +// Result carries the provider output and the host receipt. Invoke also returns +// a non-nil Result on an RPC error so a minted effect ID is never lost. +type Result struct { + Output json.RawMessage + Receipt Receipt +} // The message types this package's methods return, re-exported under the // facade's own name — see the same block in package accounts for why an diff --git a/go.mod b/go.mod index dad3056..33c701a 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,10 @@ require ( connectrpc.com/connect v1.21.0 github.com/codefly-dev/core v0.3.32 github.com/codefly-dev/sdk-go/workcontext v0.0.0-20260927024316-b3fb40d43099 - golang.org/x/tools v0.49.0 + github.com/google/uuid v1.6.0 + golang.org/x/tools v0.51.0 google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 google.golang.org/grpc v1.83.2 google.golang.org/protobuf v1.36.12 ) @@ -19,5 +21,4 @@ require ( golang.org/x/sync v0.23.0 // indirect golang.org/x/sys v0.48.0 // indirect golang.org/x/text v0.42.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect ) diff --git a/go.sum b/go.sum index e5832c6..a7c8940 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI= From a9ec4203ee1126e8e046ad3479ea0e84351bd431 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:05:13 -0400 Subject: [PATCH 02/12] fix(datasource): align declarations with the host proto draft --- datasource/operations.go | 10 +++++----- datasource/types.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/datasource/operations.go b/datasource/operations.go index a38a3b9..de11b77 100644 --- a/datasource/operations.go +++ b/datasource/operations.go @@ -96,12 +96,12 @@ func (c *Client) DeclareOperations(ctx context.Context, orgID, sourceID string, if err != nil { return err } - var effect v1.SourceOperationEffect + var effect v1.SourceOperation_Effect switch operation.Effect { case EffectReadOnly: - effect = v1.SourceOperationEffect_SOURCE_OPERATION_EFFECT_READ_ONLY + effect = v1.SourceOperation_READ_ONLY case EffectMutation: - effect = v1.SourceOperationEffect_SOURCE_OPERATION_EFFECT_MUTATION + effect = v1.SourceOperation_MUTATION default: return &InputError{cause: fmt.Errorf("datasource: invalid operation effect %q", operation.Effect)} } @@ -136,9 +136,9 @@ func (c *Client) ListOperations(ctx context.Context, orgID, sourceID string) ([] } var effect Effect switch value.GetEffect() { - case v1.SourceOperationEffect_SOURCE_OPERATION_EFFECT_READ_ONLY: + case v1.SourceOperation_READ_ONLY: effect = EffectReadOnly - case v1.SourceOperationEffect_SOURCE_OPERATION_EFFECT_MUTATION: + case v1.SourceOperation_MUTATION: effect = EffectMutation default: return nil, fmt.Errorf("datasource: unsupported host operation effect %v", value.GetEffect()) diff --git a/datasource/types.go b/datasource/types.go index 33d4ba0..8a89f4d 100644 --- a/datasource/types.go +++ b/datasource/types.go @@ -25,7 +25,7 @@ type Operation struct { Input json.RawMessage Output json.RawMessage Effect Effect - MaxOutputBytes int64 + MaxOutputBytes uint32 } // ReceiptStatus is the host's observation of an effect, not a retry decision. From 46f30326432ec42afabebdfb2a8953fcf1b11135 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:54:47 -0400 Subject: [PATCH 03/12] feat(datasource)!: consume host operation JSON contract --- .claude/skills/refresh-generated-api/SKILL.md | 8 +- AGENTS.md | 2 + CHANGELOG.md | 41 +- README.md | 38 +- SOURCE.txt | 5 +- buf.gen.yaml | 9 + datasource/doc.go | 2 + datasource/json.go | 25 +- datasource/json_test.go | 21 +- datasource/operations.go | 39 +- datasource/operations_test.go | 99 +- datasource/types.go | 7 + .../accountsv1connect/datasource.connect.go | 124 ++ .../installations.connect.go | 102 ++ .../solution_entitlements_service.connect.go | 120 ++ .../solution_registry_service.connect.go | 90 +- gen/saas/accounts/v1/datasource.pb.go | 1259 ++++++++++++++--- gen/saas/accounts/v1/installations.pb.go | 620 +++++++- .../accounts/v1/module_capabilities.pb.go | 36 +- .../accounts/v1/solution_entitlements.pb.go | 321 +++++ .../v1/solution_entitlements_service.pb.go | 69 + gen/saas/accounts/v1/solution_registry.pb.go | 858 ++++++----- .../v1/solution_registry_service.pb.go | 32 +- go.mod | 2 +- go.sum | 4 +- moduleauthority/host_fake_test.go | 13 +- moduleauthority/installation.go | 11 +- moduleauthority/installation_test.go | 4 +- 28 files changed, 3200 insertions(+), 761 deletions(-) create mode 100644 gen/saas/accounts/v1/accountsv1connect/solution_entitlements_service.connect.go create mode 100644 gen/saas/accounts/v1/solution_entitlements.pb.go create mode 100644 gen/saas/accounts/v1/solution_entitlements_service.pb.go diff --git a/.claude/skills/refresh-generated-api/SKILL.md b/.claude/skills/refresh-generated-api/SKILL.md index e66cb05..6e6aee8 100644 --- a/.claude/skills/refresh-generated-api/SKILL.md +++ b/.claude/skills/refresh-generated-api/SKILL.md @@ -41,7 +41,13 @@ which is what makes the descriptors carry this module's path. `out: gen` in the template is relative to `-o`, so `-o "$repo"` writes `$repo/gen`. Takes about a second. -Two properties of that template matter: +Three properties of that template matter: + +- **The inputs select the complete host-owned `saas/` schema tree.** The + vendored `codefly/` closure is import-only: its `go_package_prefix` resolves + to `github.com/codefly-dev/core/generated/go`, and the Go dependency must + match the host's vendored contract version. Generating another copy of Core's + descriptors here creates duplicate protobuf registrations at initialization. - **`clean: true` wipes `gen/` first.** So a proto deleted upstream disappears here rather than lingering — which is how `AuditExportService` left in 0.1.0. diff --git a/AGENTS.md b/AGENTS.md index e919edd..af5c1c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,6 +101,8 @@ examples. Read it rather than re-deriving the API from the tree. - **Never hand-edit `gen/`, and never rewrite the module path with `sed`.** The file descriptors embed length-prefixed package strings; a text rewrite corrupts them and panics at `init()`. Regenerate (skill below). + Generate the host-owned `saas/` tree; Core's vendored Runnable imports resolve + to Core's Go dependency, never a second descriptor copy in this SDK. - **Every generated type reachable from a facade needs an alias in that package's `types.go`** — and a generated enum needs its constants re-exported too, because an alias carries the type and not the constants. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a6aabb..e5f0223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## Unreleased — callable sources (awaits host #1049) +## Unreleased — callable sources (awaits host tag) -- Prepare `datasource.Client.Invoke`, `Lookup`, `DeclareOperations` and +- Add `datasource.Client.Invoke`, `Lookup`, `DeclareOperations` and `ListOperations` on the existing gateway-bound Connect client, matching the Python facade. Add operations, JSON output, receipts, UUIDv7 effect IDs and per-call deadlines; the host alone owns replay and provider dispatch. @@ -10,11 +10,42 @@ `ProviderRefused` and `ErrOutcomeUnknown`. Unknown retains the effect ID and never exposes a retryable transport error. Host messages remain unchanged. - Document the call and retry contracts in `README.md` and package examples. +- Invoke payloads use `input_json` / `output_json` strings containing exactly + one JSON object, preserving numeric precision. Declaration schemas retain + the host's `Struct` representation. Lookup sends only the effect ID; its + existing org/source arguments cannot override the host's derived authority. - Update `golang.org/x/tools` so the API boundary gate can read Go 1.27 export data; the gate and its fixtures are unchanged. -- The RPC integration is awaiting the upstream proto. `SOURCE.txt` continues - to describe the existing generated tree until regeneration from the host - PR head. This SDK must not release until the host tag exists and is consumed. +- Regenerate the full host API from PR #1052 head + `f4ce896d954101b164c3b511cbeea555f17302f7`, recorded in `SOURCE.txt` with module + version `0.1.0`. This is a development PR head, not a release tag. Release + waits for the host tag and regeneration from its commit. +- Re-export the OAuth2 grant type/constants reachable from source reads. The + generation template emits all host-owned `saas/` schemas and imports Core's + Runnable descriptors from Core v0.14.0, matching the host's vendor closure; + plugin versions are unchanged. + +### Removed / changed (breaking, inherited host contract) + +- `moduleauthority.CurrentInstallation.SolutionIdentifier` is replaced by + `TargetID` and `BindingID`, reflecting the host's immutable authority + identities. The SDK validates the target UUID and nonempty binding. +- Generated `Installation.SolutionIdentifier`, + `InstallSolutionRequest.SolutionIdentifier` and + `ModuleCurrentInstallationResponse.SolutionIdentifier` are removed in favor + of target/binding identity fields. Their `GetSolutionIdentifier` methods + disappear too. +- Generated `PutSolutionRegistration` / `DeleteSolutionRegistration` RPCs, + procedure constants and request types (including the Put request's oneof + wrappers), `SolutionFrontendRegistration` and `SolutionBackendRegistration` + are removed. The host reconciles delivered presence; there is no writer RPC + replacement. Read contracts and new entitlement/target contracts are retained. +- Generated `SolutionFrontendBinding.LeaseExpiresAt`, + `SolutionBackendBinding.LeaseExpiresAt`, `SolutionRegistration.RuntimeBoundary` + and their getters, and the + `SolutionRegistrationStatus_SOLUTION_REGISTRATION_STATUS_EXPIRED` constant + are removed by the same delivered-presence contract. No generated output was + edited or restored to retain the older API. ## Unreleased — current installation identity diff --git a/README.md b/README.md index 30c85b6..73b8f60 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,11 @@ The SDK surfaces are: ``` `Invoke` marshals a JSON object (a map, struct with JSON tags, or - `json.RawMessage`) to protobuf `Struct`. `WithDeadline(time.Time)` bounds + `json.RawMessage`) to the host's `input_json` string. The response's + `output_json` is validated as one JSON object and returned as + `json.RawMessage`, preserving its number precision and text. Invocation + payloads never pass through protobuf `Struct`; declaration schemas still + use the host's `Struct` fields. `WithDeadline(time.Time)` bounds only this call; an earlier context deadline still wins. `WithEffectID(id)` supplies an effect ID; when absent or empty, the SDK mints a UUIDv7. Once a call is attempted, even an error returns a non-nil `Result` containing that @@ -67,7 +71,11 @@ The SDK surfaces are: receipt cache. Do not change the ID to recover an uncertain mutation. `Lookup(ctx, orgID, sourceID, effectID)` returns the receipt without - invoking the provider. An unknown receipt is returned alongside + invoking the provider. The shared SDK signature keeps `orgID` and + `sourceID`, but the wire request sends **only `effect_id`**: the host derives + the tenant from the gateway's Work Context and recovers the source from the + stored effect binding. These two arguments cannot select or override lookup + authority. An unknown receipt is returned alongside `ErrOutcomeUnknown`; check it with `errors.Is`. **Unknown is an outcome, not evidence of failure or permission to repeat an effect.** Invoke also reports transport loss or deadline expiry conservatively as unknown, since @@ -114,10 +122,10 @@ The SDK surfaces are: `NotPermitted`, `UnknownOperation`, `InputError`, `RateLimited`, `ProviderRefused` and `OutcomeUnknown`. - This call surface is unreleased pending the four host RPCs from - [module-saas-starter#1049](https://github.com/codefly-dev/module-saas-starter/issues/1049). - Development regenerates the complete bindings from that PR's exact head; - release waits for its host tag and regenerates from the tagged commit. + This call surface uses the four host RPCs from + [module-saas-starter#1052](https://github.com/codefly-dev/module-saas-starter/pull/1052). + `SOURCE.txt` records its exact development PR head. It is not a release tag: + release waits for the host tag and regeneration from the tagged commit. - **`moduleauthority/`** — the module-principal side of installed operation authority. A long-running module supplies its Codefly-projected registration credential once; the client mints and refreshes the module's short-lived Work @@ -286,6 +294,13 @@ the correct module path. **Never hand-edit `gen/` or sed the module path** — t protobuf file descriptors embed length-prefixed package strings and a text rewrite corrupts them (panics at `init()`). Always regenerate. +The template generates the complete host-owned `saas/` schema tree. Its +vendored `codefly/` import closure is compiled for Runnable annotations, with +imports resolved to `github.com/codefly-dev/core/generated/go`; Core owns +those Go descriptors. Emitting a second copy in this SDK would conflict at +protobuf initialization. The Core dependency matches the host's vendored +contract version, and the generation plugin versions remain pinned. + > Wiring this regen into module-saas-starter's release (so a saas tag publishes a > matching SDK tag) is tracked in the solutions EPIC (obin-ai/lodestar#53, item 4). @@ -334,10 +349,13 @@ original verified parent token and `InstallationID`. The client presents its own module Work Context independently on the resolved authority endpoint. It never forwards a viewer bearer, selects a tenant/source, or mints broader parent scopes. -The result is `CurrentInstallation{InstallationID, TenantID, SolutionIdentifier}`. -The SDK requires the exact requested ID and a nonempty, well-formed result; -the consumer must compare `TenantID` to its independently verified request tenant -and apply its own source naming rules. A successful observation is not executable +The result is `CurrentInstallation{InstallationID, TenantID, TargetID, BindingID}`. +The SDK requires the exact requested ID, valid tenant/target UUIDs and a +nonempty binding identity. This replaces the removed `SolutionIdentifier` +field: the host returns the immutable target and its delivered binding, +rather than a reusable route alias. The consumer must compare `TenantID` +to its independently verified request tenant and use the target/binding +identities for authority decisions. A successful observation is not executable consent, a grant or durable liveness proof. Re-read for each new request. The host preserves its organization-member metadata-read rule: no new permission diff --git a/SOURCE.txt b/SOURCE.txt index e4aa77a..87bb703 100644 --- a/SOURCE.txt +++ b/SOURCE.txt @@ -1,5 +1,8 @@ -gen/ generated from codefly-dev/module-saas-starter@21f139ee433695ae2129899f265c59b99f6661af +gen/ generated from codefly-dev/module-saas-starter@f4ce896d954101b164c3b511cbeea555f17302f7 (accounts public proto: module/services/accounts/proto) module version: 0.1.0 (module/module.package.codefly.yaml) This SDK's release is matched to the saas-starter module version above. + +Development ref: PR #1052 head, not a release tag. +Release waits on the host tag; regenerate from its commit before releasing. diff --git a/buf.gen.yaml b/buf.gen.yaml index b4f29e1..3663906 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -1,5 +1,11 @@ version: v2 clean: true +inputs: + - directory: . + # Core's vendored import closure is compiled, but its Go bindings belong + # to the Core dependency. Generate every host-owned public schema. + paths: + - saas managed: enabled: true disable: @@ -10,6 +16,9 @@ managed: override: - file_option: go_package_prefix value: github.com/codefly-dev/saas-sdk-go/gen + - file_option: go_package_prefix + path: codefly + value: github.com/codefly-dev/core/generated/go plugins: - local: [go, run, google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11] out: gen diff --git a/datasource/doc.go b/datasource/doc.go index 722300b..aeba048 100644 --- a/datasource/doc.go +++ b/datasource/doc.go @@ -8,6 +8,8 @@ // never return it. Invoke, Lookup, DeclareOperations and ListOperations are // the call half. A caller supplies JSON input and an operation name, never a // provider URL or credential. +// Input and output use JSON-object strings in the host's bounded wire profile; +// numeric values never cross protobuf Struct's floating-point representation. // // result, err := datasource.New(gw).Invoke(ctx, orgID, sourceID, // "list_invoices", map[string]any{"limit": 20}) diff --git a/datasource/json.go b/datasource/json.go index ec7f1d4..4072bd9 100644 --- a/datasource/json.go +++ b/datasource/json.go @@ -4,20 +4,35 @@ import ( "bytes" "encoding/json" "errors" + "unicode/utf8" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" ) -func inputStruct(input any) (*structpb.Struct, error) { +// Payloads remain JSON text, preserving integer precision and number spelling. +// Only operation declaration schemas cross the Struct boundary below. +func inputJSON(input any) (string, error) { data, err := json.Marshal(input) if err != nil { - return nil, &InputError{cause: err} + return "", &InputError{cause: err} + } + if _, err := objectJSON(string(data)); err != nil { + return "", &InputError{cause: err} + } + return string(data), nil +} + +func objectJSON(value string) (json.RawMessage, error) { + data := []byte(value) + trimmed := bytes.TrimSpace(data) + if len(trimmed) == 0 || trimmed[0] != '{' || !utf8.Valid(data) || !json.Valid(data) { + return nil, errors.New("datasource: expected exactly one JSON object") } - return jsonStruct(data) + return json.RawMessage(data), nil } -func jsonStruct(data []byte) (*structpb.Struct, error) { +func schemaStruct(data []byte) (*structpb.Struct, error) { if trimmed := bytes.TrimSpace(data); len(trimmed) == 0 || trimmed[0] != '{' { return nil, &InputError{cause: errors.New("datasource: input must be a JSON object")} } @@ -28,7 +43,7 @@ func jsonStruct(data []byte) (*structpb.Struct, error) { return value, nil } -func structJSON(value *structpb.Struct) (json.RawMessage, error) { +func schemaJSON(value *structpb.Struct) (json.RawMessage, error) { if value == nil { return nil, nil } diff --git a/datasource/json_test.go b/datasource/json_test.go index b7384c0..20bab5f 100644 --- a/datasource/json_test.go +++ b/datasource/json_test.go @@ -16,11 +16,11 @@ func TestInputJSONObject(t *testing.T) { }{Value: "example"}, json.RawMessage(`{"count":2,"empty":{},"nothing":null}`), } { - encoded, err := inputStruct(input) + encoded, err := inputJSON(input) if err != nil { t.Fatal(err) } - output, err := structJSON(encoded) + output, err := objectJSON(encoded) if err != nil { t.Fatal(err) } @@ -37,11 +37,24 @@ func TestInputJSONObject(t *testing.T) { } } for _, input := range []any{nil, "text", 42, []string{}, make(chan int), - map[string]any{"bad": math.Inf(1)}, json.RawMessage(`{"broken"`)} { - _, err := inputStruct(input) + map[string]any{"bad": math.Inf(1)}, json.RawMessage(`{"broken"`), json.RawMessage(`{} {}`)} { + _, err := inputJSON(input) var invalid *InputError if !errors.As(err, &invalid) { t.Errorf("%T: error = %v, want InputError", input, err) } } } + +func TestObjectJSONRejectsNonObjectAndTrailingData(t *testing.T) { + for _, value := range []string{"", "null", "[]", "42", `"text"`, "{} {}", "{} trailing", "{", "{\"x\":\"\xff\"}"} { + if _, err := objectJSON(value); err == nil { + t.Errorf("accepted invalid object %q", value) + } + } + const original = " {\"id\":9007199254740993,\"decimal\":1.234567890123456789} \n" + got, err := objectJSON(original) + if err != nil || string(got) != original { + t.Fatalf("JSON text changed: %q, %v", got, err) + } +} diff --git a/datasource/operations.go b/datasource/operations.go index de11b77..99617f3 100644 --- a/datasource/operations.go +++ b/datasource/operations.go @@ -17,7 +17,7 @@ import ( // On an RPC error the returned Result still carries the effect ID. An unknown // outcome must be looked up, not interpreted as permission to repeat an effect. func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, input any, opts ...InvokeOption) (*Result, error) { - value, err := inputStruct(input) + value, err := inputJSON(input) if err != nil { return nil, err } @@ -39,7 +39,7 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, } result := &Result{Receipt: Receipt{EffectID: options.effectID, Status: ReceiptStatusUnknown}} request := connect.NewRequest(&v1.InvokeSourceOperationRequest{ - OrgId: orgID, SourceId: sourceID, Operation: operation, Input: value, EffectId: options.effectID, + OrgId: orgID, SourceId: sourceID, Operation: operation, InputJson: value, EffectId: options.effectID, }) request.Header().Set("x-codefly-effect-id", options.effectID) response, err := c.inner.InvokeSourceOperation(ctx, request) @@ -54,7 +54,7 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, if receipt.Status == ReceiptStatusUnknown { return result, ErrOutcomeUnknown } - result.Output, err = structJSON(response.Msg.GetOutput()) + result.Output, err = objectJSON(response.Msg.GetOutputJson()) return result, err } @@ -62,9 +62,11 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, // unknown outcome is returned alongside ErrOutcomeUnknown (errors.Is works). // An unavailable lookup also leaves the outcome unknown; it is never a reason // to dispatch the effect again. +// orgID and sourceID preserve the shared SDK signature; the host derives their +// authority from the gateway's Work Context and the stored effect binding. func (c *Client) Lookup(ctx context.Context, orgID, sourceID, effectID string) (*Receipt, error) { response, err := c.inner.LookupInvokeSourceOperation(ctx, connect.NewRequest(&v1.LookupInvokeSourceOperationRequest{ - OrgId: orgID, SourceId: sourceID, EffectId: effectID, + EffectId: effectID, })) if err != nil { mapped := mapHostError(err, connect.CodeOf(err) == connect.CodeUnavailable, time.Now()) @@ -88,20 +90,20 @@ func (c *Client) Lookup(ctx context.Context, orgID, sourceID, effectID string) ( func (c *Client) DeclareOperations(ctx context.Context, orgID, sourceID string, operations []Operation) error { declared := make([]*v1.SourceOperation, 0, len(operations)) for _, operation := range operations { - input, err := jsonStruct(operation.Input) + input, err := schemaStruct(operation.Input) if err != nil { return err } - output, err := jsonStruct(operation.Output) + output, err := schemaStruct(operation.Output) if err != nil { return err } var effect v1.SourceOperation_Effect switch operation.Effect { case EffectReadOnly: - effect = v1.SourceOperation_READ_ONLY + effect = v1.SourceOperation_EFFECT_READ_ONLY case EffectMutation: - effect = v1.SourceOperation_MUTATION + effect = v1.SourceOperation_EFFECT_MUTATION default: return &InputError{cause: fmt.Errorf("datasource: invalid operation effect %q", operation.Effect)} } @@ -126,19 +128,19 @@ func (c *Client) ListOperations(ctx context.Context, orgID, sourceID string) ([] } operations := make([]Operation, 0, len(response.Msg.GetOperations())) for _, value := range response.Msg.GetOperations() { - input, err := structJSON(value.GetInputSchema()) + input, err := schemaJSON(value.GetInputSchema()) if err != nil { return nil, err } - output, err := structJSON(value.GetOutputSchema()) + output, err := schemaJSON(value.GetOutputSchema()) if err != nil { return nil, err } var effect Effect switch value.GetEffect() { - case v1.SourceOperation_READ_ONLY: + case v1.SourceOperation_EFFECT_READ_ONLY: effect = EffectReadOnly - case v1.SourceOperation_MUTATION: + case v1.SourceOperation_EFFECT_MUTATION: effect = EffectMutation default: return nil, fmt.Errorf("datasource: unsupported host operation effect %v", value.GetEffect()) @@ -157,20 +159,19 @@ func operationReceipt(value *v1.SourceOperationReceipt, effectID string) (*Recei } receipt := &Receipt{EffectID: effectID, ProviderStatus: int(value.GetProviderStatus())} switch value.GetStatus() { - case v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_COMMITTED: + case "committed": receipt.Status = ReceiptStatusCommitted - case v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_REFUSED: - receipt.Status = ReceiptStatusRefused - case v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_UNKNOWN: + case "unknown": receipt.Status = ReceiptStatusUnknown default: return nil, fmt.Errorf("datasource: unsupported host receipt status %v", value.GetStatus()) } - if committed := value.GetCommittedAt(); committed != nil { - if err := committed.CheckValid(); err != nil { + if committed := value.GetCommittedAt(); committed != "" { + var err error + receipt.CommittedAt, err = time.Parse(time.RFC3339Nano, committed) + if err != nil { return nil, fmt.Errorf("datasource: invalid receipt commit time: %w", err) } - receipt.CommittedAt = committed.AsTime() } return receipt, nil } diff --git a/datasource/operations_test.go b/datasource/operations_test.go index fdbb23a..60173dc 100644 --- a/datasource/operations_test.go +++ b/datasource/operations_test.go @@ -17,8 +17,6 @@ import ( "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" "github.com/codefly-dev/saas-sdk-go/datasource" v1 "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1" @@ -28,7 +26,7 @@ import ( type operationsHandler struct { accountsv1connect.UnimplementedDatasourceServiceHandler invoke func(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) - lookup func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.LookupInvokeSourceOperationResponse], error) + lookup func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) declare func(context.Context, *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) list func(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) } @@ -37,7 +35,7 @@ func (h *operationsHandler) InvokeSourceOperation(ctx context.Context, req *conn return h.invoke(ctx, req) } -func (h *operationsHandler) LookupInvokeSourceOperation(ctx context.Context, req *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.LookupInvokeSourceOperationResponse], error) { +func (h *operationsHandler) LookupInvokeSourceOperation(ctx context.Context, req *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { return h.lookup(ctx, req) } @@ -87,19 +85,10 @@ func newOperationsGateway(t *testing.T, h *operationsHandler) gw { return gw{base: server.URL, client: client} } -func object(t *testing.T, value map[string]any) *structpb.Struct { - t.Helper() - result, err := structpb.NewStruct(value) - if err != nil { - t.Fatal(err) - } - return result -} - func committedReceipt(id string) *v1.SourceOperationReceipt { return &v1.SourceOperationReceipt{ - EffectId: id, Status: v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_COMMITTED, - CommittedAt: timestamppb.New(time.Unix(1_791_540_000, 123)), ProviderStatus: 200, + EffectId: id, Status: "committed", + CommittedAt: time.Unix(1_791_540_000, 123).UTC().Format(time.RFC3339Nano), ProviderStatus: 200, OutputJson: `{}`, } } @@ -134,7 +123,7 @@ func TestInvokeForwardsGatewayContextMintsEffectAndNeverDeduplicates(t *testing. t.Error("same-effect replay changed input") } return connect.NewResponse(&v1.InvokeSourceOperationResponse{ - Output: object(t, map[string]any{"count": float64(2)}), Receipt: committedReceipt(req.Msg.GetEffectId()), + OutputJson: `{"count":2}`, Receipt: committedReceipt(req.Msg.GetEffectId()), }), nil }} c := newOperationsClient(t, h) @@ -146,7 +135,7 @@ func TestInvokeForwardsGatewayContextMintsEffectAndNeverDeduplicates(t *testing. t.Fatal(err) } if result.Receipt.EffectID != first.GetEffectId() || result.Receipt.Status != datasource.ReceiptStatusCommitted || - !result.Receipt.CommittedAt.Equal(committedReceipt("").GetCommittedAt().AsTime()) || result.Receipt.ProviderStatus != 200 { + !result.Receipt.CommittedAt.Equal(time.Unix(1_791_540_000, 123)) || result.Receipt.ProviderStatus != 200 { t.Fatalf("receipt = %+v", result.Receipt) } var output map[string]int @@ -164,14 +153,14 @@ func TestInvokeForwardsGatewayContextMintsEffectAndNeverDeduplicates(t *testing. func TestInvokeChangedInputReachesHostAndIsRefused(t *testing.T) { var calls atomic.Int32 - var first *structpb.Struct + var first string h := &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { calls.Add(1) - if first != nil && !proto.Equal(first, req.Msg.GetInput()) { + if first != "" && first != req.Msg.GetInputJson() { return nil, connect.NewError(connect.CodeFailedPrecondition, errors.New("effect ID has different input")) } - first = req.Msg.GetInput() - return connect.NewResponse(&v1.InvokeSourceOperationResponse{Receipt: committedReceipt(req.Msg.GetEffectId())}), nil + first = req.Msg.GetInputJson() + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: committedReceipt(req.Msg.GetEffectId())}), nil }} c := newOperationsClient(t, h) result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{"amount": 1}) @@ -262,8 +251,8 @@ func checkSentinel(want error) func(*testing.T, error, time.Time, time.Time) { func TestLookupUnknownIsReceiptAndNeverInvokes(t *testing.T) { for _, unknown := range []bool{false, true} { - c := newOperationsClient(t, &operationsHandler{lookup: func(_ context.Context, req *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.LookupInvokeSourceOperationResponse], error) { - if req.Msg.GetOrgId() != "org" || req.Msg.GetSourceId() != "source" || req.Msg.GetEffectId() != "effect" { + c := newOperationsClient(t, &operationsHandler{lookup: func(_ context.Context, req *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + if req.Msg.GetEffectId() != "effect" || req.Msg.ProtoReflect().Descriptor().Fields().Len() != 1 { t.Errorf("lookup selectors = %v", req.Msg) } if req.Header().Get("x-codefly-effect-id") != "" { @@ -271,11 +260,11 @@ func TestLookupUnknownIsReceiptAndNeverInvokes(t *testing.T) { } receipt := committedReceipt("effect") if unknown { - receipt.Status = v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_UNKNOWN - receipt.CommittedAt = nil + receipt.Status = "unknown" + receipt.CommittedAt = "" receipt.ProviderStatus = 0 } - return connect.NewResponse(&v1.LookupInvokeSourceOperationResponse{Receipt: receipt}), nil + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: receipt}), nil }}) receipt, err := c.Lookup(context.Background(), "org", "source", "effect") if unknown != errors.Is(err, datasource.ErrOutcomeUnknown) || receipt == nil || receipt.EffectID != "effect" { @@ -288,7 +277,7 @@ func TestLookupUnknownIsReceiptAndNeverInvokes(t *testing.T) { } func TestLookupUnavailableRetainsUnknownReceipt(t *testing.T) { - c := newOperationsClient(t, &operationsHandler{lookup: func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.LookupInvokeSourceOperationResponse], error) { + c := newOperationsClient(t, &operationsHandler{lookup: func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { return nil, connect.NewError(connect.CodeUnavailable, errors.New("host unreachable")) }}) receipt, err := c.Lookup(context.Background(), "org", "source", "effect") @@ -372,17 +361,17 @@ func TestInvokeUnknownAndMalformedReceiptsRetainOriginalEffectID(t *testing.T) { name string receipt *v1.SourceOperationReceipt }{ - {"unknown", &v1.SourceOperationReceipt{EffectId: "effect", Status: v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_UNKNOWN}}, + {"unknown", &v1.SourceOperationReceipt{EffectId: "effect", Status: "unknown"}}, {"missing", nil}, {"mismatch", committedReceipt("another-effect")}, {"unspecified", &v1.SourceOperationReceipt{EffectId: "effect"}}, - {"invalid timestamp", &v1.SourceOperationReceipt{EffectId: "effect", Status: v1.SourceOperationReceiptStatus_SOURCE_OPERATION_RECEIPT_STATUS_COMMITTED, CommittedAt: ×tamppb.Timestamp{Nanos: -1}}}, + {"invalid timestamp", &v1.SourceOperationReceipt{EffectId: "effect", Status: "committed", CommittedAt: "invalid-time"}}, } { t.Run(tt.name, func(t *testing.T) { var calls atomic.Int32 c := newOperationsClient(t, &operationsHandler{invoke: func(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { calls.Add(1) - return connect.NewResponse(&v1.InvokeSourceOperationResponse{Receipt: tt.receipt}), nil + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: tt.receipt}), nil }}) result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{}, datasource.WithEffectID("effect")) if !errors.Is(err, datasource.ErrOutcomeUnknown) || result == nil || result.Receipt.EffectID != "effect" || calls.Load() != 1 { @@ -412,7 +401,7 @@ func TestInvokeInvalidJSONDoesNotReachHost(t *testing.T) { func TestInvokeAddsOnlyEffectHeaderToGeneratedClient(t *testing.T) { gateway := newOperationsGateway(t, &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { - return connect.NewResponse(&v1.InvokeSourceOperationResponse{Receipt: committedReceipt(req.Msg.GetEffectId())}), nil + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: committedReceipt(req.Msg.GetEffectId())}), nil }}) var headers []http.Header interceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { @@ -424,7 +413,7 @@ func TestInvokeAddsOnlyEffectHeaderToGeneratedClient(t *testing.T) { option := connect.WithInterceptors(interceptor) raw := accountsv1connect.NewDatasourceServiceClient(gateway.HTTPClient(), gateway.BaseURL(), option) _, err := raw.InvokeSourceOperation(context.Background(), connect.NewRequest(&v1.InvokeSourceOperationRequest{ - OrgId: "org", SourceId: "source", Operation: "list_invoices", EffectId: "effect", Input: object(t, map[string]any{}), + OrgId: "org", SourceId: "source", Operation: "list_invoices", EffectId: "effect", InputJson: `{}`, })) if err != nil { t.Fatal(err) @@ -455,9 +444,53 @@ func TestWithDeadlineKeepsEarlierParentDeadline(t *testing.T) { } }) c := newOperationsClient(t, &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { - return connect.NewResponse(&v1.InvokeSourceOperationResponse{Receipt: committedReceipt(req.Msg.GetEffectId())}), nil + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: committedReceipt(req.Msg.GetEffectId())}), nil }}, connect.WithInterceptors(interceptor)) if _, err := c.Invoke(ctx, "org", "source", "list_invoices", map[string]any{}, datasource.WithDeadline(time.Now().Add(time.Hour))); err != nil { t.Fatal(err) } } + +func TestInvokeJSONTextPreservesNumbersOverConnect(t *testing.T) { + for _, tt := range []struct { + name string + opts []connect.ClientOption + }{ + {"binary", nil}, + {"json", []connect.ClientOption{connect.WithProtoJSON()}}, + } { + t.Run(tt.name, func(t *testing.T) { + const input = `{"decimal":1.234567890123456789,"id":9007199254740993}` + const output = " {\"id\":18446744073709551615,\"value\":1.234567890123456789} " + c := newOperationsClient(t, &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + if req.Msg.GetInputJson() != input { + t.Errorf("input JSON changed: %s", req.Msg.GetInputJson()) + } + receipt := committedReceipt(req.Msg.GetEffectId()) + receipt.OutputJson = output + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: output, Receipt: receipt}), nil + }}, tt.opts...) + for _, inputValue := range []any{ + json.RawMessage(input), + map[string]any{"id": int64(9007199254740993), "decimal": json.Number("1.234567890123456789")}, + } { + result, err := c.Invoke(context.Background(), "org", "source", "list_invoices", inputValue) + if err != nil || string(result.Output) != output { + t.Fatalf("result = %+v, %v", result, err) + } + } + }) + } +} + +func TestInvokeInvalidHostOutputRetainsCommittedReceipt(t *testing.T) { + for _, output := range []string{"", "null", "[]", "{} {}", `{"incomplete"`} { + c := newOperationsClient(t, &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: output, Receipt: committedReceipt(req.Msg.GetEffectId())}), nil + }}) + result, err := c.Invoke(context.Background(), "org", "source", "list_invoices", map[string]any{}) + if err == nil || result == nil || result.Receipt.Status != datasource.ReceiptStatusCommitted || result.Receipt.EffectID == "" { + t.Fatalf("invalid output %q: %+v, %v", output, result, err) + } + } +} diff --git a/datasource/types.go b/datasource/types.go index 8a89f4d..40da53a 100644 --- a/datasource/types.go +++ b/datasource/types.go @@ -77,6 +77,8 @@ type ( ApiDatasourceConfig = v1.ApiDatasourceConfig // ApiOAuth2Config is the non-secret OAuth configuration of an API source. ApiOAuth2Config = v1.ApiOAuth2Config + // ApiOAuth2Grant selects the host's OAuth2 credential lifecycle. + ApiOAuth2Grant = v1.ApiOAuth2Config_Grant // ApiCredentialKind identifies how an API source authenticates. ApiCredentialKind = v1.ApiCredentialKind // CrawlerDatasourceConfig is the configuration of a public web crawler. @@ -118,6 +120,11 @@ const ( ) const ( + ApiOAuth2GrantUnspecified = v1.ApiOAuth2Config_GRANT_UNSPECIFIED + ApiOAuth2GrantRefreshToken = v1.ApiOAuth2Config_GRANT_REFRESH_TOKEN + ApiOAuth2GrantClientCredentials = v1.ApiOAuth2Config_GRANT_CLIENT_CREDENTIALS + ApiOAuth2GrantAuthorizationCode = v1.ApiOAuth2Config_GRANT_AUTHORIZATION_CODE + ApiCredentialKindUnspecified = v1.ApiCredentialKind_API_CREDENTIAL_KIND_UNSPECIFIED ApiCredentialKindBearer = v1.ApiCredentialKind_API_CREDENTIAL_KIND_BEARER ApiCredentialKindBasic = v1.ApiCredentialKind_API_CREDENTIAL_KIND_BASIC diff --git a/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go b/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go index cac300e..afb5bc4 100644 --- a/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go +++ b/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go @@ -33,6 +33,18 @@ const ( // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( + // DatasourceServiceInvokeSourceOperationProcedure is the fully-qualified name of the + // DatasourceService's InvokeSourceOperation RPC. + DatasourceServiceInvokeSourceOperationProcedure = "/saas.accounts.v1.DatasourceService/InvokeSourceOperation" + // DatasourceServiceLookupInvokeSourceOperationProcedure is the fully-qualified name of the + // DatasourceService's LookupInvokeSourceOperation RPC. + DatasourceServiceLookupInvokeSourceOperationProcedure = "/saas.accounts.v1.DatasourceService/LookupInvokeSourceOperation" + // DatasourceServiceListSourceOperationsProcedure is the fully-qualified name of the + // DatasourceService's ListSourceOperations RPC. + DatasourceServiceListSourceOperationsProcedure = "/saas.accounts.v1.DatasourceService/ListSourceOperations" + // DatasourceServiceDeclareSourceOperationsProcedure is the fully-qualified name of the + // DatasourceService's DeclareSourceOperations RPC. + DatasourceServiceDeclareSourceOperationsProcedure = "/saas.accounts.v1.DatasourceService/DeclareSourceOperations" // DatasourceServiceAddGitHubSourceProcedure is the fully-qualified name of the DatasourceService's // AddGitHubSource RPC. DatasourceServiceAddGitHubSourceProcedure = "/saas.accounts.v1.DatasourceService/AddGitHubSource" @@ -106,6 +118,14 @@ const ( // DatasourceServiceClient is a client for the saas.accounts.v1.DatasourceService service. type DatasourceServiceClient interface { + // InvokeSourceOperation calls an admitted operation under current authority. + InvokeSourceOperation(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) + // LookupInvokeSourceOperation recovers a receipt without contacting the provider. + LookupInvokeSourceOperation(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) + // ListSourceOperations reads the currently admitted declarations under source authority. + ListSourceOperations(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) + // DeclareSourceOperations atomically replaces the source operation set, as an administrator. + DeclareSourceOperations(context.Context, *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) // AddGitHubSource registers a GitHub repository as a datasource, encrypts and // stores its access token (and optional webhook signing secret), and returns // the non-secret projection. @@ -199,6 +219,30 @@ func NewDatasourceServiceClient(httpClient connect.HTTPClient, baseURL string, o baseURL = strings.TrimRight(baseURL, "/") datasourceServiceMethods := v1.File_saas_accounts_v1_datasource_proto.Services().ByName("DatasourceService").Methods() return &datasourceServiceClient{ + invokeSourceOperation: connect.NewClient[v1.InvokeSourceOperationRequest, v1.InvokeSourceOperationResponse]( + httpClient, + baseURL+DatasourceServiceInvokeSourceOperationProcedure, + connect.WithSchema(datasourceServiceMethods.ByName("InvokeSourceOperation")), + connect.WithClientOptions(opts...), + ), + lookupInvokeSourceOperation: connect.NewClient[v1.LookupInvokeSourceOperationRequest, v1.InvokeSourceOperationResponse]( + httpClient, + baseURL+DatasourceServiceLookupInvokeSourceOperationProcedure, + connect.WithSchema(datasourceServiceMethods.ByName("LookupInvokeSourceOperation")), + connect.WithClientOptions(opts...), + ), + listSourceOperations: connect.NewClient[v1.ListSourceOperationsRequest, v1.ListSourceOperationsResponse]( + httpClient, + baseURL+DatasourceServiceListSourceOperationsProcedure, + connect.WithSchema(datasourceServiceMethods.ByName("ListSourceOperations")), + connect.WithClientOptions(opts...), + ), + declareSourceOperations: connect.NewClient[v1.DeclareSourceOperationsRequest, v1.DeclareSourceOperationsResponse]( + httpClient, + baseURL+DatasourceServiceDeclareSourceOperationsProcedure, + connect.WithSchema(datasourceServiceMethods.ByName("DeclareSourceOperations")), + connect.WithClientOptions(opts...), + ), addGitHubSource: connect.NewClient[v1.AddGitHubSourceRequest, v1.AddGitHubSourceResponse]( httpClient, baseURL+DatasourceServiceAddGitHubSourceProcedure, @@ -342,6 +386,10 @@ func NewDatasourceServiceClient(httpClient connect.HTTPClient, baseURL string, o // datasourceServiceClient implements DatasourceServiceClient. type datasourceServiceClient struct { + invokeSourceOperation *connect.Client[v1.InvokeSourceOperationRequest, v1.InvokeSourceOperationResponse] + lookupInvokeSourceOperation *connect.Client[v1.LookupInvokeSourceOperationRequest, v1.InvokeSourceOperationResponse] + listSourceOperations *connect.Client[v1.ListSourceOperationsRequest, v1.ListSourceOperationsResponse] + declareSourceOperations *connect.Client[v1.DeclareSourceOperationsRequest, v1.DeclareSourceOperationsResponse] addGitHubSource *connect.Client[v1.AddGitHubSourceRequest, v1.AddGitHubSourceResponse] addSource *connect.Client[v1.AddSourceRequest, v1.AddSourceResponse] getDatasourceCatalog *connect.Client[v1.GetDatasourceCatalogRequest, v1.GetDatasourceCatalogResponse] @@ -367,6 +415,26 @@ type datasourceServiceClient struct { revokeSourceDelegation *connect.Client[v1.RevokeSourceDelegationRequest, v1.RevokeSourceDelegationResponse] } +// InvokeSourceOperation calls saas.accounts.v1.DatasourceService.InvokeSourceOperation. +func (c *datasourceServiceClient) InvokeSourceOperation(ctx context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return c.invokeSourceOperation.CallUnary(ctx, req) +} + +// LookupInvokeSourceOperation calls saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation. +func (c *datasourceServiceClient) LookupInvokeSourceOperation(ctx context.Context, req *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return c.lookupInvokeSourceOperation.CallUnary(ctx, req) +} + +// ListSourceOperations calls saas.accounts.v1.DatasourceService.ListSourceOperations. +func (c *datasourceServiceClient) ListSourceOperations(ctx context.Context, req *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) { + return c.listSourceOperations.CallUnary(ctx, req) +} + +// DeclareSourceOperations calls saas.accounts.v1.DatasourceService.DeclareSourceOperations. +func (c *datasourceServiceClient) DeclareSourceOperations(ctx context.Context, req *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) { + return c.declareSourceOperations.CallUnary(ctx, req) +} + // AddGitHubSource calls saas.accounts.v1.DatasourceService.AddGitHubSource. func (c *datasourceServiceClient) AddGitHubSource(ctx context.Context, req *connect.Request[v1.AddGitHubSourceRequest]) (*connect.Response[v1.AddGitHubSourceResponse], error) { return c.addGitHubSource.CallUnary(ctx, req) @@ -486,6 +554,14 @@ func (c *datasourceServiceClient) RevokeSourceDelegation(ctx context.Context, re // DatasourceServiceHandler is an implementation of the saas.accounts.v1.DatasourceService service. type DatasourceServiceHandler interface { + // InvokeSourceOperation calls an admitted operation under current authority. + InvokeSourceOperation(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) + // LookupInvokeSourceOperation recovers a receipt without contacting the provider. + LookupInvokeSourceOperation(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) + // ListSourceOperations reads the currently admitted declarations under source authority. + ListSourceOperations(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) + // DeclareSourceOperations atomically replaces the source operation set, as an administrator. + DeclareSourceOperations(context.Context, *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) // AddGitHubSource registers a GitHub repository as a datasource, encrypts and // stores its access token (and optional webhook signing secret), and returns // the non-secret projection. @@ -575,6 +651,30 @@ type DatasourceServiceHandler interface { // and JSON codecs. They also support gzip compression. func NewDatasourceServiceHandler(svc DatasourceServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { datasourceServiceMethods := v1.File_saas_accounts_v1_datasource_proto.Services().ByName("DatasourceService").Methods() + datasourceServiceInvokeSourceOperationHandler := connect.NewUnaryHandler( + DatasourceServiceInvokeSourceOperationProcedure, + svc.InvokeSourceOperation, + connect.WithSchema(datasourceServiceMethods.ByName("InvokeSourceOperation")), + connect.WithHandlerOptions(opts...), + ) + datasourceServiceLookupInvokeSourceOperationHandler := connect.NewUnaryHandler( + DatasourceServiceLookupInvokeSourceOperationProcedure, + svc.LookupInvokeSourceOperation, + connect.WithSchema(datasourceServiceMethods.ByName("LookupInvokeSourceOperation")), + connect.WithHandlerOptions(opts...), + ) + datasourceServiceListSourceOperationsHandler := connect.NewUnaryHandler( + DatasourceServiceListSourceOperationsProcedure, + svc.ListSourceOperations, + connect.WithSchema(datasourceServiceMethods.ByName("ListSourceOperations")), + connect.WithHandlerOptions(opts...), + ) + datasourceServiceDeclareSourceOperationsHandler := connect.NewUnaryHandler( + DatasourceServiceDeclareSourceOperationsProcedure, + svc.DeclareSourceOperations, + connect.WithSchema(datasourceServiceMethods.ByName("DeclareSourceOperations")), + connect.WithHandlerOptions(opts...), + ) datasourceServiceAddGitHubSourceHandler := connect.NewUnaryHandler( DatasourceServiceAddGitHubSourceProcedure, svc.AddGitHubSource, @@ -715,6 +815,14 @@ func NewDatasourceServiceHandler(svc DatasourceServiceHandler, opts ...connect.H ) return "/saas.accounts.v1.DatasourceService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { + case DatasourceServiceInvokeSourceOperationProcedure: + datasourceServiceInvokeSourceOperationHandler.ServeHTTP(w, r) + case DatasourceServiceLookupInvokeSourceOperationProcedure: + datasourceServiceLookupInvokeSourceOperationHandler.ServeHTTP(w, r) + case DatasourceServiceListSourceOperationsProcedure: + datasourceServiceListSourceOperationsHandler.ServeHTTP(w, r) + case DatasourceServiceDeclareSourceOperationsProcedure: + datasourceServiceDeclareSourceOperationsHandler.ServeHTTP(w, r) case DatasourceServiceAddGitHubSourceProcedure: datasourceServiceAddGitHubSourceHandler.ServeHTTP(w, r) case DatasourceServiceAddSourceProcedure: @@ -770,6 +878,22 @@ func NewDatasourceServiceHandler(svc DatasourceServiceHandler, opts ...connect.H // UnimplementedDatasourceServiceHandler returns CodeUnimplemented from all methods. type UnimplementedDatasourceServiceHandler struct{} +func (UnimplementedDatasourceServiceHandler) InvokeSourceOperation(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.DatasourceService.InvokeSourceOperation is not implemented")) +} + +func (UnimplementedDatasourceServiceHandler) LookupInvokeSourceOperation(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation is not implemented")) +} + +func (UnimplementedDatasourceServiceHandler) ListSourceOperations(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.DatasourceService.ListSourceOperations is not implemented")) +} + +func (UnimplementedDatasourceServiceHandler) DeclareSourceOperations(context.Context, *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.DatasourceService.DeclareSourceOperations is not implemented")) +} + func (UnimplementedDatasourceServiceHandler) AddGitHubSource(context.Context, *connect.Request[v1.AddGitHubSourceRequest]) (*connect.Response[v1.AddGitHubSourceResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.DatasourceService.AddGitHubSource is not implemented")) } diff --git a/gen/saas/accounts/v1/accountsv1connect/installations.connect.go b/gen/saas/accounts/v1/accountsv1connect/installations.connect.go index ee117b7..c349fe2 100644 --- a/gen/saas/accounts/v1/accountsv1connect/installations.connect.go +++ b/gen/saas/accounts/v1/accountsv1connect/installations.connect.go @@ -46,6 +46,12 @@ const ( // InstallationServiceGetInstallationProcedure is the fully-qualified name of the // InstallationService's GetInstallation RPC. InstallationServiceGetInstallationProcedure = "/saas.accounts.v1.InstallationService/GetInstallation" + // InstallationServiceListInstallationsProcedure is the fully-qualified name of the + // InstallationService's ListInstallations RPC. + InstallationServiceListInstallationsProcedure = "/saas.accounts.v1.InstallationService/ListInstallations" + // InstallationServiceListAvailableSolutionsProcedure is the fully-qualified name of the + // InstallationService's ListAvailableSolutions RPC. + InstallationServiceListAvailableSolutionsProcedure = "/saas.accounts.v1.InstallationService/ListAvailableSolutions" ) // InstallationServiceClient is a client for the saas.accounts.v1.InstallationService service. @@ -69,6 +75,30 @@ type InstallationServiceClient interface { // can see when an installation has gone unhealthy (owner offboarded, agent // disabled, grant expired) before the next headless task fails closed. GetInstallation(context.Context, *connect.Request[v1.GetInstallationRequest]) (*connect.Response[v1.GetInstallationResponse], error) + // ListInstallations enumerates one organization's installations with their live + // health. It is internal-tier on purpose: it takes the organization as a request + // field, so only a caller holding the cluster-internal credential may ask, and + // the tenant it asks about is the one the auth-gateway projected from a verified + // identity — never one a browser supplied. It sits beside + // PermissionService.ListAccessibleScopes for that reason: the two are read + // together to answer what a viewer's organization installed and what that viewer + // was granted. + ListInstallations(context.Context, *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) + // ListAvailableSolutions is the catalogue: the solution targets an + // administrator may install right now. + // + // It answers from ACCEPTED applied state — a live target whose binding's + // newest APPLIED generation is a present one — and deliberately not from the + // diagnostic ListSolutionHostBindings, which also reports desired generations + // that were refused. The distinction is the point: a refused generation is + // something an operator must see and something an administrator must not be + // able to consent to, because consenting to a release this host never admitted + // records authority over a presence that does not exist. + // + // It carries the route alias, which the installation record deliberately does + // not: here the alias is a display and routing fact read from the target in + // the same statement, not an identity anything joins on. + ListAvailableSolutions(context.Context, *connect.Request[v1.ListAvailableSolutionsRequest]) (*connect.Response[v1.ListAvailableSolutionsResponse], error) } // NewInstallationServiceClient constructs a client for the saas.accounts.v1.InstallationService @@ -106,6 +136,18 @@ func NewInstallationServiceClient(httpClient connect.HTTPClient, baseURL string, connect.WithSchema(installationServiceMethods.ByName("GetInstallation")), connect.WithClientOptions(opts...), ), + listInstallations: connect.NewClient[v1.ListInstallationsRequest, v1.ListInstallationsResponse]( + httpClient, + baseURL+InstallationServiceListInstallationsProcedure, + connect.WithSchema(installationServiceMethods.ByName("ListInstallations")), + connect.WithClientOptions(opts...), + ), + listAvailableSolutions: connect.NewClient[v1.ListAvailableSolutionsRequest, v1.ListAvailableSolutionsResponse]( + httpClient, + baseURL+InstallationServiceListAvailableSolutionsProcedure, + connect.WithSchema(installationServiceMethods.ByName("ListAvailableSolutions")), + connect.WithClientOptions(opts...), + ), } } @@ -115,6 +157,8 @@ type installationServiceClient struct { uninstallSolution *connect.Client[v1.UninstallSolutionRequest, emptypb.Empty] transferInstallationOwnership *connect.Client[v1.TransferInstallationOwnershipRequest, v1.Installation] getInstallation *connect.Client[v1.GetInstallationRequest, v1.GetInstallationResponse] + listInstallations *connect.Client[v1.ListInstallationsRequest, v1.ListInstallationsResponse] + listAvailableSolutions *connect.Client[v1.ListAvailableSolutionsRequest, v1.ListAvailableSolutionsResponse] } // InstallSolution calls saas.accounts.v1.InstallationService.InstallSolution. @@ -138,6 +182,16 @@ func (c *installationServiceClient) GetInstallation(ctx context.Context, req *co return c.getInstallation.CallUnary(ctx, req) } +// ListInstallations calls saas.accounts.v1.InstallationService.ListInstallations. +func (c *installationServiceClient) ListInstallations(ctx context.Context, req *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) { + return c.listInstallations.CallUnary(ctx, req) +} + +// ListAvailableSolutions calls saas.accounts.v1.InstallationService.ListAvailableSolutions. +func (c *installationServiceClient) ListAvailableSolutions(ctx context.Context, req *connect.Request[v1.ListAvailableSolutionsRequest]) (*connect.Response[v1.ListAvailableSolutionsResponse], error) { + return c.listAvailableSolutions.CallUnary(ctx, req) +} + // InstallationServiceHandler is an implementation of the saas.accounts.v1.InstallationService // service. type InstallationServiceHandler interface { @@ -160,6 +214,30 @@ type InstallationServiceHandler interface { // can see when an installation has gone unhealthy (owner offboarded, agent // disabled, grant expired) before the next headless task fails closed. GetInstallation(context.Context, *connect.Request[v1.GetInstallationRequest]) (*connect.Response[v1.GetInstallationResponse], error) + // ListInstallations enumerates one organization's installations with their live + // health. It is internal-tier on purpose: it takes the organization as a request + // field, so only a caller holding the cluster-internal credential may ask, and + // the tenant it asks about is the one the auth-gateway projected from a verified + // identity — never one a browser supplied. It sits beside + // PermissionService.ListAccessibleScopes for that reason: the two are read + // together to answer what a viewer's organization installed and what that viewer + // was granted. + ListInstallations(context.Context, *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) + // ListAvailableSolutions is the catalogue: the solution targets an + // administrator may install right now. + // + // It answers from ACCEPTED applied state — a live target whose binding's + // newest APPLIED generation is a present one — and deliberately not from the + // diagnostic ListSolutionHostBindings, which also reports desired generations + // that were refused. The distinction is the point: a refused generation is + // something an operator must see and something an administrator must not be + // able to consent to, because consenting to a release this host never admitted + // records authority over a presence that does not exist. + // + // It carries the route alias, which the installation record deliberately does + // not: here the alias is a display and routing fact read from the target in + // the same statement, not an identity anything joins on. + ListAvailableSolutions(context.Context, *connect.Request[v1.ListAvailableSolutionsRequest]) (*connect.Response[v1.ListAvailableSolutionsResponse], error) } // NewInstallationServiceHandler builds an HTTP handler from the service implementation. It returns @@ -193,6 +271,18 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne connect.WithSchema(installationServiceMethods.ByName("GetInstallation")), connect.WithHandlerOptions(opts...), ) + installationServiceListInstallationsHandler := connect.NewUnaryHandler( + InstallationServiceListInstallationsProcedure, + svc.ListInstallations, + connect.WithSchema(installationServiceMethods.ByName("ListInstallations")), + connect.WithHandlerOptions(opts...), + ) + installationServiceListAvailableSolutionsHandler := connect.NewUnaryHandler( + InstallationServiceListAvailableSolutionsProcedure, + svc.ListAvailableSolutions, + connect.WithSchema(installationServiceMethods.ByName("ListAvailableSolutions")), + connect.WithHandlerOptions(opts...), + ) return "/saas.accounts.v1.InstallationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case InstallationServiceInstallSolutionProcedure: @@ -203,6 +293,10 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne installationServiceTransferInstallationOwnershipHandler.ServeHTTP(w, r) case InstallationServiceGetInstallationProcedure: installationServiceGetInstallationHandler.ServeHTTP(w, r) + case InstallationServiceListInstallationsProcedure: + installationServiceListInstallationsHandler.ServeHTTP(w, r) + case InstallationServiceListAvailableSolutionsProcedure: + installationServiceListAvailableSolutionsHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -227,3 +321,11 @@ func (UnimplementedInstallationServiceHandler) TransferInstallationOwnership(con func (UnimplementedInstallationServiceHandler) GetInstallation(context.Context, *connect.Request[v1.GetInstallationRequest]) (*connect.Response[v1.GetInstallationResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.GetInstallation is not implemented")) } + +func (UnimplementedInstallationServiceHandler) ListInstallations(context.Context, *connect.Request[v1.ListInstallationsRequest]) (*connect.Response[v1.ListInstallationsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.ListInstallations is not implemented")) +} + +func (UnimplementedInstallationServiceHandler) ListAvailableSolutions(context.Context, *connect.Request[v1.ListAvailableSolutionsRequest]) (*connect.Response[v1.ListAvailableSolutionsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.ListAvailableSolutions is not implemented")) +} diff --git a/gen/saas/accounts/v1/accountsv1connect/solution_entitlements_service.connect.go b/gen/saas/accounts/v1/accountsv1connect/solution_entitlements_service.connect.go new file mode 100644 index 0000000..9413e50 --- /dev/null +++ b/gen/saas/accounts/v1/accountsv1connect/solution_entitlements_service.connect.go @@ -0,0 +1,120 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: saas/accounts/v1/solution_entitlements_service.proto + +package accountsv1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // SolutionEntitlementServiceName is the fully-qualified name of the SolutionEntitlementService + // service. + SolutionEntitlementServiceName = "saas.accounts.v1.SolutionEntitlementService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // SolutionEntitlementServiceListSolutionEntitlementsProcedure is the fully-qualified name of the + // SolutionEntitlementService's ListSolutionEntitlements RPC. + SolutionEntitlementServiceListSolutionEntitlementsProcedure = "/saas.accounts.v1.SolutionEntitlementService/ListSolutionEntitlements" +) + +// SolutionEntitlementServiceClient is a client for the saas.accounts.v1.SolutionEntitlementService +// service. +type SolutionEntitlementServiceClient interface { + // ListSolutionEntitlements enumerates the installed solutions a subject may + // use, reading the installation set and the scope grant + share union in one + // org transaction so the two can never describe different moments. + ListSolutionEntitlements(context.Context, *connect.Request[v1.ListSolutionEntitlementsRequest]) (*connect.Response[v1.ListSolutionEntitlementsResponse], error) +} + +// NewSolutionEntitlementServiceClient constructs a client for the +// saas.accounts.v1.SolutionEntitlementService service. By default, it uses the Connect protocol +// with the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed requests. To +// use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or connect.WithGRPCWeb() +// options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewSolutionEntitlementServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SolutionEntitlementServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + solutionEntitlementServiceMethods := v1.File_saas_accounts_v1_solution_entitlements_service_proto.Services().ByName("SolutionEntitlementService").Methods() + return &solutionEntitlementServiceClient{ + listSolutionEntitlements: connect.NewClient[v1.ListSolutionEntitlementsRequest, v1.ListSolutionEntitlementsResponse]( + httpClient, + baseURL+SolutionEntitlementServiceListSolutionEntitlementsProcedure, + connect.WithSchema(solutionEntitlementServiceMethods.ByName("ListSolutionEntitlements")), + connect.WithClientOptions(opts...), + ), + } +} + +// solutionEntitlementServiceClient implements SolutionEntitlementServiceClient. +type solutionEntitlementServiceClient struct { + listSolutionEntitlements *connect.Client[v1.ListSolutionEntitlementsRequest, v1.ListSolutionEntitlementsResponse] +} + +// ListSolutionEntitlements calls +// saas.accounts.v1.SolutionEntitlementService.ListSolutionEntitlements. +func (c *solutionEntitlementServiceClient) ListSolutionEntitlements(ctx context.Context, req *connect.Request[v1.ListSolutionEntitlementsRequest]) (*connect.Response[v1.ListSolutionEntitlementsResponse], error) { + return c.listSolutionEntitlements.CallUnary(ctx, req) +} + +// SolutionEntitlementServiceHandler is an implementation of the +// saas.accounts.v1.SolutionEntitlementService service. +type SolutionEntitlementServiceHandler interface { + // ListSolutionEntitlements enumerates the installed solutions a subject may + // use, reading the installation set and the scope grant + share union in one + // org transaction so the two can never describe different moments. + ListSolutionEntitlements(context.Context, *connect.Request[v1.ListSolutionEntitlementsRequest]) (*connect.Response[v1.ListSolutionEntitlementsResponse], error) +} + +// NewSolutionEntitlementServiceHandler builds an HTTP handler from the service implementation. It +// returns the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewSolutionEntitlementServiceHandler(svc SolutionEntitlementServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + solutionEntitlementServiceMethods := v1.File_saas_accounts_v1_solution_entitlements_service_proto.Services().ByName("SolutionEntitlementService").Methods() + solutionEntitlementServiceListSolutionEntitlementsHandler := connect.NewUnaryHandler( + SolutionEntitlementServiceListSolutionEntitlementsProcedure, + svc.ListSolutionEntitlements, + connect.WithSchema(solutionEntitlementServiceMethods.ByName("ListSolutionEntitlements")), + connect.WithHandlerOptions(opts...), + ) + return "/saas.accounts.v1.SolutionEntitlementService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case SolutionEntitlementServiceListSolutionEntitlementsProcedure: + solutionEntitlementServiceListSolutionEntitlementsHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedSolutionEntitlementServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedSolutionEntitlementServiceHandler struct{} + +func (UnimplementedSolutionEntitlementServiceHandler) ListSolutionEntitlements(context.Context, *connect.Request[v1.ListSolutionEntitlementsRequest]) (*connect.Response[v1.ListSolutionEntitlementsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.SolutionEntitlementService.ListSolutionEntitlements is not implemented")) +} diff --git a/gen/saas/accounts/v1/accountsv1connect/solution_registry_service.connect.go b/gen/saas/accounts/v1/accountsv1connect/solution_registry_service.connect.go index 814dbe1..702a871 100644 --- a/gen/saas/accounts/v1/accountsv1connect/solution_registry_service.connect.go +++ b/gen/saas/accounts/v1/accountsv1connect/solution_registry_service.connect.go @@ -33,12 +33,9 @@ const ( // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( - // SolutionRegistryServicePutSolutionRegistrationProcedure is the fully-qualified name of the - // SolutionRegistryService's PutSolutionRegistration RPC. - SolutionRegistryServicePutSolutionRegistrationProcedure = "/saas.accounts.v1.SolutionRegistryService/PutSolutionRegistration" - // SolutionRegistryServiceDeleteSolutionRegistrationProcedure is the fully-qualified name of the - // SolutionRegistryService's DeleteSolutionRegistration RPC. - SolutionRegistryServiceDeleteSolutionRegistrationProcedure = "/saas.accounts.v1.SolutionRegistryService/DeleteSolutionRegistration" + // SolutionRegistryServiceListSolutionHostBindingsProcedure is the fully-qualified name of the + // SolutionRegistryService's ListSolutionHostBindings RPC. + SolutionRegistryServiceListSolutionHostBindingsProcedure = "/saas.accounts.v1.SolutionRegistryService/ListSolutionHostBindings" // SolutionRegistryServiceListSolutionRegistrationsProcedure is the fully-qualified name of the // SolutionRegistryService's ListSolutionRegistrations RPC. SolutionRegistryServiceListSolutionRegistrationsProcedure = "/saas.accounts.v1.SolutionRegistryService/ListSolutionRegistrations" @@ -47,12 +44,12 @@ const ( // SolutionRegistryServiceClient is a client for the saas.accounts.v1.SolutionRegistryService // service. type SolutionRegistryServiceClient interface { - // PutSolutionRegistration writes or renews one half of a solution's - // registration under compare-and-swap revision semantics. - PutSolutionRegistration(context.Context, *connect.Request[v1.PutSolutionRegistrationRequest]) (*connect.Response[v1.SolutionRegistration], error) - // DeleteSolutionRegistration deregisters a solution, leaving a tombstone that - // a later heartbeat from the retired deployment cannot resurrect. - DeleteSolutionRegistration(context.Context, *connect.Request[v1.DeleteSolutionRegistrationRequest]) (*connect.Response[v1.SolutionRegistration], error) + // ListSolutionHostBindings returns the declared bindings: what delivery has + // shown this host, what the host applied, and why a desired generation is not + // the applied one (issue #952). It is the answer to "is this solution missing, + // or declared and unhealthy?", which neither the registry snapshot nor a health + // probe can give on its own. + ListSolutionHostBindings(context.Context, *connect.Request[v1.ListSolutionHostBindingsRequest]) (*connect.Response[v1.ListSolutionHostBindingsResponse], error) // ListSolutionRegistrations returns the whole registry so a restarted or // lagging replica can rebuild its routing cache from authoritative state. ListSolutionRegistrations(context.Context, *connect.Request[v1.ListSolutionRegistrationsRequest]) (*connect.Response[v1.ListSolutionRegistrationsResponse], error) @@ -69,16 +66,10 @@ func NewSolutionRegistryServiceClient(httpClient connect.HTTPClient, baseURL str baseURL = strings.TrimRight(baseURL, "/") solutionRegistryServiceMethods := v1.File_saas_accounts_v1_solution_registry_service_proto.Services().ByName("SolutionRegistryService").Methods() return &solutionRegistryServiceClient{ - putSolutionRegistration: connect.NewClient[v1.PutSolutionRegistrationRequest, v1.SolutionRegistration]( + listSolutionHostBindings: connect.NewClient[v1.ListSolutionHostBindingsRequest, v1.ListSolutionHostBindingsResponse]( httpClient, - baseURL+SolutionRegistryServicePutSolutionRegistrationProcedure, - connect.WithSchema(solutionRegistryServiceMethods.ByName("PutSolutionRegistration")), - connect.WithClientOptions(opts...), - ), - deleteSolutionRegistration: connect.NewClient[v1.DeleteSolutionRegistrationRequest, v1.SolutionRegistration]( - httpClient, - baseURL+SolutionRegistryServiceDeleteSolutionRegistrationProcedure, - connect.WithSchema(solutionRegistryServiceMethods.ByName("DeleteSolutionRegistration")), + baseURL+SolutionRegistryServiceListSolutionHostBindingsProcedure, + connect.WithSchema(solutionRegistryServiceMethods.ByName("ListSolutionHostBindings")), connect.WithClientOptions(opts...), ), listSolutionRegistrations: connect.NewClient[v1.ListSolutionRegistrationsRequest, v1.ListSolutionRegistrationsResponse]( @@ -92,20 +83,13 @@ func NewSolutionRegistryServiceClient(httpClient connect.HTTPClient, baseURL str // solutionRegistryServiceClient implements SolutionRegistryServiceClient. type solutionRegistryServiceClient struct { - putSolutionRegistration *connect.Client[v1.PutSolutionRegistrationRequest, v1.SolutionRegistration] - deleteSolutionRegistration *connect.Client[v1.DeleteSolutionRegistrationRequest, v1.SolutionRegistration] - listSolutionRegistrations *connect.Client[v1.ListSolutionRegistrationsRequest, v1.ListSolutionRegistrationsResponse] -} - -// PutSolutionRegistration calls saas.accounts.v1.SolutionRegistryService.PutSolutionRegistration. -func (c *solutionRegistryServiceClient) PutSolutionRegistration(ctx context.Context, req *connect.Request[v1.PutSolutionRegistrationRequest]) (*connect.Response[v1.SolutionRegistration], error) { - return c.putSolutionRegistration.CallUnary(ctx, req) + listSolutionHostBindings *connect.Client[v1.ListSolutionHostBindingsRequest, v1.ListSolutionHostBindingsResponse] + listSolutionRegistrations *connect.Client[v1.ListSolutionRegistrationsRequest, v1.ListSolutionRegistrationsResponse] } -// DeleteSolutionRegistration calls -// saas.accounts.v1.SolutionRegistryService.DeleteSolutionRegistration. -func (c *solutionRegistryServiceClient) DeleteSolutionRegistration(ctx context.Context, req *connect.Request[v1.DeleteSolutionRegistrationRequest]) (*connect.Response[v1.SolutionRegistration], error) { - return c.deleteSolutionRegistration.CallUnary(ctx, req) +// ListSolutionHostBindings calls saas.accounts.v1.SolutionRegistryService.ListSolutionHostBindings. +func (c *solutionRegistryServiceClient) ListSolutionHostBindings(ctx context.Context, req *connect.Request[v1.ListSolutionHostBindingsRequest]) (*connect.Response[v1.ListSolutionHostBindingsResponse], error) { + return c.listSolutionHostBindings.CallUnary(ctx, req) } // ListSolutionRegistrations calls @@ -117,12 +101,12 @@ func (c *solutionRegistryServiceClient) ListSolutionRegistrations(ctx context.Co // SolutionRegistryServiceHandler is an implementation of the // saas.accounts.v1.SolutionRegistryService service. type SolutionRegistryServiceHandler interface { - // PutSolutionRegistration writes or renews one half of a solution's - // registration under compare-and-swap revision semantics. - PutSolutionRegistration(context.Context, *connect.Request[v1.PutSolutionRegistrationRequest]) (*connect.Response[v1.SolutionRegistration], error) - // DeleteSolutionRegistration deregisters a solution, leaving a tombstone that - // a later heartbeat from the retired deployment cannot resurrect. - DeleteSolutionRegistration(context.Context, *connect.Request[v1.DeleteSolutionRegistrationRequest]) (*connect.Response[v1.SolutionRegistration], error) + // ListSolutionHostBindings returns the declared bindings: what delivery has + // shown this host, what the host applied, and why a desired generation is not + // the applied one (issue #952). It is the answer to "is this solution missing, + // or declared and unhealthy?", which neither the registry snapshot nor a health + // probe can give on its own. + ListSolutionHostBindings(context.Context, *connect.Request[v1.ListSolutionHostBindingsRequest]) (*connect.Response[v1.ListSolutionHostBindingsResponse], error) // ListSolutionRegistrations returns the whole registry so a restarted or // lagging replica can rebuild its routing cache from authoritative state. ListSolutionRegistrations(context.Context, *connect.Request[v1.ListSolutionRegistrationsRequest]) (*connect.Response[v1.ListSolutionRegistrationsResponse], error) @@ -135,16 +119,10 @@ type SolutionRegistryServiceHandler interface { // and JSON codecs. They also support gzip compression. func NewSolutionRegistryServiceHandler(svc SolutionRegistryServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { solutionRegistryServiceMethods := v1.File_saas_accounts_v1_solution_registry_service_proto.Services().ByName("SolutionRegistryService").Methods() - solutionRegistryServicePutSolutionRegistrationHandler := connect.NewUnaryHandler( - SolutionRegistryServicePutSolutionRegistrationProcedure, - svc.PutSolutionRegistration, - connect.WithSchema(solutionRegistryServiceMethods.ByName("PutSolutionRegistration")), - connect.WithHandlerOptions(opts...), - ) - solutionRegistryServiceDeleteSolutionRegistrationHandler := connect.NewUnaryHandler( - SolutionRegistryServiceDeleteSolutionRegistrationProcedure, - svc.DeleteSolutionRegistration, - connect.WithSchema(solutionRegistryServiceMethods.ByName("DeleteSolutionRegistration")), + solutionRegistryServiceListSolutionHostBindingsHandler := connect.NewUnaryHandler( + SolutionRegistryServiceListSolutionHostBindingsProcedure, + svc.ListSolutionHostBindings, + connect.WithSchema(solutionRegistryServiceMethods.ByName("ListSolutionHostBindings")), connect.WithHandlerOptions(opts...), ) solutionRegistryServiceListSolutionRegistrationsHandler := connect.NewUnaryHandler( @@ -155,10 +133,8 @@ func NewSolutionRegistryServiceHandler(svc SolutionRegistryServiceHandler, opts ) return "/saas.accounts.v1.SolutionRegistryService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case SolutionRegistryServicePutSolutionRegistrationProcedure: - solutionRegistryServicePutSolutionRegistrationHandler.ServeHTTP(w, r) - case SolutionRegistryServiceDeleteSolutionRegistrationProcedure: - solutionRegistryServiceDeleteSolutionRegistrationHandler.ServeHTTP(w, r) + case SolutionRegistryServiceListSolutionHostBindingsProcedure: + solutionRegistryServiceListSolutionHostBindingsHandler.ServeHTTP(w, r) case SolutionRegistryServiceListSolutionRegistrationsProcedure: solutionRegistryServiceListSolutionRegistrationsHandler.ServeHTTP(w, r) default: @@ -170,12 +146,8 @@ func NewSolutionRegistryServiceHandler(svc SolutionRegistryServiceHandler, opts // UnimplementedSolutionRegistryServiceHandler returns CodeUnimplemented from all methods. type UnimplementedSolutionRegistryServiceHandler struct{} -func (UnimplementedSolutionRegistryServiceHandler) PutSolutionRegistration(context.Context, *connect.Request[v1.PutSolutionRegistrationRequest]) (*connect.Response[v1.SolutionRegistration], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.SolutionRegistryService.PutSolutionRegistration is not implemented")) -} - -func (UnimplementedSolutionRegistryServiceHandler) DeleteSolutionRegistration(context.Context, *connect.Request[v1.DeleteSolutionRegistrationRequest]) (*connect.Response[v1.SolutionRegistration], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.SolutionRegistryService.DeleteSolutionRegistration is not implemented")) +func (UnimplementedSolutionRegistryServiceHandler) ListSolutionHostBindings(context.Context, *connect.Request[v1.ListSolutionHostBindingsRequest]) (*connect.Response[v1.ListSolutionHostBindingsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.SolutionRegistryService.ListSolutionHostBindings is not implemented")) } func (UnimplementedSolutionRegistryServiceHandler) ListSolutionRegistrations(context.Context, *connect.Request[v1.ListSolutionRegistrationsRequest]) (*connect.Response[v1.ListSolutionRegistrationsResponse], error) { diff --git a/gen/saas/accounts/v1/datasource.pb.go b/gen/saas/accounts/v1/datasource.pb.go index 0b7ff24..02cf713 100644 --- a/gen/saas/accounts/v1/datasource.pb.go +++ b/gen/saas/accounts/v1/datasource.pb.go @@ -8,11 +8,14 @@ package accountsv1 import ( _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + _ "github.com/codefly-dev/core/generated/go/codefly/runnable/v0" v1 "github.com/codefly-dev/saas-sdk-go/gen/saas/jobs/v1" _ "github.com/codefly-dev/saas-sdk-go/gen/saas/policy/v1" + _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" + structpb "google.golang.org/protobuf/types/known/structpb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" @@ -811,6 +814,108 @@ func (SourceDelegationRevocation) EnumDescriptor() ([]byte, []int) { return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{11} } +type ApiOAuth2Config_Grant int32 + +const ( + // Default preserves stored refresh-token sources. + ApiOAuth2Config_GRANT_UNSPECIFIED ApiOAuth2Config_Grant = 0 + ApiOAuth2Config_GRANT_REFRESH_TOKEN ApiOAuth2Config_Grant = 1 + ApiOAuth2Config_GRANT_CLIENT_CREDENTIALS ApiOAuth2Config_Grant = 2 + ApiOAuth2Config_GRANT_AUTHORIZATION_CODE ApiOAuth2Config_Grant = 3 +) + +// Enum value maps for ApiOAuth2Config_Grant. +var ( + ApiOAuth2Config_Grant_name = map[int32]string{ + 0: "GRANT_UNSPECIFIED", + 1: "GRANT_REFRESH_TOKEN", + 2: "GRANT_CLIENT_CREDENTIALS", + 3: "GRANT_AUTHORIZATION_CODE", + } + ApiOAuth2Config_Grant_value = map[string]int32{ + "GRANT_UNSPECIFIED": 0, + "GRANT_REFRESH_TOKEN": 1, + "GRANT_CLIENT_CREDENTIALS": 2, + "GRANT_AUTHORIZATION_CODE": 3, + } +) + +func (x ApiOAuth2Config_Grant) Enum() *ApiOAuth2Config_Grant { + p := new(ApiOAuth2Config_Grant) + *p = x + return p +} + +func (x ApiOAuth2Config_Grant) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ApiOAuth2Config_Grant) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_datasource_proto_enumTypes[12].Descriptor() +} + +func (ApiOAuth2Config_Grant) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_datasource_proto_enumTypes[12] +} + +func (x ApiOAuth2Config_Grant) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ApiOAuth2Config_Grant.Descriptor instead. +func (ApiOAuth2Config_Grant) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{1, 0} +} + +type SourceOperation_Effect int32 + +const ( + SourceOperation_EFFECT_UNSPECIFIED SourceOperation_Effect = 0 + SourceOperation_EFFECT_READ_ONLY SourceOperation_Effect = 1 + SourceOperation_EFFECT_MUTATION SourceOperation_Effect = 2 +) + +// Enum value maps for SourceOperation_Effect. +var ( + SourceOperation_Effect_name = map[int32]string{ + 0: "EFFECT_UNSPECIFIED", + 1: "EFFECT_READ_ONLY", + 2: "EFFECT_MUTATION", + } + SourceOperation_Effect_value = map[string]int32{ + "EFFECT_UNSPECIFIED": 0, + "EFFECT_READ_ONLY": 1, + "EFFECT_MUTATION": 2, + } +) + +func (x SourceOperation_Effect) Enum() *SourceOperation_Effect { + p := new(SourceOperation_Effect) + *p = x + return p +} + +func (x SourceOperation_Effect) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SourceOperation_Effect) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_datasource_proto_enumTypes[13].Descriptor() +} + +func (SourceOperation_Effect) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_datasource_proto_enumTypes[13] +} + +func (x SourceOperation_Effect) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SourceOperation_Effect.Descriptor instead. +func (SourceOperation_Effect) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{65, 0} +} + // GitHubDatasourceConfig is the public, non-secret configuration of a GitHub // datasource. The access token and webhook signing secret are never carried // here; they live only as SecretCipher envelopes in the credential store. @@ -893,6 +998,7 @@ func (x *GitHubDatasourceConfig) GetFileExtensions() []string { // envelope in the credential store and are rotated in place on refresh. type ApiOAuth2Config struct { state protoimpl.MessageState `protogen:"open.v1"` + Grant ApiOAuth2Config_Grant `protobuf:"varint,4,opt,name=grant,proto3,enum=saas.accounts.v1.ApiOAuth2Config_Grant" json:"grant,omitempty"` // Token endpoint the refresh_token grant is exchanged at, e.g. // "https://oauth2.example.com/token". Absolute http(s) URL. TokenUrl string `protobuf:"bytes,1,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` @@ -934,6 +1040,13 @@ func (*ApiOAuth2Config) Descriptor() ([]byte, []int) { return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{1} } +func (x *ApiOAuth2Config) GetGrant() ApiOAuth2Config_Grant { + if x != nil { + return x.Grant + } + return ApiOAuth2Config_GRANT_UNSPECIFIED +} + func (x *ApiOAuth2Config) GetTokenUrl() string { if x != nil { return x.TokenUrl @@ -2117,8 +2230,14 @@ type DatasourceProviderDescriptor struct { // every source of it with no live delivery. Always false for a connector // whose supports_webhook is false. LiveDeliveryConfigured bool `protobuf:"varint,15,opt,name=live_delivery_configured,json=liveDeliveryConfigured,proto3" json:"live_delivery_configured,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Sync admission remains accepts_new_sources; operations have a separate gate. + AcceptsOperations bool `protobuf:"varint,16,opt,name=accepts_operations,json=acceptsOperations,proto3" json:"accepts_operations,omitempty"` + OperationsGap string `protobuf:"bytes,17,opt,name=operations_gap,json=operationsGap,proto3" json:"operations_gap,omitempty"` + OauthAuthorizeUrl string `protobuf:"bytes,18,opt,name=oauth_authorize_url,json=oauthAuthorizeUrl,proto3" json:"oauth_authorize_url,omitempty"` + OauthTokenUrl string `protobuf:"bytes,19,opt,name=oauth_token_url,json=oauthTokenUrl,proto3" json:"oauth_token_url,omitempty"` + OauthScopes []string `protobuf:"bytes,20,rep,name=oauth_scopes,json=oauthScopes,proto3" json:"oauth_scopes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DatasourceProviderDescriptor) Reset() { @@ -2256,6 +2375,41 @@ func (x *DatasourceProviderDescriptor) GetLiveDeliveryConfigured() bool { return false } +func (x *DatasourceProviderDescriptor) GetAcceptsOperations() bool { + if x != nil { + return x.AcceptsOperations + } + return false +} + +func (x *DatasourceProviderDescriptor) GetOperationsGap() string { + if x != nil { + return x.OperationsGap + } + return "" +} + +func (x *DatasourceProviderDescriptor) GetOauthAuthorizeUrl() string { + if x != nil { + return x.OauthAuthorizeUrl + } + return "" +} + +func (x *DatasourceProviderDescriptor) GetOauthTokenUrl() string { + if x != nil { + return x.OauthTokenUrl + } + return "" +} + +func (x *DatasourceProviderDescriptor) GetOauthScopes() []string { + if x != nil { + return x.OauthScopes + } + return nil +} + type GetDatasourceCatalogRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -4007,7 +4161,9 @@ type BeginDatasourceAccountLinkRequest struct { Connector string `protobuf:"bytes,2,opt,name=connector,proto3" json:"connector,omitempty"` // Where the provider sends the browser back. The provider checks it against // the callback URLs registered for the host's app; empty uses its default. - RedirectUri string `protobuf:"bytes,3,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"` + RedirectUri string `protobuf:"bytes,3,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"` + // Required for generic OAuth: the personal source created by this caller. + SourceId string `protobuf:"bytes,4,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4063,6 +4219,13 @@ func (x *BeginDatasourceAccountLinkRequest) GetRedirectUri() string { return "" } +func (x *BeginDatasourceAccountLinkRequest) GetSourceId() string { + if x != nil { + return x.SourceId + } + return "" +} + type BeginDatasourceAccountLinkResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Where to send the browser to sign in to the provider. @@ -5320,20 +5483,598 @@ func (x *RevokeSourceDelegationResponse) GetDelegation() *SourceDelegation { return nil } +// DatasourceService is the tenant-facing surface a solution drives (directly or +// through the generated SDK) to declare and operate connected datasources. +// SourceOperation is an admitted route and bounded JSON contract, never a credential. +type SourceOperation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"` + Query []string `protobuf:"bytes,5,rep,name=query,proto3" json:"query,omitempty"` + InputSchema *structpb.Struct `protobuf:"bytes,6,opt,name=input_schema,json=inputSchema,proto3" json:"input_schema,omitempty"` + OutputSchema *structpb.Struct `protobuf:"bytes,7,opt,name=output_schema,json=outputSchema,proto3" json:"output_schema,omitempty"` + Effect SourceOperation_Effect `protobuf:"varint,8,opt,name=effect,proto3,enum=saas.accounts.v1.SourceOperation_Effect" json:"effect,omitempty"` + MaxOutputBytes uint32 `protobuf:"varint,9,opt,name=max_output_bytes,json=maxOutputBytes,proto3" json:"max_output_bytes,omitempty"` + Digest string `protobuf:"bytes,10,opt,name=digest,proto3" json:"digest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceOperation) Reset() { + *x = SourceOperation{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceOperation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceOperation) ProtoMessage() {} + +func (x *SourceOperation) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SourceOperation.ProtoReflect.Descriptor instead. +func (*SourceOperation) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{65} +} + +func (x *SourceOperation) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SourceOperation) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *SourceOperation) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *SourceOperation) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *SourceOperation) GetQuery() []string { + if x != nil { + return x.Query + } + return nil +} + +func (x *SourceOperation) GetInputSchema() *structpb.Struct { + if x != nil { + return x.InputSchema + } + return nil +} + +func (x *SourceOperation) GetOutputSchema() *structpb.Struct { + if x != nil { + return x.OutputSchema + } + return nil +} + +func (x *SourceOperation) GetEffect() SourceOperation_Effect { + if x != nil { + return x.Effect + } + return SourceOperation_EFFECT_UNSPECIFIED +} + +func (x *SourceOperation) GetMaxOutputBytes() uint32 { + if x != nil { + return x.MaxOutputBytes + } + return 0 +} + +func (x *SourceOperation) GetDigest() string { + if x != nil { + return x.Digest + } + return "" +} + +type DeclareSourceOperationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + SourceId string `protobuf:"bytes,2,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` + Operations []*SourceOperation `protobuf:"bytes,3,rep,name=operations,proto3" json:"operations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeclareSourceOperationsRequest) Reset() { + *x = DeclareSourceOperationsRequest{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeclareSourceOperationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeclareSourceOperationsRequest) ProtoMessage() {} + +func (x *DeclareSourceOperationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeclareSourceOperationsRequest.ProtoReflect.Descriptor instead. +func (*DeclareSourceOperationsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{66} +} + +func (x *DeclareSourceOperationsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *DeclareSourceOperationsRequest) GetSourceId() string { + if x != nil { + return x.SourceId + } + return "" +} + +func (x *DeclareSourceOperationsRequest) GetOperations() []*SourceOperation { + if x != nil { + return x.Operations + } + return nil +} + +type DeclareSourceOperationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Operations []*SourceOperation `protobuf:"bytes,1,rep,name=operations,proto3" json:"operations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeclareSourceOperationsResponse) Reset() { + *x = DeclareSourceOperationsResponse{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeclareSourceOperationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeclareSourceOperationsResponse) ProtoMessage() {} + +func (x *DeclareSourceOperationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeclareSourceOperationsResponse.ProtoReflect.Descriptor instead. +func (*DeclareSourceOperationsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{67} +} + +func (x *DeclareSourceOperationsResponse) GetOperations() []*SourceOperation { + if x != nil { + return x.Operations + } + return nil +} + +type ListSourceOperationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + SourceId string `protobuf:"bytes,2,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSourceOperationsRequest) Reset() { + *x = ListSourceOperationsRequest{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSourceOperationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSourceOperationsRequest) ProtoMessage() {} + +func (x *ListSourceOperationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[68] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSourceOperationsRequest.ProtoReflect.Descriptor instead. +func (*ListSourceOperationsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{68} +} + +func (x *ListSourceOperationsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *ListSourceOperationsRequest) GetSourceId() string { + if x != nil { + return x.SourceId + } + return "" +} + +type ListSourceOperationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Operations []*SourceOperation `protobuf:"bytes,1,rep,name=operations,proto3" json:"operations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSourceOperationsResponse) Reset() { + *x = ListSourceOperationsResponse{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSourceOperationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSourceOperationsResponse) ProtoMessage() {} + +func (x *ListSourceOperationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[69] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSourceOperationsResponse.ProtoReflect.Descriptor instead. +func (*ListSourceOperationsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{69} +} + +func (x *ListSourceOperationsResponse) GetOperations() []*SourceOperation { + if x != nil { + return x.Operations + } + return nil +} + +// InvokeSourceOperationRequest carries exactly one JSON object in input_json. +// effect_id must match x-codefly-effect-id; either location may supply it. +type InvokeSourceOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + SourceId string `protobuf:"bytes,2,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` + Operation string `protobuf:"bytes,3,opt,name=operation,proto3" json:"operation,omitempty"` + InputJson string `protobuf:"bytes,4,opt,name=input_json,json=inputJson,proto3" json:"input_json,omitempty"` + EffectId string `protobuf:"bytes,5,opt,name=effect_id,json=effectId,proto3" json:"effect_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InvokeSourceOperationRequest) Reset() { + *x = InvokeSourceOperationRequest{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InvokeSourceOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvokeSourceOperationRequest) ProtoMessage() {} + +func (x *InvokeSourceOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[70] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvokeSourceOperationRequest.ProtoReflect.Descriptor instead. +func (*InvokeSourceOperationRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{70} +} + +func (x *InvokeSourceOperationRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *InvokeSourceOperationRequest) GetSourceId() string { + if x != nil { + return x.SourceId + } + return "" +} + +func (x *InvokeSourceOperationRequest) GetOperation() string { + if x != nil { + return x.Operation + } + return "" +} + +func (x *InvokeSourceOperationRequest) GetInputJson() string { + if x != nil { + return x.InputJson + } + return "" +} + +func (x *InvokeSourceOperationRequest) GetEffectId() string { + if x != nil { + return x.EffectId + } + return "" +} + +// SourceOperationReceipt contains only the committed result and safe metadata. +type SourceOperationReceipt struct { + state protoimpl.MessageState `protogen:"open.v1"` + EffectId string `protobuf:"bytes,1,opt,name=effect_id,json=effectId,proto3" json:"effect_id,omitempty"` + CommittedAt string `protobuf:"bytes,2,opt,name=committed_at,json=committedAt,proto3" json:"committed_at,omitempty"` // UTC RFC3339Nano, empty for an unknown outcome. + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` // committed or unknown; unknown never authorizes redispatch. + ProviderStatus uint32 `protobuf:"varint,4,opt,name=provider_status,json=providerStatus,proto3" json:"provider_status,omitempty"` + OutputJson string `protobuf:"bytes,5,opt,name=output_json,json=outputJson,proto3" json:"output_json,omitempty"` // Exactly one JSON object; {} when unknown. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceOperationReceipt) Reset() { + *x = SourceOperationReceipt{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceOperationReceipt) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceOperationReceipt) ProtoMessage() {} + +func (x *SourceOperationReceipt) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SourceOperationReceipt.ProtoReflect.Descriptor instead. +func (*SourceOperationReceipt) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{71} +} + +func (x *SourceOperationReceipt) GetEffectId() string { + if x != nil { + return x.EffectId + } + return "" +} + +func (x *SourceOperationReceipt) GetCommittedAt() string { + if x != nil { + return x.CommittedAt + } + return "" +} + +func (x *SourceOperationReceipt) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *SourceOperationReceipt) GetProviderStatus() uint32 { + if x != nil { + return x.ProviderStatus + } + return 0 +} + +func (x *SourceOperationReceipt) GetOutputJson() string { + if x != nil { + return x.OutputJson + } + return "" +} + +type InvokeSourceOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + OutputJson string `protobuf:"bytes,1,opt,name=output_json,json=outputJson,proto3" json:"output_json,omitempty"` // Exactly one JSON object; {} when unknown. + Receipt *SourceOperationReceipt `protobuf:"bytes,2,opt,name=receipt,proto3" json:"receipt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InvokeSourceOperationResponse) Reset() { + *x = InvokeSourceOperationResponse{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InvokeSourceOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvokeSourceOperationResponse) ProtoMessage() {} + +func (x *InvokeSourceOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[72] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvokeSourceOperationResponse.ProtoReflect.Descriptor instead. +func (*InvokeSourceOperationResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{72} +} + +func (x *InvokeSourceOperationResponse) GetOutputJson() string { + if x != nil { + return x.OutputJson + } + return "" +} + +func (x *InvokeSourceOperationResponse) GetReceipt() *SourceOperationReceipt { + if x != nil { + return x.Receipt + } + return nil +} + +// Lookup uses the caller's verified tenant and the effect's stored binding. +// A single string effect selector is the Runnable runtime's recovery contract. +type LookupInvokeSourceOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EffectId string `protobuf:"bytes,1,opt,name=effect_id,json=effectId,proto3" json:"effect_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LookupInvokeSourceOperationRequest) Reset() { + *x = LookupInvokeSourceOperationRequest{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LookupInvokeSourceOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LookupInvokeSourceOperationRequest) ProtoMessage() {} + +func (x *LookupInvokeSourceOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[73] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LookupInvokeSourceOperationRequest.ProtoReflect.Descriptor instead. +func (*LookupInvokeSourceOperationRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{73} +} + +func (x *LookupInvokeSourceOperationRequest) GetEffectId() string { + if x != nil { + return x.EffectId + } + return "" +} + var File_saas_accounts_v1_datasource_proto protoreflect.FileDescriptor const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "\n" + - "!saas/accounts/v1/datasource.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"\x83\x01\n" + + "!saas/accounts/v1/datasource.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a!codefly/runnable/v0/options.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"\x83\x01\n" + "\x16GitHubDatasourceConfig\x12\x12\n" + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x14\n" + "\x05paths\x18\x02 \x03(\tR\x05paths\x12\x16\n" + "\x06branch\x18\x03 \x01(\tR\x06branch\x12'\n" + - "\x0ffile_extensions\x18\x04 \x03(\tR\x0efileExtensions\"c\n" + - "\x0fApiOAuth2Config\x12\x1b\n" + + "\x0ffile_extensions\x18\x04 \x03(\tR\x0efileExtensions\"\x97\x02\n" + + "\x0fApiOAuth2Config\x12=\n" + + "\x05grant\x18\x04 \x01(\x0e2'.saas.accounts.v1.ApiOAuth2Config.GrantR\x05grant\x12\x1b\n" + "\ttoken_url\x18\x01 \x01(\tR\btokenUrl\x12\x1b\n" + "\tclient_id\x18\x02 \x01(\tR\bclientId\x12\x16\n" + - "\x06scopes\x18\x03 \x03(\tR\x06scopes\"\xc1\x02\n" + + "\x06scopes\x18\x03 \x03(\tR\x06scopes\"s\n" + + "\x05Grant\x12\x15\n" + + "\x11GRANT_UNSPECIFIED\x10\x00\x12\x17\n" + + "\x13GRANT_REFRESH_TOKEN\x10\x01\x12\x1c\n" + + "\x18GRANT_CLIENT_CREDENTIALS\x10\x02\x12\x1c\n" + + "\x18GRANT_AUTHORIZATION_CODE\x10\x03\"\xc1\x02\n" + "\x13ApiDatasourceConfig\x12\x19\n" + "\bbase_url\x18\x01 \x01(\tR\abaseUrl\x12#\n" + "\rresource_path\x18\x02 \x01(\tR\fresourcePath\x12L\n" + @@ -5432,7 +6173,7 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "\x0emax_item_bytes\x18\x03 \x01(\x03R\fmaxItemBytes\x122\n" + "\x15operations_per_window\x18\x04 \x01(\rR\x13operationsPerWindow\x12%\n" + "\x0ewindow_seconds\x18\x05 \x01(\x03R\rwindowSeconds\x128\n" + - "\x18background_share_percent\x18\x06 \x01(\rR\x16backgroundSharePercent\"\x8b\a\n" + + "\x18background_share_percent\x18\x06 \x01(\rR\x16backgroundSharePercent\"\xdc\b\n" + "\x1cDatasourceProviderDescriptor\x12@\n" + "\bprovider\x18\x01 \x01(\x0e2$.saas.accounts.v1.DatasourceProviderR\bprovider\x12!\n" + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12 \n" + @@ -5451,7 +6192,12 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "conformant\x12'\n" + "\x0fconformance_gap\x18\r \x01(\tR\x0econformanceGap\x12.\n" + "\x13accepts_new_sources\x18\x0e \x01(\bR\x11acceptsNewSources\x128\n" + - "\x18live_delivery_configured\x18\x0f \x01(\bR\x16liveDeliveryConfigured\"\x1d\n" + + "\x18live_delivery_configured\x18\x0f \x01(\bR\x16liveDeliveryConfigured\x12-\n" + + "\x12accepts_operations\x18\x10 \x01(\bR\x11acceptsOperations\x12%\n" + + "\x0eoperations_gap\x18\x11 \x01(\tR\roperationsGap\x12.\n" + + "\x13oauth_authorize_url\x18\x12 \x01(\tR\x11oauthAuthorizeUrl\x12&\n" + + "\x0foauth_token_url\x18\x13 \x01(\tR\roauthTokenUrl\x12!\n" + + "\foauth_scopes\x18\x14 \x03(\tR\voauthScopes\"\x1d\n" + "\x1bGetDatasourceCatalogRequest\"l\n" + "\x1cGetDatasourceCatalogResponse\x12L\n" + "\tproviders\x18\x01 \x03(\v2..saas.accounts.v1.DatasourceProviderDescriptorR\tproviders\"5\n" + @@ -5584,11 +6330,12 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "created_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"=\n" + "\x17DatasourceDirectoryTeam\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + - "\x04name\x18\x02 \x01(\tR\x04name\"\xb2\x01\n" + + "\x04name\x18\x02 \x01(\tR\x04name\"\xcf\x01\n" + "!BeginDatasourceAccountLinkRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12?\n" + "\tconnector\x18\x02 \x01(\tB!\xbaH\x1er\x1c\x10\x02\x18 2\x16^[a-z][a-z0-9-]{1,31}$R\tconnector\x12+\n" + - "\fredirect_uri\x18\x03 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x10R\vredirectUri\"\x9a\x01\n" + + "\fredirect_uri\x18\x03 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x10R\vredirectUri\x12\x1b\n" + + "\tsource_id\x18\x04 \x01(\tR\bsourceId\"\x9a\x01\n" + "\"BeginDatasourceAccountLinkResponse\x12#\n" + "\rauthorize_url\x18\x01 \x01(\tR\fauthorizeUrl\x12\x14\n" + "\x05state\x18\x02 \x01(\tR\x05state\x129\n" + @@ -5597,7 +6344,7 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "$CompleteDatasourceAccountLinkRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12 \n" + "\x05state\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\bR\x05state\x12\x1e\n" + + "\xbaH\ar\x05\x10\x01\x18\x80@R\x05state\x12\x1e\n" + "\x04code\x18\x03 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\xff\x01R\x04code\"d\n" + "%CompleteDatasourceAccountLinkResponse\x12;\n" + @@ -5673,7 +6420,61 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "\x1eRevokeSourceDelegationResponse\x12B\n" + "\n" + "delegation\x18\x01 \x01(\v2\".saas.accounts.v1.SourceDelegationR\n" + - "delegation*\xb7\x01\n" + + "delegation\"\xd4\x03\n" + + "\x0fSourceOperation\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x16\n" + + "\x06method\x18\x03 \x01(\tR\x06method\x12\x12\n" + + "\x04path\x18\x04 \x01(\tR\x04path\x12\x14\n" + + "\x05query\x18\x05 \x03(\tR\x05query\x12:\n" + + "\finput_schema\x18\x06 \x01(\v2\x17.google.protobuf.StructR\vinputSchema\x12<\n" + + "\routput_schema\x18\a \x01(\v2\x17.google.protobuf.StructR\foutputSchema\x12@\n" + + "\x06effect\x18\b \x01(\x0e2(.saas.accounts.v1.SourceOperation.EffectR\x06effect\x12(\n" + + "\x10max_output_bytes\x18\t \x01(\rR\x0emaxOutputBytes\x12\x16\n" + + "\x06digest\x18\n" + + " \x01(\tR\x06digest\"K\n" + + "\x06Effect\x12\x16\n" + + "\x12EFFECT_UNSPECIFIED\x10\x00\x12\x14\n" + + "\x10EFFECT_READ_ONLY\x10\x01\x12\x13\n" + + "\x0fEFFECT_MUTATION\x10\x02\"\xab\x01\n" + + "\x1eDeclareSourceOperationsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12%\n" + + "\tsource_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\bsourceId\x12A\n" + + "\n" + + "operations\x18\x03 \x03(\v2!.saas.accounts.v1.SourceOperationR\n" + + "operations\"d\n" + + "\x1fDeclareSourceOperationsResponse\x12A\n" + + "\n" + + "operations\x18\x01 \x03(\v2!.saas.accounts.v1.SourceOperationR\n" + + "operations\"e\n" + + "\x1bListSourceOperationsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12%\n" + + "\tsource_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\bsourceId\"a\n" + + "\x1cListSourceOperationsResponse\x12A\n" + + "\n" + + "operations\x18\x01 \x03(\v2!.saas.accounts.v1.SourceOperationR\n" + + "operations\"\xf4\x01\n" + + "\x1cInvokeSourceOperationRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12%\n" + + "\tsource_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\bsourceId\x12;\n" + + "\toperation\x18\x03 \x01(\tB\x1d\xbaH\x1ar\x182\x16^[a-z][a-z0-9_]{0,63}$R\toperation\x12(\n" + + "\n" + + "input_json\x18\x04 \x01(\tB\t\xbaH\x06r\x04(\x80\x80\x04R\tinputJson\x12%\n" + + "\teffect_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03(\x80\x01R\beffectId\"\xba\x01\n" + + "\x16SourceOperationReceipt\x12\x1b\n" + + "\teffect_id\x18\x01 \x01(\tR\beffectId\x12!\n" + + "\fcommitted_at\x18\x02 \x01(\tR\vcommittedAt\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12'\n" + + "\x0fprovider_status\x18\x04 \x01(\rR\x0eproviderStatus\x12\x1f\n" + + "\voutput_json\x18\x05 \x01(\tR\n" + + "outputJson\"\x84\x01\n" + + "\x1dInvokeSourceOperationResponse\x12\x1f\n" + + "\voutput_json\x18\x01 \x01(\tR\n" + + "outputJson\x12B\n" + + "\areceipt\x18\x02 \x01(\v2(.saas.accounts.v1.SourceOperationReceiptR\areceipt\"M\n" + + "\"LookupInvokeSourceOperationRequest\x12'\n" + + "\teffect_id\x18\x01 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01(\x80\x01R\beffectId*\xb7\x01\n" + "\x12DatasourceProvider\x12#\n" + "\x1fDATASOURCE_PROVIDER_UNSPECIFIED\x10\x00\x12\x1e\n" + "\x1aDATASOURCE_PROVIDER_GITHUB\x10\x01\x12\x1b\n" + @@ -5749,8 +6550,20 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "+SOURCE_DELEGATION_REVOCATION_MEMBER_REMOVED\x10\x04\x120\n" + ",SOURCE_DELEGATION_REVOCATION_PERMISSION_LOST\x10\x05\x12.\n" + "*SOURCE_DELEGATION_REVOCATION_USER_INACTIVE\x10\x06\x120\n" + - ",SOURCE_DELEGATION_REVOCATION_BINDING_CHANGED\x10\a2\xd2 \n" + - "\x11DatasourceService\x12\xac\x01\n" + + ",SOURCE_DELEGATION_REVOCATION_BINDING_CHANGED\x10\a2\xd6*\n" + + "\x11DatasourceService\x12\xfc\x03\n" + + "\x15InvokeSourceOperation\x12..saas.accounts.v1.InvokeSourceOperationRequest\x1a/.saas.accounts.v1.InvokeSourceOperationResponse\"\x81\x03\xc2\xf3\x18n\b\x02\x10\x03\x1a\x11datasource:invoke\"\x11datasource:invoke*\x0f\n" + + "\tsource_id\x10\x04\x18\x030\x01:%\n" + + "!saas.datasource.operation.invoked\x10\x04@\x03H\x04P\x04X\x04`\x01\xda\xf3\x18\xaf\x01\n" + + "\x02\b<\x12\x03\b\xd8\x04\x18\x05\"\x02\b\x02*\vUNAVAILABLE*\x12RESOURCE_EXHAUSTED*\aABORTED2\x0fsaas-datasourceJ?/saas.accounts.v1.DatasourceService/LookupInvokeSourceOperationP\x80\x80\x04X\x80\x80@`\x01r\x18\n" + + "\x06source\x12\x06invoke\x12\x04read\x18\x01\x82\xd3\xe4\x93\x02U:\x01*\"P/v1/organizations/{org_id}/datasources/{source_id}/operations/{operation}:invoke\x12\x87\x02\n" + + "\x1bLookupInvokeSourceOperation\x124.saas.accounts.v1.LookupInvokeSourceOperationRequest\x1a/.saas.accounts.v1.InvokeSourceOperationResponse\"\x80\x01\xc2\xf3\x18G\b\x02\x10\x03\x1a\x0fdatasource:read\"\x0fdatasource:read*\x0f\n" + + "\teffect_id\x10\x04\x18\x030\x01:\x02\x10\x01@\x01H\x03P\x03X\x04`\x01\x82\xd3\xe4\x93\x02/\x12-/v1/datasource-operation-receipts/{effect_id}\x12\xe2\x01\n" + + "\x14ListSourceOperations\x12-.saas.accounts.v1.ListSourceOperationsRequest\x1a..saas.accounts.v1.ListSourceOperationsResponse\"k\xc2\xf3\x18\"\b\x02\x10\x03*\f\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02?\x12=/v1/organizations/{org_id}/datasources/{source_id}/operations\x12\x93\x02\n" + + "\x17DeclareSourceOperations\x120.saas.accounts.v1.DeclareSourceOperationsRequest\x1a1.saas.accounts.v1.DeclareSourceOperationsResponse\"\x92\x01\xc2\xf3\x18F\b\x02\x10\x04*\f\n" + + "\x06org_id\x10\x02\x18\x010\x01:&\n" + + "\"saas.datasource.operation.declared\x10\x02@\x01H\x04P\x03X\x03`\x01\x82\xd3\xe4\x93\x02B:\x01*\"=/v1/organizations/{org_id}/datasources/{source_id}/operations\x12\xac\x01\n" + "\x0fAddGitHubSource\x12(.saas.accounts.v1.AddGitHubSourceRequest\x1a).saas.accounts.v1.AddGitHubSourceResponse\"D\xc2\xf3\x18@\b\x02\x10\x04*\f\n" + "\x06org_id\x10\x02\x18\x010\x01: \n" + "\x1csaas.datasource.source.added\x10\x02@\x01H\x04P\x04X\x03`\x01\x12\x9a\x01\n" + @@ -5827,8 +6640,8 @@ func file_saas_accounts_v1_datasource_proto_rawDescGZIP() []byte { return file_saas_accounts_v1_datasource_proto_rawDescData } -var file_saas_accounts_v1_datasource_proto_enumTypes = make([]protoimpl.EnumInfo, 12) -var file_saas_accounts_v1_datasource_proto_msgTypes = make([]protoimpl.MessageInfo, 65) +var file_saas_accounts_v1_datasource_proto_enumTypes = make([]protoimpl.EnumInfo, 14) +var file_saas_accounts_v1_datasource_proto_msgTypes = make([]protoimpl.MessageInfo, 74) var file_saas_accounts_v1_datasource_proto_goTypes = []any{ (DatasourceProvider)(0), // 0: saas.accounts.v1.DatasourceProvider (DatasourceStatus)(0), // 1: saas.accounts.v1.DatasourceStatus @@ -5842,198 +6655,226 @@ var file_saas_accounts_v1_datasource_proto_goTypes = []any{ (SourceSyncTrigger)(0), // 9: saas.accounts.v1.SourceSyncTrigger (DatasourceDomainStatus)(0), // 10: saas.accounts.v1.DatasourceDomainStatus (SourceDelegationRevocation)(0), // 11: saas.accounts.v1.SourceDelegationRevocation - (*GitHubDatasourceConfig)(nil), // 12: saas.accounts.v1.GitHubDatasourceConfig - (*ApiOAuth2Config)(nil), // 13: saas.accounts.v1.ApiOAuth2Config - (*ApiDatasourceConfig)(nil), // 14: saas.accounts.v1.ApiDatasourceConfig - (*CrawlerDatasourceConfig)(nil), // 15: saas.accounts.v1.CrawlerDatasourceConfig - (*UploadDatasourceConfig)(nil), // 16: saas.accounts.v1.UploadDatasourceConfig - (*Datasource)(nil), // 17: saas.accounts.v1.Datasource - (*AddGitHubSourceRequest)(nil), // 18: saas.accounts.v1.AddGitHubSourceRequest - (*AddGitHubSourceResponse)(nil), // 19: saas.accounts.v1.AddGitHubSourceResponse - (*AddSourceRequest)(nil), // 20: saas.accounts.v1.AddSourceRequest - (*AddSourceResponse)(nil), // 21: saas.accounts.v1.AddSourceResponse - (*DatasourceConfigField)(nil), // 22: saas.accounts.v1.DatasourceConfigField - (*DatasourceConnectorBudget)(nil), // 23: saas.accounts.v1.DatasourceConnectorBudget - (*DatasourceProviderDescriptor)(nil), // 24: saas.accounts.v1.DatasourceProviderDescriptor - (*GetDatasourceCatalogRequest)(nil), // 25: saas.accounts.v1.GetDatasourceCatalogRequest - (*GetDatasourceCatalogResponse)(nil), // 26: saas.accounts.v1.GetDatasourceCatalogResponse - (*ListSourcesRequest)(nil), // 27: saas.accounts.v1.ListSourcesRequest - (*ListSourcesResponse)(nil), // 28: saas.accounts.v1.ListSourcesResponse - (*GetSourceRequest)(nil), // 29: saas.accounts.v1.GetSourceRequest - (*GetSourceResponse)(nil), // 30: saas.accounts.v1.GetSourceResponse - (*SyncSourceRequest)(nil), // 31: saas.accounts.v1.SyncSourceRequest - (*SyncSourceResponse)(nil), // 32: saas.accounts.v1.SyncSourceResponse - (*GetSourceSyncRequest)(nil), // 33: saas.accounts.v1.GetSourceSyncRequest - (*SourceSyncDelivery)(nil), // 34: saas.accounts.v1.SourceSyncDelivery - (*SourceSyncChanges)(nil), // 35: saas.accounts.v1.SourceSyncChanges - (*SourceSyncFailure)(nil), // 36: saas.accounts.v1.SourceSyncFailure - (*SourceSyncProgress)(nil), // 37: saas.accounts.v1.SourceSyncProgress - (*GetSourceSyncResponse)(nil), // 38: saas.accounts.v1.GetSourceSyncResponse - (*DeleteSourceRequest)(nil), // 39: saas.accounts.v1.DeleteSourceRequest - (*DeleteSourceResponse)(nil), // 40: saas.accounts.v1.DeleteSourceResponse - (*GitHubAppRepository)(nil), // 41: saas.accounts.v1.GitHubAppRepository - (*BeginGitHubAppSetupRequest)(nil), // 42: saas.accounts.v1.BeginGitHubAppSetupRequest - (*BeginGitHubAppSetupResponse)(nil), // 43: saas.accounts.v1.BeginGitHubAppSetupResponse - (*CompleteGitHubAppSetupRequest)(nil), // 44: saas.accounts.v1.CompleteGitHubAppSetupRequest - (*CompleteGitHubAppSetupResponse)(nil), // 45: saas.accounts.v1.CompleteGitHubAppSetupResponse - (*MigrateGitHubSourceToAppRequest)(nil), // 46: saas.accounts.v1.MigrateGitHubSourceToAppRequest - (*MigrateGitHubSourceToAppResponse)(nil), // 47: saas.accounts.v1.MigrateGitHubSourceToAppResponse - (*DatasourceAccountLink)(nil), // 48: saas.accounts.v1.DatasourceAccountLink - (*DatasourceGroupBinding)(nil), // 49: saas.accounts.v1.DatasourceGroupBinding - (*DatasourceVerifiedDomain)(nil), // 50: saas.accounts.v1.DatasourceVerifiedDomain - (*DatasourceDirectoryTeam)(nil), // 51: saas.accounts.v1.DatasourceDirectoryTeam - (*BeginDatasourceAccountLinkRequest)(nil), // 52: saas.accounts.v1.BeginDatasourceAccountLinkRequest - (*BeginDatasourceAccountLinkResponse)(nil), // 53: saas.accounts.v1.BeginDatasourceAccountLinkResponse - (*CompleteDatasourceAccountLinkRequest)(nil), // 54: saas.accounts.v1.CompleteDatasourceAccountLinkRequest - (*CompleteDatasourceAccountLinkResponse)(nil), // 55: saas.accounts.v1.CompleteDatasourceAccountLinkResponse - (*ListMyDatasourceAccountLinksRequest)(nil), // 56: saas.accounts.v1.ListMyDatasourceAccountLinksRequest - (*ListMyDatasourceAccountLinksResponse)(nil), // 57: saas.accounts.v1.ListMyDatasourceAccountLinksResponse - (*DeleteDatasourceAccountLinkRequest)(nil), // 58: saas.accounts.v1.DeleteDatasourceAccountLinkRequest - (*DeleteDatasourceAccountLinkResponse)(nil), // 59: saas.accounts.v1.DeleteDatasourceAccountLinkResponse - (*GetDatasourceDirectoryRequest)(nil), // 60: saas.accounts.v1.GetDatasourceDirectoryRequest - (*GetDatasourceDirectoryResponse)(nil), // 61: saas.accounts.v1.GetDatasourceDirectoryResponse - (*BindDatasourceGroupRequest)(nil), // 62: saas.accounts.v1.BindDatasourceGroupRequest - (*BindDatasourceGroupResponse)(nil), // 63: saas.accounts.v1.BindDatasourceGroupResponse - (*UnbindDatasourceGroupRequest)(nil), // 64: saas.accounts.v1.UnbindDatasourceGroupRequest - (*UnbindDatasourceGroupResponse)(nil), // 65: saas.accounts.v1.UnbindDatasourceGroupResponse - (*ClaimDatasourceDomainRequest)(nil), // 66: saas.accounts.v1.ClaimDatasourceDomainRequest - (*ClaimDatasourceDomainResponse)(nil), // 67: saas.accounts.v1.ClaimDatasourceDomainResponse - (*VerifyDatasourceDomainRequest)(nil), // 68: saas.accounts.v1.VerifyDatasourceDomainRequest - (*VerifyDatasourceDomainResponse)(nil), // 69: saas.accounts.v1.VerifyDatasourceDomainResponse - (*DeleteDatasourceDomainRequest)(nil), // 70: saas.accounts.v1.DeleteDatasourceDomainRequest - (*DeleteDatasourceDomainResponse)(nil), // 71: saas.accounts.v1.DeleteDatasourceDomainResponse - (*SourceDelegation)(nil), // 72: saas.accounts.v1.SourceDelegation - (*ListSourceDelegationsRequest)(nil), // 73: saas.accounts.v1.ListSourceDelegationsRequest - (*ListSourceDelegationsResponse)(nil), // 74: saas.accounts.v1.ListSourceDelegationsResponse - (*RevokeSourceDelegationRequest)(nil), // 75: saas.accounts.v1.RevokeSourceDelegationRequest - (*RevokeSourceDelegationResponse)(nil), // 76: saas.accounts.v1.RevokeSourceDelegationResponse - (*timestamppb.Timestamp)(nil), // 77: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 78: google.protobuf.Duration - (v1.JobState)(0), // 79: saas.jobs.v1.JobState - (*v1.JobExecutionReference)(nil), // 80: saas.jobs.v1.JobExecutionReference + (ApiOAuth2Config_Grant)(0), // 12: saas.accounts.v1.ApiOAuth2Config.Grant + (SourceOperation_Effect)(0), // 13: saas.accounts.v1.SourceOperation.Effect + (*GitHubDatasourceConfig)(nil), // 14: saas.accounts.v1.GitHubDatasourceConfig + (*ApiOAuth2Config)(nil), // 15: saas.accounts.v1.ApiOAuth2Config + (*ApiDatasourceConfig)(nil), // 16: saas.accounts.v1.ApiDatasourceConfig + (*CrawlerDatasourceConfig)(nil), // 17: saas.accounts.v1.CrawlerDatasourceConfig + (*UploadDatasourceConfig)(nil), // 18: saas.accounts.v1.UploadDatasourceConfig + (*Datasource)(nil), // 19: saas.accounts.v1.Datasource + (*AddGitHubSourceRequest)(nil), // 20: saas.accounts.v1.AddGitHubSourceRequest + (*AddGitHubSourceResponse)(nil), // 21: saas.accounts.v1.AddGitHubSourceResponse + (*AddSourceRequest)(nil), // 22: saas.accounts.v1.AddSourceRequest + (*AddSourceResponse)(nil), // 23: saas.accounts.v1.AddSourceResponse + (*DatasourceConfigField)(nil), // 24: saas.accounts.v1.DatasourceConfigField + (*DatasourceConnectorBudget)(nil), // 25: saas.accounts.v1.DatasourceConnectorBudget + (*DatasourceProviderDescriptor)(nil), // 26: saas.accounts.v1.DatasourceProviderDescriptor + (*GetDatasourceCatalogRequest)(nil), // 27: saas.accounts.v1.GetDatasourceCatalogRequest + (*GetDatasourceCatalogResponse)(nil), // 28: saas.accounts.v1.GetDatasourceCatalogResponse + (*ListSourcesRequest)(nil), // 29: saas.accounts.v1.ListSourcesRequest + (*ListSourcesResponse)(nil), // 30: saas.accounts.v1.ListSourcesResponse + (*GetSourceRequest)(nil), // 31: saas.accounts.v1.GetSourceRequest + (*GetSourceResponse)(nil), // 32: saas.accounts.v1.GetSourceResponse + (*SyncSourceRequest)(nil), // 33: saas.accounts.v1.SyncSourceRequest + (*SyncSourceResponse)(nil), // 34: saas.accounts.v1.SyncSourceResponse + (*GetSourceSyncRequest)(nil), // 35: saas.accounts.v1.GetSourceSyncRequest + (*SourceSyncDelivery)(nil), // 36: saas.accounts.v1.SourceSyncDelivery + (*SourceSyncChanges)(nil), // 37: saas.accounts.v1.SourceSyncChanges + (*SourceSyncFailure)(nil), // 38: saas.accounts.v1.SourceSyncFailure + (*SourceSyncProgress)(nil), // 39: saas.accounts.v1.SourceSyncProgress + (*GetSourceSyncResponse)(nil), // 40: saas.accounts.v1.GetSourceSyncResponse + (*DeleteSourceRequest)(nil), // 41: saas.accounts.v1.DeleteSourceRequest + (*DeleteSourceResponse)(nil), // 42: saas.accounts.v1.DeleteSourceResponse + (*GitHubAppRepository)(nil), // 43: saas.accounts.v1.GitHubAppRepository + (*BeginGitHubAppSetupRequest)(nil), // 44: saas.accounts.v1.BeginGitHubAppSetupRequest + (*BeginGitHubAppSetupResponse)(nil), // 45: saas.accounts.v1.BeginGitHubAppSetupResponse + (*CompleteGitHubAppSetupRequest)(nil), // 46: saas.accounts.v1.CompleteGitHubAppSetupRequest + (*CompleteGitHubAppSetupResponse)(nil), // 47: saas.accounts.v1.CompleteGitHubAppSetupResponse + (*MigrateGitHubSourceToAppRequest)(nil), // 48: saas.accounts.v1.MigrateGitHubSourceToAppRequest + (*MigrateGitHubSourceToAppResponse)(nil), // 49: saas.accounts.v1.MigrateGitHubSourceToAppResponse + (*DatasourceAccountLink)(nil), // 50: saas.accounts.v1.DatasourceAccountLink + (*DatasourceGroupBinding)(nil), // 51: saas.accounts.v1.DatasourceGroupBinding + (*DatasourceVerifiedDomain)(nil), // 52: saas.accounts.v1.DatasourceVerifiedDomain + (*DatasourceDirectoryTeam)(nil), // 53: saas.accounts.v1.DatasourceDirectoryTeam + (*BeginDatasourceAccountLinkRequest)(nil), // 54: saas.accounts.v1.BeginDatasourceAccountLinkRequest + (*BeginDatasourceAccountLinkResponse)(nil), // 55: saas.accounts.v1.BeginDatasourceAccountLinkResponse + (*CompleteDatasourceAccountLinkRequest)(nil), // 56: saas.accounts.v1.CompleteDatasourceAccountLinkRequest + (*CompleteDatasourceAccountLinkResponse)(nil), // 57: saas.accounts.v1.CompleteDatasourceAccountLinkResponse + (*ListMyDatasourceAccountLinksRequest)(nil), // 58: saas.accounts.v1.ListMyDatasourceAccountLinksRequest + (*ListMyDatasourceAccountLinksResponse)(nil), // 59: saas.accounts.v1.ListMyDatasourceAccountLinksResponse + (*DeleteDatasourceAccountLinkRequest)(nil), // 60: saas.accounts.v1.DeleteDatasourceAccountLinkRequest + (*DeleteDatasourceAccountLinkResponse)(nil), // 61: saas.accounts.v1.DeleteDatasourceAccountLinkResponse + (*GetDatasourceDirectoryRequest)(nil), // 62: saas.accounts.v1.GetDatasourceDirectoryRequest + (*GetDatasourceDirectoryResponse)(nil), // 63: saas.accounts.v1.GetDatasourceDirectoryResponse + (*BindDatasourceGroupRequest)(nil), // 64: saas.accounts.v1.BindDatasourceGroupRequest + (*BindDatasourceGroupResponse)(nil), // 65: saas.accounts.v1.BindDatasourceGroupResponse + (*UnbindDatasourceGroupRequest)(nil), // 66: saas.accounts.v1.UnbindDatasourceGroupRequest + (*UnbindDatasourceGroupResponse)(nil), // 67: saas.accounts.v1.UnbindDatasourceGroupResponse + (*ClaimDatasourceDomainRequest)(nil), // 68: saas.accounts.v1.ClaimDatasourceDomainRequest + (*ClaimDatasourceDomainResponse)(nil), // 69: saas.accounts.v1.ClaimDatasourceDomainResponse + (*VerifyDatasourceDomainRequest)(nil), // 70: saas.accounts.v1.VerifyDatasourceDomainRequest + (*VerifyDatasourceDomainResponse)(nil), // 71: saas.accounts.v1.VerifyDatasourceDomainResponse + (*DeleteDatasourceDomainRequest)(nil), // 72: saas.accounts.v1.DeleteDatasourceDomainRequest + (*DeleteDatasourceDomainResponse)(nil), // 73: saas.accounts.v1.DeleteDatasourceDomainResponse + (*SourceDelegation)(nil), // 74: saas.accounts.v1.SourceDelegation + (*ListSourceDelegationsRequest)(nil), // 75: saas.accounts.v1.ListSourceDelegationsRequest + (*ListSourceDelegationsResponse)(nil), // 76: saas.accounts.v1.ListSourceDelegationsResponse + (*RevokeSourceDelegationRequest)(nil), // 77: saas.accounts.v1.RevokeSourceDelegationRequest + (*RevokeSourceDelegationResponse)(nil), // 78: saas.accounts.v1.RevokeSourceDelegationResponse + (*SourceOperation)(nil), // 79: saas.accounts.v1.SourceOperation + (*DeclareSourceOperationsRequest)(nil), // 80: saas.accounts.v1.DeclareSourceOperationsRequest + (*DeclareSourceOperationsResponse)(nil), // 81: saas.accounts.v1.DeclareSourceOperationsResponse + (*ListSourceOperationsRequest)(nil), // 82: saas.accounts.v1.ListSourceOperationsRequest + (*ListSourceOperationsResponse)(nil), // 83: saas.accounts.v1.ListSourceOperationsResponse + (*InvokeSourceOperationRequest)(nil), // 84: saas.accounts.v1.InvokeSourceOperationRequest + (*SourceOperationReceipt)(nil), // 85: saas.accounts.v1.SourceOperationReceipt + (*InvokeSourceOperationResponse)(nil), // 86: saas.accounts.v1.InvokeSourceOperationResponse + (*LookupInvokeSourceOperationRequest)(nil), // 87: saas.accounts.v1.LookupInvokeSourceOperationRequest + (*timestamppb.Timestamp)(nil), // 88: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 89: google.protobuf.Duration + (v1.JobState)(0), // 90: saas.jobs.v1.JobState + (*v1.JobExecutionReference)(nil), // 91: saas.jobs.v1.JobExecutionReference + (*structpb.Struct)(nil), // 92: google.protobuf.Struct } var file_saas_accounts_v1_datasource_proto_depIdxs = []int32{ - 3, // 0: saas.accounts.v1.ApiDatasourceConfig.credential_kind:type_name -> saas.accounts.v1.ApiCredentialKind - 13, // 1: saas.accounts.v1.ApiDatasourceConfig.oauth2:type_name -> saas.accounts.v1.ApiOAuth2Config - 0, // 2: saas.accounts.v1.Datasource.provider:type_name -> saas.accounts.v1.DatasourceProvider - 12, // 3: saas.accounts.v1.Datasource.github:type_name -> saas.accounts.v1.GitHubDatasourceConfig - 1, // 4: saas.accounts.v1.Datasource.status:type_name -> saas.accounts.v1.DatasourceStatus - 77, // 5: saas.accounts.v1.Datasource.created_at:type_name -> google.protobuf.Timestamp - 77, // 6: saas.accounts.v1.Datasource.updated_at:type_name -> google.protobuf.Timestamp - 77, // 7: saas.accounts.v1.Datasource.last_synced_at:type_name -> google.protobuf.Timestamp - 14, // 8: saas.accounts.v1.Datasource.api:type_name -> saas.accounts.v1.ApiDatasourceConfig - 15, // 9: saas.accounts.v1.Datasource.crawler:type_name -> saas.accounts.v1.CrawlerDatasourceConfig - 16, // 10: saas.accounts.v1.Datasource.upload:type_name -> saas.accounts.v1.UploadDatasourceConfig - 77, // 11: saas.accounts.v1.Datasource.last_ingested_at:type_name -> google.protobuf.Timestamp - 2, // 12: saas.accounts.v1.Datasource.live_delivery:type_name -> saas.accounts.v1.DatasourceLiveDelivery - 78, // 13: saas.accounts.v1.Datasource.reconcile_interval:type_name -> google.protobuf.Duration - 17, // 14: saas.accounts.v1.AddGitHubSourceResponse.datasource:type_name -> saas.accounts.v1.Datasource - 0, // 15: saas.accounts.v1.AddSourceRequest.provider:type_name -> saas.accounts.v1.DatasourceProvider - 12, // 16: saas.accounts.v1.AddSourceRequest.github:type_name -> saas.accounts.v1.GitHubDatasourceConfig - 14, // 17: saas.accounts.v1.AddSourceRequest.api:type_name -> saas.accounts.v1.ApiDatasourceConfig - 15, // 18: saas.accounts.v1.AddSourceRequest.crawler:type_name -> saas.accounts.v1.CrawlerDatasourceConfig - 16, // 19: saas.accounts.v1.AddSourceRequest.upload:type_name -> saas.accounts.v1.UploadDatasourceConfig - 17, // 20: saas.accounts.v1.AddSourceResponse.datasource:type_name -> saas.accounts.v1.Datasource - 0, // 21: saas.accounts.v1.DatasourceProviderDescriptor.provider:type_name -> saas.accounts.v1.DatasourceProvider - 22, // 22: saas.accounts.v1.DatasourceProviderDescriptor.config_fields:type_name -> saas.accounts.v1.DatasourceConfigField - 3, // 23: saas.accounts.v1.DatasourceProviderDescriptor.supported_credential_kinds:type_name -> saas.accounts.v1.ApiCredentialKind - 4, // 24: saas.accounts.v1.DatasourceProviderDescriptor.interface:type_name -> saas.accounts.v1.DatasourceConnectorInterface - 5, // 25: saas.accounts.v1.DatasourceProviderDescriptor.credential_modes:type_name -> saas.accounts.v1.DatasourceCredentialMode - 6, // 26: saas.accounts.v1.DatasourceProviderDescriptor.readers_model:type_name -> saas.accounts.v1.DatasourceReadersModel - 23, // 27: saas.accounts.v1.DatasourceProviderDescriptor.budget:type_name -> saas.accounts.v1.DatasourceConnectorBudget - 24, // 28: saas.accounts.v1.GetDatasourceCatalogResponse.providers:type_name -> saas.accounts.v1.DatasourceProviderDescriptor - 17, // 29: saas.accounts.v1.ListSourcesResponse.datasources:type_name -> saas.accounts.v1.Datasource - 17, // 30: saas.accounts.v1.GetSourceResponse.datasource:type_name -> saas.accounts.v1.Datasource - 79, // 31: saas.accounts.v1.SourceSyncDelivery.state:type_name -> saas.jobs.v1.JobState - 80, // 32: saas.accounts.v1.SourceSyncDelivery.execution:type_name -> saas.jobs.v1.JobExecutionReference - 8, // 33: saas.accounts.v1.SourceSyncFailure.reason:type_name -> saas.accounts.v1.SourceSyncFailureReason - 77, // 34: saas.accounts.v1.SourceSyncFailure.retry_at:type_name -> google.protobuf.Timestamp - 7, // 35: saas.accounts.v1.SourceSyncProgress.phase:type_name -> saas.accounts.v1.SourceSyncPhase - 9, // 36: saas.accounts.v1.SourceSyncProgress.trigger:type_name -> saas.accounts.v1.SourceSyncTrigger - 77, // 37: saas.accounts.v1.SourceSyncProgress.queued_at:type_name -> google.protobuf.Timestamp - 77, // 38: saas.accounts.v1.SourceSyncProgress.fetching_at:type_name -> google.protobuf.Timestamp - 77, // 39: saas.accounts.v1.SourceSyncProgress.compiled_at:type_name -> google.protobuf.Timestamp - 77, // 40: saas.accounts.v1.SourceSyncProgress.handed_off_at:type_name -> google.protobuf.Timestamp - 77, // 41: saas.accounts.v1.SourceSyncProgress.finished_at:type_name -> google.protobuf.Timestamp - 35, // 42: saas.accounts.v1.SourceSyncProgress.changes:type_name -> saas.accounts.v1.SourceSyncChanges - 36, // 43: saas.accounts.v1.SourceSyncProgress.failure:type_name -> saas.accounts.v1.SourceSyncFailure - 79, // 44: saas.accounts.v1.GetSourceSyncResponse.state:type_name -> saas.jobs.v1.JobState - 34, // 45: saas.accounts.v1.GetSourceSyncResponse.deliveries:type_name -> saas.accounts.v1.SourceSyncDelivery - 37, // 46: saas.accounts.v1.GetSourceSyncResponse.progress:type_name -> saas.accounts.v1.SourceSyncProgress - 77, // 47: saas.accounts.v1.BeginGitHubAppSetupResponse.expires_at:type_name -> google.protobuf.Timestamp - 41, // 48: saas.accounts.v1.CompleteGitHubAppSetupResponse.repositories:type_name -> saas.accounts.v1.GitHubAppRepository - 17, // 49: saas.accounts.v1.MigrateGitHubSourceToAppResponse.datasource:type_name -> saas.accounts.v1.Datasource - 77, // 50: saas.accounts.v1.DatasourceAccountLink.created_at:type_name -> google.protobuf.Timestamp - 77, // 51: saas.accounts.v1.DatasourceGroupBinding.created_at:type_name -> google.protobuf.Timestamp - 10, // 52: saas.accounts.v1.DatasourceVerifiedDomain.status:type_name -> saas.accounts.v1.DatasourceDomainStatus - 77, // 53: saas.accounts.v1.DatasourceVerifiedDomain.verified_at:type_name -> google.protobuf.Timestamp - 77, // 54: saas.accounts.v1.DatasourceVerifiedDomain.created_at:type_name -> google.protobuf.Timestamp - 77, // 55: saas.accounts.v1.BeginDatasourceAccountLinkResponse.expires_at:type_name -> google.protobuf.Timestamp - 48, // 56: saas.accounts.v1.CompleteDatasourceAccountLinkResponse.link:type_name -> saas.accounts.v1.DatasourceAccountLink - 48, // 57: saas.accounts.v1.ListMyDatasourceAccountLinksResponse.links:type_name -> saas.accounts.v1.DatasourceAccountLink - 48, // 58: saas.accounts.v1.GetDatasourceDirectoryResponse.links:type_name -> saas.accounts.v1.DatasourceAccountLink - 49, // 59: saas.accounts.v1.GetDatasourceDirectoryResponse.bindings:type_name -> saas.accounts.v1.DatasourceGroupBinding - 50, // 60: saas.accounts.v1.GetDatasourceDirectoryResponse.domains:type_name -> saas.accounts.v1.DatasourceVerifiedDomain - 51, // 61: saas.accounts.v1.GetDatasourceDirectoryResponse.teams:type_name -> saas.accounts.v1.DatasourceDirectoryTeam - 49, // 62: saas.accounts.v1.BindDatasourceGroupResponse.binding:type_name -> saas.accounts.v1.DatasourceGroupBinding - 50, // 63: saas.accounts.v1.ClaimDatasourceDomainResponse.domain:type_name -> saas.accounts.v1.DatasourceVerifiedDomain - 50, // 64: saas.accounts.v1.VerifyDatasourceDomainResponse.domain:type_name -> saas.accounts.v1.DatasourceVerifiedDomain - 77, // 65: saas.accounts.v1.SourceDelegation.created_at:type_name -> google.protobuf.Timestamp - 77, // 66: saas.accounts.v1.SourceDelegation.revoked_at:type_name -> google.protobuf.Timestamp - 11, // 67: saas.accounts.v1.SourceDelegation.revocation:type_name -> saas.accounts.v1.SourceDelegationRevocation - 72, // 68: saas.accounts.v1.ListSourceDelegationsResponse.delegations:type_name -> saas.accounts.v1.SourceDelegation - 72, // 69: saas.accounts.v1.RevokeSourceDelegationResponse.delegation:type_name -> saas.accounts.v1.SourceDelegation - 18, // 70: saas.accounts.v1.DatasourceService.AddGitHubSource:input_type -> saas.accounts.v1.AddGitHubSourceRequest - 20, // 71: saas.accounts.v1.DatasourceService.AddSource:input_type -> saas.accounts.v1.AddSourceRequest - 25, // 72: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:input_type -> saas.accounts.v1.GetDatasourceCatalogRequest - 27, // 73: saas.accounts.v1.DatasourceService.ListSources:input_type -> saas.accounts.v1.ListSourcesRequest - 29, // 74: saas.accounts.v1.DatasourceService.GetSource:input_type -> saas.accounts.v1.GetSourceRequest - 31, // 75: saas.accounts.v1.DatasourceService.SyncSource:input_type -> saas.accounts.v1.SyncSourceRequest - 33, // 76: saas.accounts.v1.DatasourceService.GetSourceSync:input_type -> saas.accounts.v1.GetSourceSyncRequest - 39, // 77: saas.accounts.v1.DatasourceService.DeleteSource:input_type -> saas.accounts.v1.DeleteSourceRequest - 42, // 78: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:input_type -> saas.accounts.v1.BeginGitHubAppSetupRequest - 44, // 79: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:input_type -> saas.accounts.v1.CompleteGitHubAppSetupRequest - 46, // 80: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:input_type -> saas.accounts.v1.MigrateGitHubSourceToAppRequest - 52, // 81: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:input_type -> saas.accounts.v1.BeginDatasourceAccountLinkRequest - 54, // 82: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:input_type -> saas.accounts.v1.CompleteDatasourceAccountLinkRequest - 56, // 83: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:input_type -> saas.accounts.v1.ListMyDatasourceAccountLinksRequest - 58, // 84: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:input_type -> saas.accounts.v1.DeleteDatasourceAccountLinkRequest - 60, // 85: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:input_type -> saas.accounts.v1.GetDatasourceDirectoryRequest - 62, // 86: saas.accounts.v1.DatasourceService.BindDatasourceGroup:input_type -> saas.accounts.v1.BindDatasourceGroupRequest - 64, // 87: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:input_type -> saas.accounts.v1.UnbindDatasourceGroupRequest - 66, // 88: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:input_type -> saas.accounts.v1.ClaimDatasourceDomainRequest - 68, // 89: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:input_type -> saas.accounts.v1.VerifyDatasourceDomainRequest - 70, // 90: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:input_type -> saas.accounts.v1.DeleteDatasourceDomainRequest - 73, // 91: saas.accounts.v1.DatasourceService.ListSourceDelegations:input_type -> saas.accounts.v1.ListSourceDelegationsRequest - 75, // 92: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:input_type -> saas.accounts.v1.RevokeSourceDelegationRequest - 19, // 93: saas.accounts.v1.DatasourceService.AddGitHubSource:output_type -> saas.accounts.v1.AddGitHubSourceResponse - 21, // 94: saas.accounts.v1.DatasourceService.AddSource:output_type -> saas.accounts.v1.AddSourceResponse - 26, // 95: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:output_type -> saas.accounts.v1.GetDatasourceCatalogResponse - 28, // 96: saas.accounts.v1.DatasourceService.ListSources:output_type -> saas.accounts.v1.ListSourcesResponse - 30, // 97: saas.accounts.v1.DatasourceService.GetSource:output_type -> saas.accounts.v1.GetSourceResponse - 32, // 98: saas.accounts.v1.DatasourceService.SyncSource:output_type -> saas.accounts.v1.SyncSourceResponse - 38, // 99: saas.accounts.v1.DatasourceService.GetSourceSync:output_type -> saas.accounts.v1.GetSourceSyncResponse - 40, // 100: saas.accounts.v1.DatasourceService.DeleteSource:output_type -> saas.accounts.v1.DeleteSourceResponse - 43, // 101: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:output_type -> saas.accounts.v1.BeginGitHubAppSetupResponse - 45, // 102: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:output_type -> saas.accounts.v1.CompleteGitHubAppSetupResponse - 47, // 103: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:output_type -> saas.accounts.v1.MigrateGitHubSourceToAppResponse - 53, // 104: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:output_type -> saas.accounts.v1.BeginDatasourceAccountLinkResponse - 55, // 105: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:output_type -> saas.accounts.v1.CompleteDatasourceAccountLinkResponse - 57, // 106: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:output_type -> saas.accounts.v1.ListMyDatasourceAccountLinksResponse - 59, // 107: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:output_type -> saas.accounts.v1.DeleteDatasourceAccountLinkResponse - 61, // 108: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:output_type -> saas.accounts.v1.GetDatasourceDirectoryResponse - 63, // 109: saas.accounts.v1.DatasourceService.BindDatasourceGroup:output_type -> saas.accounts.v1.BindDatasourceGroupResponse - 65, // 110: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:output_type -> saas.accounts.v1.UnbindDatasourceGroupResponse - 67, // 111: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:output_type -> saas.accounts.v1.ClaimDatasourceDomainResponse - 69, // 112: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:output_type -> saas.accounts.v1.VerifyDatasourceDomainResponse - 71, // 113: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:output_type -> saas.accounts.v1.DeleteDatasourceDomainResponse - 74, // 114: saas.accounts.v1.DatasourceService.ListSourceDelegations:output_type -> saas.accounts.v1.ListSourceDelegationsResponse - 76, // 115: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:output_type -> saas.accounts.v1.RevokeSourceDelegationResponse - 93, // [93:116] is the sub-list for method output_type - 70, // [70:93] is the sub-list for method input_type - 70, // [70:70] is the sub-list for extension type_name - 70, // [70:70] is the sub-list for extension extendee - 0, // [0:70] is the sub-list for field type_name + 12, // 0: saas.accounts.v1.ApiOAuth2Config.grant:type_name -> saas.accounts.v1.ApiOAuth2Config.Grant + 3, // 1: saas.accounts.v1.ApiDatasourceConfig.credential_kind:type_name -> saas.accounts.v1.ApiCredentialKind + 15, // 2: saas.accounts.v1.ApiDatasourceConfig.oauth2:type_name -> saas.accounts.v1.ApiOAuth2Config + 0, // 3: saas.accounts.v1.Datasource.provider:type_name -> saas.accounts.v1.DatasourceProvider + 14, // 4: saas.accounts.v1.Datasource.github:type_name -> saas.accounts.v1.GitHubDatasourceConfig + 1, // 5: saas.accounts.v1.Datasource.status:type_name -> saas.accounts.v1.DatasourceStatus + 88, // 6: saas.accounts.v1.Datasource.created_at:type_name -> google.protobuf.Timestamp + 88, // 7: saas.accounts.v1.Datasource.updated_at:type_name -> google.protobuf.Timestamp + 88, // 8: saas.accounts.v1.Datasource.last_synced_at:type_name -> google.protobuf.Timestamp + 16, // 9: saas.accounts.v1.Datasource.api:type_name -> saas.accounts.v1.ApiDatasourceConfig + 17, // 10: saas.accounts.v1.Datasource.crawler:type_name -> saas.accounts.v1.CrawlerDatasourceConfig + 18, // 11: saas.accounts.v1.Datasource.upload:type_name -> saas.accounts.v1.UploadDatasourceConfig + 88, // 12: saas.accounts.v1.Datasource.last_ingested_at:type_name -> google.protobuf.Timestamp + 2, // 13: saas.accounts.v1.Datasource.live_delivery:type_name -> saas.accounts.v1.DatasourceLiveDelivery + 89, // 14: saas.accounts.v1.Datasource.reconcile_interval:type_name -> google.protobuf.Duration + 19, // 15: saas.accounts.v1.AddGitHubSourceResponse.datasource:type_name -> saas.accounts.v1.Datasource + 0, // 16: saas.accounts.v1.AddSourceRequest.provider:type_name -> saas.accounts.v1.DatasourceProvider + 14, // 17: saas.accounts.v1.AddSourceRequest.github:type_name -> saas.accounts.v1.GitHubDatasourceConfig + 16, // 18: saas.accounts.v1.AddSourceRequest.api:type_name -> saas.accounts.v1.ApiDatasourceConfig + 17, // 19: saas.accounts.v1.AddSourceRequest.crawler:type_name -> saas.accounts.v1.CrawlerDatasourceConfig + 18, // 20: saas.accounts.v1.AddSourceRequest.upload:type_name -> saas.accounts.v1.UploadDatasourceConfig + 19, // 21: saas.accounts.v1.AddSourceResponse.datasource:type_name -> saas.accounts.v1.Datasource + 0, // 22: saas.accounts.v1.DatasourceProviderDescriptor.provider:type_name -> saas.accounts.v1.DatasourceProvider + 24, // 23: saas.accounts.v1.DatasourceProviderDescriptor.config_fields:type_name -> saas.accounts.v1.DatasourceConfigField + 3, // 24: saas.accounts.v1.DatasourceProviderDescriptor.supported_credential_kinds:type_name -> saas.accounts.v1.ApiCredentialKind + 4, // 25: saas.accounts.v1.DatasourceProviderDescriptor.interface:type_name -> saas.accounts.v1.DatasourceConnectorInterface + 5, // 26: saas.accounts.v1.DatasourceProviderDescriptor.credential_modes:type_name -> saas.accounts.v1.DatasourceCredentialMode + 6, // 27: saas.accounts.v1.DatasourceProviderDescriptor.readers_model:type_name -> saas.accounts.v1.DatasourceReadersModel + 25, // 28: saas.accounts.v1.DatasourceProviderDescriptor.budget:type_name -> saas.accounts.v1.DatasourceConnectorBudget + 26, // 29: saas.accounts.v1.GetDatasourceCatalogResponse.providers:type_name -> saas.accounts.v1.DatasourceProviderDescriptor + 19, // 30: saas.accounts.v1.ListSourcesResponse.datasources:type_name -> saas.accounts.v1.Datasource + 19, // 31: saas.accounts.v1.GetSourceResponse.datasource:type_name -> saas.accounts.v1.Datasource + 90, // 32: saas.accounts.v1.SourceSyncDelivery.state:type_name -> saas.jobs.v1.JobState + 91, // 33: saas.accounts.v1.SourceSyncDelivery.execution:type_name -> saas.jobs.v1.JobExecutionReference + 8, // 34: saas.accounts.v1.SourceSyncFailure.reason:type_name -> saas.accounts.v1.SourceSyncFailureReason + 88, // 35: saas.accounts.v1.SourceSyncFailure.retry_at:type_name -> google.protobuf.Timestamp + 7, // 36: saas.accounts.v1.SourceSyncProgress.phase:type_name -> saas.accounts.v1.SourceSyncPhase + 9, // 37: saas.accounts.v1.SourceSyncProgress.trigger:type_name -> saas.accounts.v1.SourceSyncTrigger + 88, // 38: saas.accounts.v1.SourceSyncProgress.queued_at:type_name -> google.protobuf.Timestamp + 88, // 39: saas.accounts.v1.SourceSyncProgress.fetching_at:type_name -> google.protobuf.Timestamp + 88, // 40: saas.accounts.v1.SourceSyncProgress.compiled_at:type_name -> google.protobuf.Timestamp + 88, // 41: saas.accounts.v1.SourceSyncProgress.handed_off_at:type_name -> google.protobuf.Timestamp + 88, // 42: saas.accounts.v1.SourceSyncProgress.finished_at:type_name -> google.protobuf.Timestamp + 37, // 43: saas.accounts.v1.SourceSyncProgress.changes:type_name -> saas.accounts.v1.SourceSyncChanges + 38, // 44: saas.accounts.v1.SourceSyncProgress.failure:type_name -> saas.accounts.v1.SourceSyncFailure + 90, // 45: saas.accounts.v1.GetSourceSyncResponse.state:type_name -> saas.jobs.v1.JobState + 36, // 46: saas.accounts.v1.GetSourceSyncResponse.deliveries:type_name -> saas.accounts.v1.SourceSyncDelivery + 39, // 47: saas.accounts.v1.GetSourceSyncResponse.progress:type_name -> saas.accounts.v1.SourceSyncProgress + 88, // 48: saas.accounts.v1.BeginGitHubAppSetupResponse.expires_at:type_name -> google.protobuf.Timestamp + 43, // 49: saas.accounts.v1.CompleteGitHubAppSetupResponse.repositories:type_name -> saas.accounts.v1.GitHubAppRepository + 19, // 50: saas.accounts.v1.MigrateGitHubSourceToAppResponse.datasource:type_name -> saas.accounts.v1.Datasource + 88, // 51: saas.accounts.v1.DatasourceAccountLink.created_at:type_name -> google.protobuf.Timestamp + 88, // 52: saas.accounts.v1.DatasourceGroupBinding.created_at:type_name -> google.protobuf.Timestamp + 10, // 53: saas.accounts.v1.DatasourceVerifiedDomain.status:type_name -> saas.accounts.v1.DatasourceDomainStatus + 88, // 54: saas.accounts.v1.DatasourceVerifiedDomain.verified_at:type_name -> google.protobuf.Timestamp + 88, // 55: saas.accounts.v1.DatasourceVerifiedDomain.created_at:type_name -> google.protobuf.Timestamp + 88, // 56: saas.accounts.v1.BeginDatasourceAccountLinkResponse.expires_at:type_name -> google.protobuf.Timestamp + 50, // 57: saas.accounts.v1.CompleteDatasourceAccountLinkResponse.link:type_name -> saas.accounts.v1.DatasourceAccountLink + 50, // 58: saas.accounts.v1.ListMyDatasourceAccountLinksResponse.links:type_name -> saas.accounts.v1.DatasourceAccountLink + 50, // 59: saas.accounts.v1.GetDatasourceDirectoryResponse.links:type_name -> saas.accounts.v1.DatasourceAccountLink + 51, // 60: saas.accounts.v1.GetDatasourceDirectoryResponse.bindings:type_name -> saas.accounts.v1.DatasourceGroupBinding + 52, // 61: saas.accounts.v1.GetDatasourceDirectoryResponse.domains:type_name -> saas.accounts.v1.DatasourceVerifiedDomain + 53, // 62: saas.accounts.v1.GetDatasourceDirectoryResponse.teams:type_name -> saas.accounts.v1.DatasourceDirectoryTeam + 51, // 63: saas.accounts.v1.BindDatasourceGroupResponse.binding:type_name -> saas.accounts.v1.DatasourceGroupBinding + 52, // 64: saas.accounts.v1.ClaimDatasourceDomainResponse.domain:type_name -> saas.accounts.v1.DatasourceVerifiedDomain + 52, // 65: saas.accounts.v1.VerifyDatasourceDomainResponse.domain:type_name -> saas.accounts.v1.DatasourceVerifiedDomain + 88, // 66: saas.accounts.v1.SourceDelegation.created_at:type_name -> google.protobuf.Timestamp + 88, // 67: saas.accounts.v1.SourceDelegation.revoked_at:type_name -> google.protobuf.Timestamp + 11, // 68: saas.accounts.v1.SourceDelegation.revocation:type_name -> saas.accounts.v1.SourceDelegationRevocation + 74, // 69: saas.accounts.v1.ListSourceDelegationsResponse.delegations:type_name -> saas.accounts.v1.SourceDelegation + 74, // 70: saas.accounts.v1.RevokeSourceDelegationResponse.delegation:type_name -> saas.accounts.v1.SourceDelegation + 92, // 71: saas.accounts.v1.SourceOperation.input_schema:type_name -> google.protobuf.Struct + 92, // 72: saas.accounts.v1.SourceOperation.output_schema:type_name -> google.protobuf.Struct + 13, // 73: saas.accounts.v1.SourceOperation.effect:type_name -> saas.accounts.v1.SourceOperation.Effect + 79, // 74: saas.accounts.v1.DeclareSourceOperationsRequest.operations:type_name -> saas.accounts.v1.SourceOperation + 79, // 75: saas.accounts.v1.DeclareSourceOperationsResponse.operations:type_name -> saas.accounts.v1.SourceOperation + 79, // 76: saas.accounts.v1.ListSourceOperationsResponse.operations:type_name -> saas.accounts.v1.SourceOperation + 85, // 77: saas.accounts.v1.InvokeSourceOperationResponse.receipt:type_name -> saas.accounts.v1.SourceOperationReceipt + 84, // 78: saas.accounts.v1.DatasourceService.InvokeSourceOperation:input_type -> saas.accounts.v1.InvokeSourceOperationRequest + 87, // 79: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:input_type -> saas.accounts.v1.LookupInvokeSourceOperationRequest + 82, // 80: saas.accounts.v1.DatasourceService.ListSourceOperations:input_type -> saas.accounts.v1.ListSourceOperationsRequest + 80, // 81: saas.accounts.v1.DatasourceService.DeclareSourceOperations:input_type -> saas.accounts.v1.DeclareSourceOperationsRequest + 20, // 82: saas.accounts.v1.DatasourceService.AddGitHubSource:input_type -> saas.accounts.v1.AddGitHubSourceRequest + 22, // 83: saas.accounts.v1.DatasourceService.AddSource:input_type -> saas.accounts.v1.AddSourceRequest + 27, // 84: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:input_type -> saas.accounts.v1.GetDatasourceCatalogRequest + 29, // 85: saas.accounts.v1.DatasourceService.ListSources:input_type -> saas.accounts.v1.ListSourcesRequest + 31, // 86: saas.accounts.v1.DatasourceService.GetSource:input_type -> saas.accounts.v1.GetSourceRequest + 33, // 87: saas.accounts.v1.DatasourceService.SyncSource:input_type -> saas.accounts.v1.SyncSourceRequest + 35, // 88: saas.accounts.v1.DatasourceService.GetSourceSync:input_type -> saas.accounts.v1.GetSourceSyncRequest + 41, // 89: saas.accounts.v1.DatasourceService.DeleteSource:input_type -> saas.accounts.v1.DeleteSourceRequest + 44, // 90: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:input_type -> saas.accounts.v1.BeginGitHubAppSetupRequest + 46, // 91: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:input_type -> saas.accounts.v1.CompleteGitHubAppSetupRequest + 48, // 92: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:input_type -> saas.accounts.v1.MigrateGitHubSourceToAppRequest + 54, // 93: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:input_type -> saas.accounts.v1.BeginDatasourceAccountLinkRequest + 56, // 94: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:input_type -> saas.accounts.v1.CompleteDatasourceAccountLinkRequest + 58, // 95: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:input_type -> saas.accounts.v1.ListMyDatasourceAccountLinksRequest + 60, // 96: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:input_type -> saas.accounts.v1.DeleteDatasourceAccountLinkRequest + 62, // 97: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:input_type -> saas.accounts.v1.GetDatasourceDirectoryRequest + 64, // 98: saas.accounts.v1.DatasourceService.BindDatasourceGroup:input_type -> saas.accounts.v1.BindDatasourceGroupRequest + 66, // 99: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:input_type -> saas.accounts.v1.UnbindDatasourceGroupRequest + 68, // 100: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:input_type -> saas.accounts.v1.ClaimDatasourceDomainRequest + 70, // 101: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:input_type -> saas.accounts.v1.VerifyDatasourceDomainRequest + 72, // 102: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:input_type -> saas.accounts.v1.DeleteDatasourceDomainRequest + 75, // 103: saas.accounts.v1.DatasourceService.ListSourceDelegations:input_type -> saas.accounts.v1.ListSourceDelegationsRequest + 77, // 104: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:input_type -> saas.accounts.v1.RevokeSourceDelegationRequest + 86, // 105: saas.accounts.v1.DatasourceService.InvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse + 86, // 106: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse + 83, // 107: saas.accounts.v1.DatasourceService.ListSourceOperations:output_type -> saas.accounts.v1.ListSourceOperationsResponse + 81, // 108: saas.accounts.v1.DatasourceService.DeclareSourceOperations:output_type -> saas.accounts.v1.DeclareSourceOperationsResponse + 21, // 109: saas.accounts.v1.DatasourceService.AddGitHubSource:output_type -> saas.accounts.v1.AddGitHubSourceResponse + 23, // 110: saas.accounts.v1.DatasourceService.AddSource:output_type -> saas.accounts.v1.AddSourceResponse + 28, // 111: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:output_type -> saas.accounts.v1.GetDatasourceCatalogResponse + 30, // 112: saas.accounts.v1.DatasourceService.ListSources:output_type -> saas.accounts.v1.ListSourcesResponse + 32, // 113: saas.accounts.v1.DatasourceService.GetSource:output_type -> saas.accounts.v1.GetSourceResponse + 34, // 114: saas.accounts.v1.DatasourceService.SyncSource:output_type -> saas.accounts.v1.SyncSourceResponse + 40, // 115: saas.accounts.v1.DatasourceService.GetSourceSync:output_type -> saas.accounts.v1.GetSourceSyncResponse + 42, // 116: saas.accounts.v1.DatasourceService.DeleteSource:output_type -> saas.accounts.v1.DeleteSourceResponse + 45, // 117: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:output_type -> saas.accounts.v1.BeginGitHubAppSetupResponse + 47, // 118: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:output_type -> saas.accounts.v1.CompleteGitHubAppSetupResponse + 49, // 119: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:output_type -> saas.accounts.v1.MigrateGitHubSourceToAppResponse + 55, // 120: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:output_type -> saas.accounts.v1.BeginDatasourceAccountLinkResponse + 57, // 121: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:output_type -> saas.accounts.v1.CompleteDatasourceAccountLinkResponse + 59, // 122: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:output_type -> saas.accounts.v1.ListMyDatasourceAccountLinksResponse + 61, // 123: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:output_type -> saas.accounts.v1.DeleteDatasourceAccountLinkResponse + 63, // 124: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:output_type -> saas.accounts.v1.GetDatasourceDirectoryResponse + 65, // 125: saas.accounts.v1.DatasourceService.BindDatasourceGroup:output_type -> saas.accounts.v1.BindDatasourceGroupResponse + 67, // 126: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:output_type -> saas.accounts.v1.UnbindDatasourceGroupResponse + 69, // 127: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:output_type -> saas.accounts.v1.ClaimDatasourceDomainResponse + 71, // 128: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:output_type -> saas.accounts.v1.VerifyDatasourceDomainResponse + 73, // 129: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:output_type -> saas.accounts.v1.DeleteDatasourceDomainResponse + 76, // 130: saas.accounts.v1.DatasourceService.ListSourceDelegations:output_type -> saas.accounts.v1.ListSourceDelegationsResponse + 78, // 131: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:output_type -> saas.accounts.v1.RevokeSourceDelegationResponse + 105, // [105:132] is the sub-list for method output_type + 78, // [78:105] is the sub-list for method input_type + 78, // [78:78] is the sub-list for extension type_name + 78, // [78:78] is the sub-list for extension extendee + 0, // [0:78] is the sub-list for field type_name } func init() { file_saas_accounts_v1_datasource_proto_init() } @@ -6059,8 +6900,8 @@ func file_saas_accounts_v1_datasource_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_datasource_proto_rawDesc), len(file_saas_accounts_v1_datasource_proto_rawDesc)), - NumEnums: 12, - NumMessages: 65, + NumEnums: 14, + NumMessages: 74, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/saas/accounts/v1/installations.pb.go b/gen/saas/accounts/v1/installations.pb.go index b8c34a0..d68dad8 100644 --- a/gen/saas/accounts/v1/installations.pb.go +++ b/gen/saas/accounts/v1/installations.pb.go @@ -149,15 +149,34 @@ type Installation struct { Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` OrgId string `protobuf:"bytes,2,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` AgentPrincipalId string `protobuf:"bytes,3,opt,name=agent_principal_id,json=agentPrincipalId,proto3" json:"agent_principal_id,omitempty"` - SolutionIdentifier string `protobuf:"bytes,4,opt,name=solution_identifier,json=solutionIdentifier,proto3" json:"solution_identifier,omitempty"` OwnerPrincipalId string `protobuf:"bytes,5,opt,name=owner_principal_id,json=ownerPrincipalId,proto3" json:"owner_principal_id,omitempty"` CoOwnerPrincipalIds []string `protobuf:"bytes,6,rep,name=co_owner_principal_ids,json=coOwnerPrincipalIds,proto3" json:"co_owner_principal_ids,omitempty"` RootScopeNodeId string `protobuf:"bytes,7,opt,name=root_scope_node_id,json=rootScopeNodeId,proto3" json:"root_scope_node_id,omitempty"` Status InstallationStatus `protobuf:"varint,8,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` RevokedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=revoked_at,json=revokedAt,proto3,oneof" json:"revoked_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The immutable solution target this installation was consented to: one + // continuous period of one binding's presence on this host. It is never + // reused, so a replacement binding — even one claiming the same route alias — + // is a different target and inherits nothing. + // + // Deliberately NOT accompanied by the route alias. The alias is a property of + // the target that a later generation may move, and resolving it here would + // mean joining the host's presence state into a request-path listing — which + // would require granting request traffic read over every organisation's + // presence to answer about one. A consumer that needs the alias asks + // ListAvailableSolutions, which serves accepted applied state, and joins on + // this id. + // + // Empty only on a revoked historical row whose target could not be + // established at the cutover; an active installation always names one. + TargetId string `protobuf:"bytes,11,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + // Why a revoked installation was revoked, when it was not an administrator + // uninstalling: the installation-identity cutover, or a tenancy narrowing that + // withdrew this organisation. Empty on an active row. + RevokedReason string `protobuf:"bytes,12,opt,name=revoked_reason,json=revokedReason,proto3" json:"revoked_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Installation) Reset() { @@ -211,13 +230,6 @@ func (x *Installation) GetAgentPrincipalId() string { return "" } -func (x *Installation) GetSolutionIdentifier() string { - if x != nil { - return x.SolutionIdentifier - } - return "" -} - func (x *Installation) GetOwnerPrincipalId() string { if x != nil { return x.OwnerPrincipalId @@ -260,13 +272,26 @@ func (x *Installation) GetRevokedAt() *timestamppb.Timestamp { return nil } +func (x *Installation) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *Installation) GetRevokedReason() string { + if x != nil { + return x.RevokedReason + } + return "" +} + type InstallSolutionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` // Canonical "publisher/name:version" of the solution's agent. The agent // Principal is created idempotently per version. - AgentIdentifier string `protobuf:"bytes,2,opt,name=agent_identifier,json=agentIdentifier,proto3" json:"agent_identifier,omitempty"` - SolutionIdentifier string `protobuf:"bytes,3,opt,name=solution_identifier,json=solutionIdentifier,proto3" json:"solution_identifier,omitempty"` + AgentIdentifier string `protobuf:"bytes,2,opt,name=agent_identifier,json=agentIdentifier,proto3" json:"agent_identifier,omitempty"` // Human display name for the agent principal. Empty defaults to the identifier. DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` // Human-readable display label for the solution's authority-root scope node @@ -287,8 +312,16 @@ type InstallSolutionRequest struct { // Empty = unrestricted. AllowedAudiences []string `protobuf:"bytes,10,rep,name=allowed_audiences,json=allowedAudiences,proto3" json:"allowed_audiences,omitempty"` AllowedScopes []string `protobuf:"bytes,11,rep,name=allowed_scopes,json=allowedScopes,proto3" json:"allowed_scopes,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The solution target being installed, from ListAvailableSolutions. Required. + // + // An installer chooses an identity, never an address. The target must be LIVE + // and ACCEPTED when the install lands: installing a withdrawn target would + // record consent to a presence that already ended, and installing a target + // whose newest generation was refused would record consent to a release this + // host never admitted. + TargetId string `protobuf:"bytes,12,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *InstallSolutionRequest) Reset() { @@ -335,13 +368,6 @@ func (x *InstallSolutionRequest) GetAgentIdentifier() string { return "" } -func (x *InstallSolutionRequest) GetSolutionIdentifier() string { - if x != nil { - return x.SolutionIdentifier - } - return "" -} - func (x *InstallSolutionRequest) GetDisplayName() string { if x != nil { return x.DisplayName @@ -391,6 +417,13 @@ func (x *InstallSolutionRequest) GetAllowedScopes() []string { return nil } +func (x *InstallSolutionRequest) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + type UninstallSolutionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -618,16 +651,440 @@ func (x *GetInstallationResponse) GetHealth() InstallationHealth { return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED } +// ListInstallationsRequest asks what one organization has installed. Every other +// RPC on this service names a single installation, so until this existed nothing +// could answer "what has this org installed?" — which is the authority read a +// per-organization solution projection is built on (issue #949). +type ListInstallationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // Restrict to one lifecycle status. UNSPECIFIED returns every status, so a + // caller that must see only live installs asks for ACTIVE explicitly rather + // than leaning on a default that would silently widen if the enum grew. + Status InstallationStatus `protobuf:"varint,2,opt,name=status,proto3,enum=saas.accounts.v1.InstallationStatus" json:"status,omitempty"` + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque cursor from a previous response's next_page_token; empty for page one. + PageToken string `protobuf:"bytes,4,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInstallationsRequest) Reset() { + *x = ListInstallationsRequest{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInstallationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInstallationsRequest) ProtoMessage() {} + +func (x *ListInstallationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInstallationsRequest.ProtoReflect.Descriptor instead. +func (*ListInstallationsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{6} +} + +func (x *ListInstallationsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *ListInstallationsRequest) GetStatus() InstallationStatus { + if x != nil { + return x.Status + } + return InstallationStatus_INSTALLATION_STATUS_UNSPECIFIED +} + +func (x *ListInstallationsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListInstallationsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// InstallationSummary is one row of the listing: the installation, plus the health +// resolved live beside it exactly as GetInstallation resolves it for one. Health +// travels with the row because a consumer that renders an installed solution as +// unavailable would otherwise need one GetInstallation per installation. +type InstallationSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` + Health InstallationHealth `protobuf:"varint,2,opt,name=health,proto3,enum=saas.accounts.v1.InstallationHealth" json:"health,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InstallationSummary) Reset() { + *x = InstallationSummary{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InstallationSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstallationSummary) ProtoMessage() {} + +func (x *InstallationSummary) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstallationSummary.ProtoReflect.Descriptor instead. +func (*InstallationSummary) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{7} +} + +func (x *InstallationSummary) GetInstallation() *Installation { + if x != nil { + return x.Installation + } + return nil +} + +func (x *InstallationSummary) GetHealth() InstallationHealth { + if x != nil { + return x.Health + } + return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED +} + +type ListInstallationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installations []*InstallationSummary `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` + // Set when more installations remain; pass it back as page_token. Empty on the + // last page. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInstallationsResponse) Reset() { + *x = ListInstallationsResponse{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInstallationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInstallationsResponse) ProtoMessage() {} + +func (x *ListInstallationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInstallationsResponse.ProtoReflect.Descriptor instead. +func (*ListInstallationsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{8} +} + +func (x *ListInstallationsResponse) GetInstallations() []*InstallationSummary { + if x != nil { + return x.Installations + } + return nil +} + +func (x *ListInstallationsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// AvailableSolution is one installable solution target: its immutable identity, +// the release the host currently has applied for it, and where it is routed. +type AvailableSolution struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The immutable target identity an install names. Never reused. + TargetId string `protobuf:"bytes,1,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + // The binding whose presence this target records. Carried so an operator can + // correlate a catalogue row with delivery; an installation does not join on it, + // because one binding may have several targets over time. + BindingId string `protobuf:"bytes,2,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"` + // The route alias the applied generation claims — `/solutions//*`. A + // later generation may move it, so it is never an identity. + RouteAlias string `protobuf:"bytes,3,opt,name=route_alias,json=routeAlias,proto3" json:"route_alias,omitempty"` + // The applied release: publisher, name, version. + ReleasePublisher string `protobuf:"bytes,4,opt,name=release_publisher,json=releasePublisher,proto3" json:"release_publisher,omitempty"` + ReleaseName string `protobuf:"bytes,5,opt,name=release_name,json=releaseName,proto3" json:"release_name,omitempty"` + ReleaseVersion string `protobuf:"bytes,6,opt,name=release_version,json=releaseVersion,proto3" json:"release_version,omitempty"` + // The generation that opened this target, and the newest applied generation. + // Equal until a later generation is applied for the same presence. + OpenedGeneration uint64 `protobuf:"varint,7,opt,name=opened_generation,json=openedGeneration,proto3" json:"opened_generation,omitempty"` + AppliedGeneration uint64 `protobuf:"varint,8,opt,name=applied_generation,json=appliedGeneration,proto3" json:"applied_generation,omitempty"` + // True when this organization already has an ACTIVE installation of this + // target. A catalogue shows it as installed rather than offering it twice; the + // unique index over (org_id, target_id) would refuse the second install anyway, + // and an administrator should not have to discover that by being refused. + Installed bool `protobuf:"varint,9,opt,name=installed,proto3" json:"installed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AvailableSolution) Reset() { + *x = AvailableSolution{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AvailableSolution) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AvailableSolution) ProtoMessage() {} + +func (x *AvailableSolution) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AvailableSolution.ProtoReflect.Descriptor instead. +func (*AvailableSolution) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{9} +} + +func (x *AvailableSolution) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *AvailableSolution) GetBindingId() string { + if x != nil { + return x.BindingId + } + return "" +} + +func (x *AvailableSolution) GetRouteAlias() string { + if x != nil { + return x.RouteAlias + } + return "" +} + +func (x *AvailableSolution) GetReleasePublisher() string { + if x != nil { + return x.ReleasePublisher + } + return "" +} + +func (x *AvailableSolution) GetReleaseName() string { + if x != nil { + return x.ReleaseName + } + return "" +} + +func (x *AvailableSolution) GetReleaseVersion() string { + if x != nil { + return x.ReleaseVersion + } + return "" +} + +func (x *AvailableSolution) GetOpenedGeneration() uint64 { + if x != nil { + return x.OpenedGeneration + } + return 0 +} + +func (x *AvailableSolution) GetAppliedGeneration() uint64 { + if x != nil { + return x.AppliedGeneration + } + return 0 +} + +func (x *AvailableSolution) GetInstalled() bool { + if x != nil { + return x.Installed + } + return false +} + +type ListAvailableSolutionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // Page bound. 0 -> server default; capped for the same reason the installation + // listing is capped. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAvailableSolutionsRequest) Reset() { + *x = ListAvailableSolutionsRequest{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAvailableSolutionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAvailableSolutionsRequest) ProtoMessage() {} + +func (x *ListAvailableSolutionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAvailableSolutionsRequest.ProtoReflect.Descriptor instead. +func (*ListAvailableSolutionsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{10} +} + +func (x *ListAvailableSolutionsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *ListAvailableSolutionsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListAvailableSolutionsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type ListAvailableSolutionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Solutions []*AvailableSolution `protobuf:"bytes,1,rep,name=solutions,proto3" json:"solutions,omitempty"` + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAvailableSolutionsResponse) Reset() { + *x = ListAvailableSolutionsResponse{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAvailableSolutionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAvailableSolutionsResponse) ProtoMessage() {} + +func (x *ListAvailableSolutionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAvailableSolutionsResponse.ProtoReflect.Descriptor instead. +func (*ListAvailableSolutionsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{11} +} + +func (x *ListAvailableSolutionsResponse) GetSolutions() []*AvailableSolution { + if x != nil { + return x.Solutions + } + return nil +} + +func (x *ListAvailableSolutionsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + var File_saas_accounts_v1_installations_proto protoreflect.FileDescriptor const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\n" + - "$saas/accounts/v1/installations.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1csaas/policy/v1/options.proto\"\x9e\x04\n" + + "$saas/accounts/v1/installations.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1csaas/policy/v1/options.proto\"\xe3\x04\n" + "\fInstallation\x12\x18\n" + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + - "\x12agent_principal_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10agentPrincipalId\x12/\n" + - "\x13solution_identifier\x18\x04 \x01(\tR\x12solutionIdentifier\x126\n" + + "\x12agent_principal_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10agentPrincipalId\x126\n" + "\x12owner_principal_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x10ownerPrincipalId\x123\n" + "\x16co_owner_principal_ids\x18\x06 \x03(\tR\x13coOwnerPrincipalIds\x125\n" + "\x12root_scope_node_id\x18\a \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0frootScopeNodeId\x12<\n" + @@ -636,14 +1093,14 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12>\n" + "\n" + "revoked_at\x18\n" + - " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01B\r\n" + - "\v_revoked_at\"\xbf\x04\n" + + " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + + "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + + "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReasonB\r\n" + + "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xc4\x04\n" + "\x16InstallSolutionRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x125\n" + "\x10agent_identifier\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x0fagentIdentifier\x12;\n" + - "\x13solution_identifier\x18\x03 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x12solutionIdentifier\x12+\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x0fagentIdentifier\x12+\n" + "\fdisplay_name\x18\x04 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\vdisplayName\x122\n" + "\x10root_scope_label\x18\x06 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\x0erootScopeLabel\x12!\n" + "\arole_id\x18\a \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06roleId\x129\n" + @@ -651,7 +1108,8 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x16co_owner_principal_ids\x18\t \x03(\tB\x0f\xbaH\f\x92\x01\t\x10 \"\x05r\x03\xb0\x01\x01R\x13coOwnerPrincipalIds\x12<\n" + "\x11allowed_audiences\x18\n" + " \x03(\tB\x0f\xbaH\f\x92\x01\t\x10@\"\x05r\x03\x18\x80\x04R\x10allowedAudiences\x126\n" + - "\x0eallowed_scopes\x18\v \x03(\tB\x0f\xbaH\f\x92\x01\t\x10@\"\x05r\x03\x18\x80\x01R\rallowedScopesJ\x04\b\x05\x10\x06R\x0froot_scope_path\"n\n" + + "\x0eallowed_scopes\x18\v \x03(\tB\x0f\xbaH\f\x92\x01\t\x10@\"\x05r\x03\x18\x80\x01R\rallowedScopes\x12%\n" + + "\ttarget_id\x18\f \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetIdJ\x04\b\x03\x10\x04J\x04\b\x05\x10\x06R\x13solution_identifierR\x0froot_scope_path\"n\n" + "\x18UninstallSolutionRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x121\n" + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\"\xff\x01\n" + @@ -665,7 +1123,41 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\"\x9b\x01\n" + "\x17GetInstallationResponse\x12B\n" + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + - "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health*z\n" + + "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\xc1\x01\n" + + "\x18ListInstallationsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12<\n" + + "\x06status\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationStatusR\x06status\x12'\n" + + "\tpage_size\x18\x03 \x01(\x05B\n" + + "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x04 \x01(\tR\tpageToken\"\x97\x01\n" + + "\x13InstallationSummary\x12B\n" + + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + + "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x90\x01\n" + + "\x19ListInstallationsResponse\x12K\n" + + "\rinstallations\x18\x01 \x03(\v2%.saas.accounts.v1.InstallationSummaryR\rinstallations\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xed\x02\n" + + "\x11AvailableSolution\x12%\n" + + "\ttarget_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetId\x12\x1d\n" + + "\n" + + "binding_id\x18\x02 \x01(\tR\tbindingId\x12\x1f\n" + + "\vroute_alias\x18\x03 \x01(\tR\n" + + "routeAlias\x12+\n" + + "\x11release_publisher\x18\x04 \x01(\tR\x10releasePublisher\x12!\n" + + "\frelease_name\x18\x05 \x01(\tR\vreleaseName\x12'\n" + + "\x0frelease_version\x18\x06 \x01(\tR\x0ereleaseVersion\x12+\n" + + "\x11opened_generation\x18\a \x01(\x04R\x10openedGeneration\x12-\n" + + "\x12applied_generation\x18\b \x01(\x04R\x11appliedGeneration\x12\x1c\n" + + "\tinstalled\x18\t \x01(\bR\tinstalled\"\x88\x01\n" + + "\x1dListAvailableSolutionsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12'\n" + + "\tpage_size\x18\x02 \x01(\x05B\n" + + "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\"\x8b\x01\n" + + "\x1eListAvailableSolutionsResponse\x12A\n" + + "\tsolutions\x18\x01 \x03(\v2#.saas.accounts.v1.AvailableSolutionR\tsolutions\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*z\n" + "\x12InstallationStatus\x12#\n" + "\x1fINSTALLATION_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + "\x1aINSTALLATION_STATUS_ACTIVE\x10\x01\x12\x1f\n" + @@ -676,7 +1168,7 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "%INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER\x10\x02\x12%\n" + "!INSTALLATION_HEALTH_AGENT_REVOKED\x10\x03\x12&\n" + "\"INSTALLATION_HEALTH_AGENT_DISABLED\x10\x04\x12.\n" + - "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x052\xef\x06\n" + + "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x052\xc5\t\n" + "\x13InstallationService\x12\xba\x01\n" + "\x0fInstallSolution\x12(.saas.accounts.v1.InstallSolutionRequest\x1a\x1e.saas.accounts.v1.Installation\"]\xc2\xf3\x18=\b\x02\x10\x04*\f\n" + "\x06org_id\x10\x02\x18\x010\x01:\x1d\n" + @@ -688,7 +1180,10 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x06org_id\x10\x02\x18\x010\x01:+\n" + "'saas.installation.ownership_transferred\x10\x02@\x01H\x04P\x03X\x03`\x01\x82\xd3\xe4\x93\x02::\x01*\"5/v1/installations/{installation_id}:transferOwnership\x12\xb9\x01\n" + "\x0fGetInstallation\x12(.saas.accounts.v1.GetInstallationRequest\x1a).saas.accounts.v1.GetInstallationResponse\"Q\xc2\xf3\x18\"\b\x02\x10\x03*\f\n" + - "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02%\x12#/v1/installations/{installation_id}B\xd0\x01\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02%\x12#/v1/installations/{installation_id}\x12\x86\x01\n" + + "\x11ListInstallations\x12*.saas.accounts.v1.ListInstallationsRequest\x1a+.saas.accounts.v1.ListInstallationsResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\xca\x01\n" + + "\x16ListAvailableSolutions\x12/.saas.accounts.v1.ListAvailableSolutionsRequest\x1a0.saas.accounts.v1.ListAvailableSolutionsResponse\"M\xc2\xf3\x18\"\b\x02\x10\x04*\f\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02!\x12\x1f/v1/installations:listAvailableB\xd0\x01\n" + "\x14com.saas.accounts.v1B\x12InstallationsProtoP\x01ZBgithub.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" var ( @@ -704,7 +1199,7 @@ func file_saas_accounts_v1_installations_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_installations_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_saas_accounts_v1_installations_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_saas_accounts_v1_installations_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_saas_accounts_v1_installations_proto_goTypes = []any{ (InstallationStatus)(0), // 0: saas.accounts.v1.InstallationStatus (InstallationHealth)(0), // 1: saas.accounts.v1.InstallationHealth @@ -714,28 +1209,43 @@ var file_saas_accounts_v1_installations_proto_goTypes = []any{ (*TransferInstallationOwnershipRequest)(nil), // 5: saas.accounts.v1.TransferInstallationOwnershipRequest (*GetInstallationRequest)(nil), // 6: saas.accounts.v1.GetInstallationRequest (*GetInstallationResponse)(nil), // 7: saas.accounts.v1.GetInstallationResponse - (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 9: google.protobuf.Empty + (*ListInstallationsRequest)(nil), // 8: saas.accounts.v1.ListInstallationsRequest + (*InstallationSummary)(nil), // 9: saas.accounts.v1.InstallationSummary + (*ListInstallationsResponse)(nil), // 10: saas.accounts.v1.ListInstallationsResponse + (*AvailableSolution)(nil), // 11: saas.accounts.v1.AvailableSolution + (*ListAvailableSolutionsRequest)(nil), // 12: saas.accounts.v1.ListAvailableSolutionsRequest + (*ListAvailableSolutionsResponse)(nil), // 13: saas.accounts.v1.ListAvailableSolutionsResponse + (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 15: google.protobuf.Empty } var file_saas_accounts_v1_installations_proto_depIdxs = []int32{ - 0, // 0: saas.accounts.v1.Installation.status:type_name -> saas.accounts.v1.InstallationStatus - 8, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp - 8, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp - 2, // 3: saas.accounts.v1.GetInstallationResponse.installation:type_name -> saas.accounts.v1.Installation - 1, // 4: saas.accounts.v1.GetInstallationResponse.health:type_name -> saas.accounts.v1.InstallationHealth - 3, // 5: saas.accounts.v1.InstallationService.InstallSolution:input_type -> saas.accounts.v1.InstallSolutionRequest - 4, // 6: saas.accounts.v1.InstallationService.UninstallSolution:input_type -> saas.accounts.v1.UninstallSolutionRequest - 5, // 7: saas.accounts.v1.InstallationService.TransferInstallationOwnership:input_type -> saas.accounts.v1.TransferInstallationOwnershipRequest - 6, // 8: saas.accounts.v1.InstallationService.GetInstallation:input_type -> saas.accounts.v1.GetInstallationRequest - 2, // 9: saas.accounts.v1.InstallationService.InstallSolution:output_type -> saas.accounts.v1.Installation - 9, // 10: saas.accounts.v1.InstallationService.UninstallSolution:output_type -> google.protobuf.Empty - 2, // 11: saas.accounts.v1.InstallationService.TransferInstallationOwnership:output_type -> saas.accounts.v1.Installation - 7, // 12: saas.accounts.v1.InstallationService.GetInstallation:output_type -> saas.accounts.v1.GetInstallationResponse - 9, // [9:13] is the sub-list for method output_type - 5, // [5:9] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 0, // 0: saas.accounts.v1.Installation.status:type_name -> saas.accounts.v1.InstallationStatus + 14, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp + 14, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp + 2, // 3: saas.accounts.v1.GetInstallationResponse.installation:type_name -> saas.accounts.v1.Installation + 1, // 4: saas.accounts.v1.GetInstallationResponse.health:type_name -> saas.accounts.v1.InstallationHealth + 0, // 5: saas.accounts.v1.ListInstallationsRequest.status:type_name -> saas.accounts.v1.InstallationStatus + 2, // 6: saas.accounts.v1.InstallationSummary.installation:type_name -> saas.accounts.v1.Installation + 1, // 7: saas.accounts.v1.InstallationSummary.health:type_name -> saas.accounts.v1.InstallationHealth + 9, // 8: saas.accounts.v1.ListInstallationsResponse.installations:type_name -> saas.accounts.v1.InstallationSummary + 11, // 9: saas.accounts.v1.ListAvailableSolutionsResponse.solutions:type_name -> saas.accounts.v1.AvailableSolution + 3, // 10: saas.accounts.v1.InstallationService.InstallSolution:input_type -> saas.accounts.v1.InstallSolutionRequest + 4, // 11: saas.accounts.v1.InstallationService.UninstallSolution:input_type -> saas.accounts.v1.UninstallSolutionRequest + 5, // 12: saas.accounts.v1.InstallationService.TransferInstallationOwnership:input_type -> saas.accounts.v1.TransferInstallationOwnershipRequest + 6, // 13: saas.accounts.v1.InstallationService.GetInstallation:input_type -> saas.accounts.v1.GetInstallationRequest + 8, // 14: saas.accounts.v1.InstallationService.ListInstallations:input_type -> saas.accounts.v1.ListInstallationsRequest + 12, // 15: saas.accounts.v1.InstallationService.ListAvailableSolutions:input_type -> saas.accounts.v1.ListAvailableSolutionsRequest + 2, // 16: saas.accounts.v1.InstallationService.InstallSolution:output_type -> saas.accounts.v1.Installation + 15, // 17: saas.accounts.v1.InstallationService.UninstallSolution:output_type -> google.protobuf.Empty + 2, // 18: saas.accounts.v1.InstallationService.TransferInstallationOwnership:output_type -> saas.accounts.v1.Installation + 7, // 19: saas.accounts.v1.InstallationService.GetInstallation:output_type -> saas.accounts.v1.GetInstallationResponse + 10, // 20: saas.accounts.v1.InstallationService.ListInstallations:output_type -> saas.accounts.v1.ListInstallationsResponse + 13, // 21: saas.accounts.v1.InstallationService.ListAvailableSolutions:output_type -> saas.accounts.v1.ListAvailableSolutionsResponse + 16, // [16:22] is the sub-list for method output_type + 10, // [10:16] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_saas_accounts_v1_installations_proto_init() } @@ -750,7 +1260,7 @@ func file_saas_accounts_v1_installations_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installations_proto_rawDesc), len(file_saas_accounts_v1_installations_proto_rawDesc)), NumEnums: 2, - NumMessages: 6, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/saas/accounts/v1/module_capabilities.pb.go b/gen/saas/accounts/v1/module_capabilities.pb.go index 729d39f..4a6eefe 100644 --- a/gen/saas/accounts/v1/module_capabilities.pb.go +++ b/gen/saas/accounts/v1/module_capabilities.pb.go @@ -4574,12 +4574,17 @@ func (x *ModuleCurrentInstallationRequest) GetInstallationId() string { } type ModuleCurrentInstallationResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - InstallationId string `protobuf:"bytes,1,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` - TenantId string `protobuf:"bytes,2,opt,name=tenant_id,json=tenantId,proto3" json:"tenant_id,omitempty"` - SolutionIdentifier string `protobuf:"bytes,3,opt,name=solution_identifier,json=solutionIdentifier,proto3" json:"solution_identifier,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + InstallationId string `protobuf:"bytes,1,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + TenantId string `protobuf:"bytes,2,opt,name=tenant_id,json=tenantId,proto3" json:"tenant_id,omitempty"` + // The immutable target this installation was consented to: one continuous + // period of one binding's presence, never reused. + TargetId string `protobuf:"bytes,4,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + // The delivered presence binding whose period that target records. This is + // what an authority decision keys on; the alias is display only. + BindingId string `protobuf:"bytes,5,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ModuleCurrentInstallationResponse) Reset() { @@ -4626,9 +4631,16 @@ func (x *ModuleCurrentInstallationResponse) GetTenantId() string { return "" } -func (x *ModuleCurrentInstallationResponse) GetSolutionIdentifier() string { +func (x *ModuleCurrentInstallationResponse) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +func (x *ModuleCurrentInstallationResponse) GetBindingId() string { if x != nil { - return x.SolutionIdentifier + return x.BindingId } return "" } @@ -4973,11 +4985,13 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "\x11expected_revision\x18\x04 \x01(\x03R\x10expectedRevision\"\x9d\x01\n" + " ModuleCurrentInstallationRequest\x12F\n" + "\x19parent_work_context_token\x18\x01 \x01(\tB\v\xbaH\br\x06\x10\x01\x18\x80\x80\x02R\x16parentWorkContextToken\x121\n" + - "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\"\x9a\x01\n" + + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\"\xca\x01\n" + "!ModuleCurrentInstallationResponse\x12'\n" + "\x0finstallation_id\x18\x01 \x01(\tR\x0einstallationId\x12\x1b\n" + - "\ttenant_id\x18\x02 \x01(\tR\btenantId\x12/\n" + - "\x13solution_identifier\x18\x03 \x01(\tR\x12solutionIdentifier*\xb8\x02\n" + + "\ttenant_id\x18\x02 \x01(\tR\btenantId\x12%\n" + + "\ttarget_id\x18\x04 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetId\x12\x1d\n" + + "\n" + + "binding_id\x18\x05 \x01(\tR\tbindingIdJ\x04\b\x03\x10\x04R\x13solution_identifier*\xb8\x02\n" + "\x14ModuleAuditFieldKind\x12'\n" + "#MODULE_AUDIT_FIELD_KIND_UNSPECIFIED\x10\x00\x12\"\n" + "\x1eMODULE_AUDIT_FIELD_KIND_STRING\x10\x01\x12 \n" + diff --git a/gen/saas/accounts/v1/solution_entitlements.pb.go b/gen/saas/accounts/v1/solution_entitlements.pb.go new file mode 100644 index 0000000..5ba0a06 --- /dev/null +++ b/gen/saas/accounts/v1/solution_entitlements.pb.go @@ -0,0 +1,321 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: saas/accounts/v1/solution_entitlements.proto + +package accountsv1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// SolutionEntitlement is one installed solution a subject may use. It is +// deliberately a projection of the installation, not the installation: a +// consumer of this message renders a menu, and the owner of record, the +// succession set and the agent principal are no part of that. +type SolutionEntitlement struct { + state protoimpl.MessageState `protogen:"open.v1"` + InstallationId string `protobuf:"bytes,2,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + // The installation's authority-root scope node — the node the viewer's grant + // had to reach for this entitlement to exist. Carried so a consumer can say + // which boundary admitted a solution without a second lookup. + RootScopeNodeId string `protobuf:"bytes,3,opt,name=root_scope_node_id,json=rootScopeNodeId,proto3" json:"root_scope_node_id,omitempty"` + // Whether the installation is healthy right now, resolved live from the same + // authority facts GetInstallation reports. + // + // A bool rather than a copy of InstallationHealth on purpose. A consumer of + // this message shows an unhealthy solution as unavailable and never renders + // the reason — an operator reads that from GetInstallation — so restating that + // enum here would be a second copy of a value set to keep in lockstep. This + // reduces it instead, by the one rule that survives the enum growing: anything + // that is not HEALTHY is not healthy. + Healthy bool `protobuf:"varint,4,opt,name=healthy,proto3" json:"healthy,omitempty"` + // The immutable solution target this entitlement is for — the key a consumer + // joins against its own registered set. + // + // A consumer resolves a route alias to a target through the host's applied + // presence state and then compares THIS id. That is what makes the alias-reuse + // transfer inexpressible: a replacement binding claiming a withdrawn alias + // resolves to its own target, which no installation of the predecessor names. + // + // The alias is deliberately absent. Carrying it here would let a consumer go + // back to joining on it, and it would be a copy of a value the presence + // snapshot already owns and may move. + TargetId string `protobuf:"bytes,5,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SolutionEntitlement) Reset() { + *x = SolutionEntitlement{} + mi := &file_saas_accounts_v1_solution_entitlements_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SolutionEntitlement) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SolutionEntitlement) ProtoMessage() {} + +func (x *SolutionEntitlement) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_solution_entitlements_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SolutionEntitlement.ProtoReflect.Descriptor instead. +func (*SolutionEntitlement) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_solution_entitlements_proto_rawDescGZIP(), []int{0} +} + +func (x *SolutionEntitlement) GetInstallationId() string { + if x != nil { + return x.InstallationId + } + return "" +} + +func (x *SolutionEntitlement) GetRootScopeNodeId() string { + if x != nil { + return x.RootScopeNodeId + } + return "" +} + +func (x *SolutionEntitlement) GetHealthy() bool { + if x != nil { + return x.Healthy + } + return false +} + +func (x *SolutionEntitlement) GetTargetId() string { + if x != nil { + return x.TargetId + } + return "" +} + +type ListSolutionEntitlementsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // The subject whose entitlements to enumerate. A principal: a team's + // entitlements reach a member through the team grant, which the underlying + // union already resolves, so asking about a principal answers "everything this + // person may use" including what a team confers. + SubjectId string `protobuf:"bytes,2,opt,name=subject_id,json=subjectId,proto3" json:"subject_id,omitempty"` + // Page bound. 0 -> server default; capped so an organization with many + // installations can never return an unbounded result in one call. + PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque cursor from a previous response's next_page_token; empty for page one. + PageToken string `protobuf:"bytes,4,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSolutionEntitlementsRequest) Reset() { + *x = ListSolutionEntitlementsRequest{} + mi := &file_saas_accounts_v1_solution_entitlements_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSolutionEntitlementsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSolutionEntitlementsRequest) ProtoMessage() {} + +func (x *ListSolutionEntitlementsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_solution_entitlements_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSolutionEntitlementsRequest.ProtoReflect.Descriptor instead. +func (*ListSolutionEntitlementsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_solution_entitlements_proto_rawDescGZIP(), []int{1} +} + +func (x *ListSolutionEntitlementsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *ListSolutionEntitlementsRequest) GetSubjectId() string { + if x != nil { + return x.SubjectId + } + return "" +} + +func (x *ListSolutionEntitlementsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListSolutionEntitlementsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type ListSolutionEntitlementsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Entitlements []*SolutionEntitlement `protobuf:"bytes,1,rep,name=entitlements,proto3" json:"entitlements,omitempty"` + // Set when more entitlements remain; pass it back as page_token. Empty on the + // last page. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSolutionEntitlementsResponse) Reset() { + *x = ListSolutionEntitlementsResponse{} + mi := &file_saas_accounts_v1_solution_entitlements_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSolutionEntitlementsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSolutionEntitlementsResponse) ProtoMessage() {} + +func (x *ListSolutionEntitlementsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_solution_entitlements_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSolutionEntitlementsResponse.ProtoReflect.Descriptor instead. +func (*ListSolutionEntitlementsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_solution_entitlements_proto_rawDescGZIP(), []int{2} +} + +func (x *ListSolutionEntitlementsResponse) GetEntitlements() []*SolutionEntitlement { + if x != nil { + return x.Entitlements + } + return nil +} + +func (x *ListSolutionEntitlementsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +var File_saas_accounts_v1_solution_entitlements_proto protoreflect.FileDescriptor + +const file_saas_accounts_v1_solution_entitlements_proto_rawDesc = "" + + "\n" + + ",saas/accounts/v1/solution_entitlements.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\"\xdb\x01\n" + + "\x13SolutionEntitlement\x121\n" + + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\x125\n" + + "\x12root_scope_node_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0frootScopeNodeId\x12\x18\n" + + "\ahealthy\x18\x04 \x01(\bR\ahealthy\x12%\n" + + "\ttarget_id\x18\x05 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetIdJ\x04\b\x01\x10\x02R\x13solution_identifier\"\xb3\x01\n" + + "\x1fListSolutionEntitlementsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12'\n" + + "\n" + + "subject_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\tsubjectId\x12'\n" + + "\tpage_size\x18\x03 \x01(\x05B\n" + + "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x04 \x01(\tR\tpageToken\"\x95\x01\n" + + " ListSolutionEntitlementsResponse\x12I\n" + + "\fentitlements\x18\x01 \x03(\v2%.saas.accounts.v1.SolutionEntitlementR\fentitlements\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageTokenB\xd7\x01\n" + + "\x14com.saas.accounts.v1B\x19SolutionEntitlementsProtoP\x01ZBgithub.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" + +var ( + file_saas_accounts_v1_solution_entitlements_proto_rawDescOnce sync.Once + file_saas_accounts_v1_solution_entitlements_proto_rawDescData []byte +) + +func file_saas_accounts_v1_solution_entitlements_proto_rawDescGZIP() []byte { + file_saas_accounts_v1_solution_entitlements_proto_rawDescOnce.Do(func() { + file_saas_accounts_v1_solution_entitlements_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_solution_entitlements_proto_rawDesc), len(file_saas_accounts_v1_solution_entitlements_proto_rawDesc))) + }) + return file_saas_accounts_v1_solution_entitlements_proto_rawDescData +} + +var file_saas_accounts_v1_solution_entitlements_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_saas_accounts_v1_solution_entitlements_proto_goTypes = []any{ + (*SolutionEntitlement)(nil), // 0: saas.accounts.v1.SolutionEntitlement + (*ListSolutionEntitlementsRequest)(nil), // 1: saas.accounts.v1.ListSolutionEntitlementsRequest + (*ListSolutionEntitlementsResponse)(nil), // 2: saas.accounts.v1.ListSolutionEntitlementsResponse +} +var file_saas_accounts_v1_solution_entitlements_proto_depIdxs = []int32{ + 0, // 0: saas.accounts.v1.ListSolutionEntitlementsResponse.entitlements:type_name -> saas.accounts.v1.SolutionEntitlement + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_saas_accounts_v1_solution_entitlements_proto_init() } +func file_saas_accounts_v1_solution_entitlements_proto_init() { + if File_saas_accounts_v1_solution_entitlements_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_solution_entitlements_proto_rawDesc), len(file_saas_accounts_v1_solution_entitlements_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_saas_accounts_v1_solution_entitlements_proto_goTypes, + DependencyIndexes: file_saas_accounts_v1_solution_entitlements_proto_depIdxs, + MessageInfos: file_saas_accounts_v1_solution_entitlements_proto_msgTypes, + }.Build() + File_saas_accounts_v1_solution_entitlements_proto = out.File + file_saas_accounts_v1_solution_entitlements_proto_goTypes = nil + file_saas_accounts_v1_solution_entitlements_proto_depIdxs = nil +} diff --git a/gen/saas/accounts/v1/solution_entitlements_service.pb.go b/gen/saas/accounts/v1/solution_entitlements_service.pb.go new file mode 100644 index 0000000..7974b86 --- /dev/null +++ b/gen/saas/accounts/v1/solution_entitlements_service.pb.go @@ -0,0 +1,69 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: saas/accounts/v1/solution_entitlements_service.proto + +package accountsv1 + +import ( + _ "github.com/codefly-dev/saas-sdk-go/gen/saas/policy/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +var File_saas_accounts_v1_solution_entitlements_service_proto protoreflect.FileDescriptor + +const file_saas_accounts_v1_solution_entitlements_service_proto_rawDesc = "" + + "\n" + + "4saas/accounts/v1/solution_entitlements_service.proto\x12\x10saas.accounts.v1\x1a,saas/accounts/v1/solution_entitlements.proto\x1a\x1csaas/policy/v1/options.proto2\xba\x01\n" + + "\x1aSolutionEntitlementService\x12\x9b\x01\n" + + "\x18ListSolutionEntitlements\x121.saas.accounts.v1.ListSolutionEntitlementsRequest\x1a2.saas.accounts.v1.ListSolutionEntitlementsResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01B\xde\x01\n" + + "\x14com.saas.accounts.v1B SolutionEntitlementsServiceProtoP\x01ZBgithub.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" + +var file_saas_accounts_v1_solution_entitlements_service_proto_goTypes = []any{ + (*ListSolutionEntitlementsRequest)(nil), // 0: saas.accounts.v1.ListSolutionEntitlementsRequest + (*ListSolutionEntitlementsResponse)(nil), // 1: saas.accounts.v1.ListSolutionEntitlementsResponse +} +var file_saas_accounts_v1_solution_entitlements_service_proto_depIdxs = []int32{ + 0, // 0: saas.accounts.v1.SolutionEntitlementService.ListSolutionEntitlements:input_type -> saas.accounts.v1.ListSolutionEntitlementsRequest + 1, // 1: saas.accounts.v1.SolutionEntitlementService.ListSolutionEntitlements:output_type -> saas.accounts.v1.ListSolutionEntitlementsResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_saas_accounts_v1_solution_entitlements_service_proto_init() } +func file_saas_accounts_v1_solution_entitlements_service_proto_init() { + if File_saas_accounts_v1_solution_entitlements_service_proto != nil { + return + } + file_saas_accounts_v1_solution_entitlements_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_solution_entitlements_service_proto_rawDesc), len(file_saas_accounts_v1_solution_entitlements_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 0, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_saas_accounts_v1_solution_entitlements_service_proto_goTypes, + DependencyIndexes: file_saas_accounts_v1_solution_entitlements_service_proto_depIdxs, + }.Build() + File_saas_accounts_v1_solution_entitlements_service_proto = out.File + file_saas_accounts_v1_solution_entitlements_service_proto_goTypes = nil + file_saas_accounts_v1_solution_entitlements_service_proto_depIdxs = nil +} diff --git a/gen/saas/accounts/v1/solution_registry.pb.go b/gen/saas/accounts/v1/solution_registry.pb.go index 883b212..8854d92 100644 --- a/gen/saas/accounts/v1/solution_registry.pb.go +++ b/gen/saas/accounts/v1/solution_registry.pb.go @@ -7,7 +7,6 @@ package accountsv1 import ( - _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" @@ -23,28 +22,22 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// SolutionRegistrationStatus is derived at read time from the stored record; it -// is never a stored column. It is what lets an operator tell a registration -// that was never completed from one whose publisher stopped renewing, and both -// from one that was deliberately removed. +// SolutionRegistrationStatus is derived from declared state and complete, +// compatible frontend/backend observations, never from an expiring lease. type SolutionRegistrationStatus int32 const ( SolutionRegistrationStatus_SOLUTION_REGISTRATION_STATUS_UNSPECIFIED SolutionRegistrationStatus = 0 - // Both halves are present, their leases are live, and their declared contract + // Both halves are present and their declared contract // versions agree. This is the only status either surface serves. SolutionRegistrationStatus_SOLUTION_REGISTRATION_STATUS_ACTIVE SolutionRegistrationStatus = 1 - // A half has never registered. The record is durable but incomplete, so no + // A half has not been observed. The record is durable but incomplete, so no // page advertises it and the gateway has nothing to route to. SolutionRegistrationStatus_SOLUTION_REGISTRATION_STATUS_PENDING SolutionRegistrationStatus = 2 - // Both halves registered, but at least one lease lapsed: the deployment that - // owns it stopped renewing. The intent survives; the endpoint is not live. - SolutionRegistrationStatus_SOLUTION_REGISTRATION_STATUS_EXPIRED SolutionRegistrationStatus = 3 // The two halves declare different contract versions, so activating them // together would serve a page against a backend it was not built for. SolutionRegistrationStatus_SOLUTION_REGISTRATION_STATUS_INCOMPATIBLE SolutionRegistrationStatus = 4 - // The registration was deregistered. The row survives as a tombstone so a - // delayed heartbeat from a retiring deployment cannot recreate it. + // An applied removal keeps the declaration as a tombstone. SolutionRegistrationStatus_SOLUTION_REGISTRATION_STATUS_TOMBSTONED SolutionRegistrationStatus = 5 ) @@ -54,7 +47,6 @@ var ( 0: "SOLUTION_REGISTRATION_STATUS_UNSPECIFIED", 1: "SOLUTION_REGISTRATION_STATUS_ACTIVE", 2: "SOLUTION_REGISTRATION_STATUS_PENDING", - 3: "SOLUTION_REGISTRATION_STATUS_EXPIRED", 4: "SOLUTION_REGISTRATION_STATUS_INCOMPATIBLE", 5: "SOLUTION_REGISTRATION_STATUS_TOMBSTONED", } @@ -62,7 +54,6 @@ var ( "SOLUTION_REGISTRATION_STATUS_UNSPECIFIED": 0, "SOLUTION_REGISTRATION_STATUS_ACTIVE": 1, "SOLUTION_REGISTRATION_STATUS_PENDING": 2, - "SOLUTION_REGISTRATION_STATUS_EXPIRED": 3, "SOLUTION_REGISTRATION_STATUS_INCOMPATIBLE": 4, "SOLUTION_REGISTRATION_STATUS_TOMBSTONED": 5, } @@ -95,16 +86,80 @@ func (SolutionRegistrationStatus) EnumDescriptor() ([]byte, []int) { return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{0} } +// SolutionDeclaredKind is what a declaration declares the presence of. +// +// Core requires it on every presence document and refuses any other value, so +// every admitted declaration has exactly one of the two. The host used to drop +// it on reconcile, which left the registry unable to say whether a record was a +// solution or a module — and the two are routed on different surfaces with +// different admission, so a host that cannot tell them apart must either serve +// both from one surface or serve neither. +// +// UNSPECIFIED is not a third kind and is never a default. It is what a reader +// decodes from a writer that did not set the field, and both routing surfaces +// refuse it by name rather than picking the kind that happens to be more +// permissive. +type SolutionDeclaredKind int32 + +const ( + SolutionDeclaredKind_SOLUTION_DECLARED_KIND_UNSPECIFIED SolutionDeclaredKind = 0 + // A composed solution instance, routed at /solutions//* with + // per-viewer installation admission. + SolutionDeclaredKind_SOLUTION_DECLARED_KIND_SOLUTION SolutionDeclaredKind = 1 + // One module instance, routed at /v1//* with the ordinary + // authenticated pipeline. + SolutionDeclaredKind_SOLUTION_DECLARED_KIND_MODULE SolutionDeclaredKind = 2 +) + +// Enum value maps for SolutionDeclaredKind. +var ( + SolutionDeclaredKind_name = map[int32]string{ + 0: "SOLUTION_DECLARED_KIND_UNSPECIFIED", + 1: "SOLUTION_DECLARED_KIND_SOLUTION", + 2: "SOLUTION_DECLARED_KIND_MODULE", + } + SolutionDeclaredKind_value = map[string]int32{ + "SOLUTION_DECLARED_KIND_UNSPECIFIED": 0, + "SOLUTION_DECLARED_KIND_SOLUTION": 1, + "SOLUTION_DECLARED_KIND_MODULE": 2, + } +) + +func (x SolutionDeclaredKind) Enum() *SolutionDeclaredKind { + p := new(SolutionDeclaredKind) + *p = x + return p +} + +func (x SolutionDeclaredKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SolutionDeclaredKind) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_solution_registry_proto_enumTypes[1].Descriptor() +} + +func (SolutionDeclaredKind) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_solution_registry_proto_enumTypes[1] +} + +func (x SolutionDeclaredKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SolutionDeclaredKind.Descriptor instead. +func (SolutionDeclaredKind) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{1} +} + // SolutionFrontendBinding is the stored frontend half. manifest is the document -// the frontend validated before handing it over; this service persists it -// verbatim and never reinterprets it, so the host keeps sole ownership of what -// a manifest may contain. +// the frontend validates while reading the projection; accounts stores it +// verbatim and keeps presence authority separate from runtime compatibility. type SolutionFrontendBinding struct { state protoimpl.MessageState `protogen:"open.v1"` Revision int64 `protobuf:"varint,1,opt,name=revision,proto3" json:"revision,omitempty"` Manifest string `protobuf:"bytes,2,opt,name=manifest,proto3" json:"manifest,omitempty"` ContractVersion string `protobuf:"bytes,3,opt,name=contract_version,json=contractVersion,proto3" json:"contract_version,omitempty"` - LeaseExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=lease_expires_at,json=leaseExpiresAt,proto3" json:"lease_expires_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -160,13 +215,6 @@ func (x *SolutionFrontendBinding) GetContractVersion() string { return "" } -func (x *SolutionFrontendBinding) GetLeaseExpiresAt() *timestamppb.Timestamp { - if x != nil { - return x.LeaseExpiresAt - } - return nil -} - // SolutionBackendBinding is the stored backend half: the upstream the gateway // proxies to and the logical service alias it is addressed by. type SolutionBackendBinding struct { @@ -175,7 +223,6 @@ type SolutionBackendBinding struct { Upstream string `protobuf:"bytes,2,opt,name=upstream,proto3" json:"upstream,omitempty"` ServiceAlias string `protobuf:"bytes,3,opt,name=service_alias,json=serviceAlias,proto3" json:"service_alias,omitempty"` ContractVersion string `protobuf:"bytes,4,opt,name=contract_version,json=contractVersion,proto3" json:"contract_version,omitempty"` - LeaseExpiresAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=lease_expires_at,json=leaseExpiresAt,proto3" json:"lease_expires_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -238,21 +285,13 @@ func (x *SolutionBackendBinding) GetContractVersion() string { return "" } -func (x *SolutionBackendBinding) GetLeaseExpiresAt() *timestamppb.Timestamp { - if x != nil { - return x.LeaseExpiresAt - } - return nil -} - // SolutionRegistration is the canonical record. revision is drawn from one // registry-wide sequence, so revisions are comparable across records and a // consumer can order two snapshots without a wall clock. type SolutionRegistration struct { state protoimpl.MessageState `protogen:"open.v1"` SolutionId string `protobuf:"bytes,1,opt,name=solution_id,json=solutionId,proto3" json:"solution_id,omitempty"` - // Owner of record. First claim binds it; a registration naming a different - // publisher is refused rather than overwriting. + // Canonical publisher attribution of the declared record. Publisher string `protobuf:"bytes,2,opt,name=publisher,proto3" json:"publisher,omitempty"` Revision int64 `protobuf:"varint,3,opt,name=revision,proto3" json:"revision,omitempty"` Status SolutionRegistrationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=saas.accounts.v1.SolutionRegistrationStatus" json:"status,omitempty"` @@ -260,20 +299,10 @@ type SolutionRegistration struct { Backend *SolutionBackendBinding `protobuf:"bytes,6,opt,name=backend,proto3,oneof" json:"backend,omitempty"` UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` TombstonedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=tombstoned_at,json=tombstonedAt,proto3,oneof" json:"tombstoned_at,omitempty"` - // The seed a solution's runtime boundary is derived from (issue #1015). - // - // ALWAYS EMPTY on the wire, on every response, deliberately. The seed is - // assigned by this service when the record is created and never again, and it - // is the only input to the per-organization boundary every Work Context - // minted for that solution is sealed under — so it is what stands between one - // solution's runs and another's. No registrant needs it: accounts derives and - // seals the boundary from the credential the solution already presents. - // - // The field is kept rather than removed so this promise is testable: a change - // that starts populating it fails a test instead of shipping. - RuntimeBoundary string `protobuf:"bytes,9,opt,name=runtime_boundary,json=runtimeBoundary,proto3" json:"runtime_boundary,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The binding and immutable target that declared this record. + Declared *SolutionDeclaredBinding `protobuf:"bytes,10,opt,name=declared,proto3,oneof" json:"declared,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SolutionRegistration) Reset() { @@ -362,37 +391,39 @@ func (x *SolutionRegistration) GetTombstonedAt() *timestamppb.Timestamp { return nil } -func (x *SolutionRegistration) GetRuntimeBoundary() string { +func (x *SolutionRegistration) GetDeclared() *SolutionDeclaredBinding { if x != nil { - return x.RuntimeBoundary + return x.Declared } - return "" + return nil } -// SolutionFrontendRegistration is the caller-supplied frontend half. Revision -// and lease are assigned by the server, never proposed by the registrant. -type SolutionFrontendRegistration struct { - state protoimpl.MessageState `protogen:"open.v1"` - Manifest string `protobuf:"bytes,1,opt,name=manifest,proto3" json:"manifest,omitempty"` - ContractVersion string `protobuf:"bytes,2,opt,name=contract_version,json=contractVersion,proto3" json:"contract_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +// ListSolutionRegistrationsRequest reads the whole registry. There is one +// registry per deployment and it holds tens of records at most, so consumers +// rebuild their cache from a full snapshot rather than tailing a change feed. +type ListSolutionRegistrationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Tombstones are omitted by default: a routing cache wants the live set. An + // operator diagnosing "was this deleted or never registered?" asks for them. + IncludeTombstoned bool `protobuf:"varint,1,opt,name=include_tombstoned,json=includeTombstoned,proto3" json:"include_tombstoned,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *SolutionFrontendRegistration) Reset() { - *x = SolutionFrontendRegistration{} +func (x *ListSolutionRegistrationsRequest) Reset() { + *x = ListSolutionRegistrationsRequest{} mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SolutionFrontendRegistration) String() string { +func (x *ListSolutionRegistrationsRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SolutionFrontendRegistration) ProtoMessage() {} +func (*ListSolutionRegistrationsRequest) ProtoMessage() {} -func (x *SolutionFrontendRegistration) ProtoReflect() protoreflect.Message { +func (x *ListSolutionRegistrationsRequest) ProtoReflect() protoreflect.Message { mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -404,51 +435,42 @@ func (x *SolutionFrontendRegistration) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SolutionFrontendRegistration.ProtoReflect.Descriptor instead. -func (*SolutionFrontendRegistration) Descriptor() ([]byte, []int) { +// Deprecated: Use ListSolutionRegistrationsRequest.ProtoReflect.Descriptor instead. +func (*ListSolutionRegistrationsRequest) Descriptor() ([]byte, []int) { return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{3} } -func (x *SolutionFrontendRegistration) GetManifest() string { - if x != nil { - return x.Manifest - } - return "" -} - -func (x *SolutionFrontendRegistration) GetContractVersion() string { +func (x *ListSolutionRegistrationsRequest) GetIncludeTombstoned() bool { if x != nil { - return x.ContractVersion + return x.IncludeTombstoned } - return "" + return false } -// SolutionBackendRegistration is the caller-supplied backend half. The gateway -// has already constrained the upstream to a permitted host before it gets here; -// this service stores what it is told and does not re-derive routing policy. -type SolutionBackendRegistration struct { - state protoimpl.MessageState `protogen:"open.v1"` - Upstream string `protobuf:"bytes,1,opt,name=upstream,proto3" json:"upstream,omitempty"` - ServiceAlias string `protobuf:"bytes,2,opt,name=service_alias,json=serviceAlias,proto3" json:"service_alias,omitempty"` - ContractVersion string `protobuf:"bytes,3,opt,name=contract_version,json=contractVersion,proto3" json:"contract_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type ListSolutionRegistrationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Registrations []*SolutionRegistration `protobuf:"bytes,1,rep,name=registrations,proto3" json:"registrations,omitempty"` + // Highest revision in the registry, tombstones included. Two consumers that + // report the same registry_revision have converged on the same state. + RegistryRevision int64 `protobuf:"varint,2,opt,name=registry_revision,json=registryRevision,proto3" json:"registry_revision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *SolutionBackendRegistration) Reset() { - *x = SolutionBackendRegistration{} +func (x *ListSolutionRegistrationsResponse) Reset() { + *x = ListSolutionRegistrationsResponse{} mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *SolutionBackendRegistration) String() string { +func (x *ListSolutionRegistrationsResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*SolutionBackendRegistration) ProtoMessage() {} +func (*ListSolutionRegistrationsResponse) ProtoMessage() {} -func (x *SolutionBackendRegistration) ProtoReflect() protoreflect.Message { +func (x *ListSolutionRegistrationsResponse) ProtoReflect() protoreflect.Message { mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -460,75 +482,71 @@ func (x *SolutionBackendRegistration) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use SolutionBackendRegistration.ProtoReflect.Descriptor instead. -func (*SolutionBackendRegistration) Descriptor() ([]byte, []int) { +// Deprecated: Use ListSolutionRegistrationsResponse.ProtoReflect.Descriptor instead. +func (*ListSolutionRegistrationsResponse) Descriptor() ([]byte, []int) { return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{4} } -func (x *SolutionBackendRegistration) GetUpstream() string { - if x != nil { - return x.Upstream - } - return "" -} - -func (x *SolutionBackendRegistration) GetServiceAlias() string { +func (x *ListSolutionRegistrationsResponse) GetRegistrations() []*SolutionRegistration { if x != nil { - return x.ServiceAlias + return x.Registrations } - return "" + return nil } -func (x *SolutionBackendRegistration) GetContractVersion() string { +func (x *ListSolutionRegistrationsResponse) GetRegistryRevision() int64 { if x != nil { - return x.ContractVersion + return x.RegistryRevision } - return "" + return 0 } -// PutSolutionRegistrationRequest writes exactly one half of one record. -// -// Writing a half whose content is byte-identical to what is stored is a lease -// renewal: it refreshes liveness and deliberately does not advance the -// revision, so a heartbeat never looks like a change to a consumer. Writing -// different content to a half that already exists requires expected_revision, -// which must equal the record's current revision — that is how a publisher -// still holding an older view is stopped from overwriting newer state, and how -// two concurrent updates serialize instead of racing. -type PutSolutionRegistrationRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SolutionId string `protobuf:"bytes,1,opt,name=solution_id,json=solutionId,proto3" json:"solution_id,omitempty"` - Publisher string `protobuf:"bytes,2,opt,name=publisher,proto3" json:"publisher,omitempty"` - // Compare-and-swap guard against the record's current revision. Required to - // change an existing half, and the only way to re-register a tombstoned - // record: the caller must name the tombstone's own revision, which a delayed - // retry from a retired deployment does not hold. - ExpectedRevision *int64 `protobuf:"varint,3,opt,name=expected_revision,json=expectedRevision,proto3,oneof" json:"expected_revision,omitempty"` - // Requested liveness window for this half. The registrant renews within it. - LeaseSeconds uint32 `protobuf:"varint,4,opt,name=lease_seconds,json=leaseSeconds,proto3" json:"lease_seconds,omitempty"` - // Types that are valid to be assigned to Half: +// SolutionDeclaredBinding identifies the applied declaration and target. +type SolutionDeclaredBinding struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable ID of the deployment instance that declared this record. It is not + // the solution id: a binding ID identifies one instance and may carry + // characters a path segment may not. + BindingId string `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"` + // The generation of that binding the host applied into this record. + Generation uint64 `protobuf:"varint,2,opt,name=generation,proto3" json:"generation,omitempty"` + // publisher/name@version of the applied generation. + Release string `protobuf:"bytes,3,opt,name=release,proto3" json:"release,omitempty"` + // The immutable solution target this declaration opened — the identity an + // installation names. It is carried on the registration record so a consumer + // that was asked about a ROUTE ALIAS can resolve it to the target that + // currently serves it and compare identities. // - // *PutSolutionRegistrationRequest_Frontend - // *PutSolutionRegistrationRequest_Backend - Half isPutSolutionRegistrationRequest_Half `protobuf_oneof:"half"` + // That resolution is the whole admission mechanism: a replacement binding + // which claimed a withdrawn alias resolves to its own target, which no + // installation of its predecessor names. A record with no declaration carries + // no target and is therefore admissible to nobody, which is the fail-closed + // answer and the direction of the cutover. + TargetId string `protobuf:"bytes,4,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + // What the declaration declared the presence of. It decides which routing + // surface serves this record, so it is carried rather than inferred: a host + // that read the kind off the shape of a record — a publisher prefix, an alias + // convention — would be guessing at exactly the question that separates a + // surface with per-viewer admission from one without. + Kind SolutionDeclaredKind `protobuf:"varint,5,opt,name=kind,proto3,enum=saas.accounts.v1.SolutionDeclaredKind" json:"kind,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *PutSolutionRegistrationRequest) Reset() { - *x = PutSolutionRegistrationRequest{} +func (x *SolutionDeclaredBinding) Reset() { + *x = SolutionDeclaredBinding{} mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *PutSolutionRegistrationRequest) String() string { +func (x *SolutionDeclaredBinding) String() string { return protoimpl.X.MessageStringOf(x) } -func (*PutSolutionRegistrationRequest) ProtoMessage() {} +func (*SolutionDeclaredBinding) ProtoMessage() {} -func (x *PutSolutionRegistrationRequest) ProtoReflect() protoreflect.Message { +func (x *SolutionDeclaredBinding) ProtoReflect() protoreflect.Message { mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -540,106 +558,153 @@ func (x *PutSolutionRegistrationRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use PutSolutionRegistrationRequest.ProtoReflect.Descriptor instead. -func (*PutSolutionRegistrationRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use SolutionDeclaredBinding.ProtoReflect.Descriptor instead. +func (*SolutionDeclaredBinding) Descriptor() ([]byte, []int) { return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{5} } -func (x *PutSolutionRegistrationRequest) GetSolutionId() string { +func (x *SolutionDeclaredBinding) GetBindingId() string { if x != nil { - return x.SolutionId + return x.BindingId } return "" } -func (x *PutSolutionRegistrationRequest) GetPublisher() string { +func (x *SolutionDeclaredBinding) GetGeneration() uint64 { if x != nil { - return x.Publisher - } - return "" -} - -func (x *PutSolutionRegistrationRequest) GetExpectedRevision() int64 { - if x != nil && x.ExpectedRevision != nil { - return *x.ExpectedRevision + return x.Generation } return 0 } -func (x *PutSolutionRegistrationRequest) GetLeaseSeconds() uint32 { +func (x *SolutionDeclaredBinding) GetRelease() string { if x != nil { - return x.LeaseSeconds + return x.Release } - return 0 + return "" } -func (x *PutSolutionRegistrationRequest) GetHalf() isPutSolutionRegistrationRequest_Half { +func (x *SolutionDeclaredBinding) GetTargetId() string { if x != nil { - return x.Half + return x.TargetId } - return nil + return "" } -func (x *PutSolutionRegistrationRequest) GetFrontend() *SolutionFrontendRegistration { +func (x *SolutionDeclaredBinding) GetKind() SolutionDeclaredKind { if x != nil { - if x, ok := x.Half.(*PutSolutionRegistrationRequest_Frontend); ok { - return x.Frontend - } + return x.Kind } - return nil + return SolutionDeclaredKind_SOLUTION_DECLARED_KIND_UNSPECIFIED +} + +// SolutionHostBindingGeneration is one generation of one document. The document +// is carried whole, canonically encoded, because "what is this host running" and +// "what has it been asked to run" are both answers an operator needs in full. +type SolutionHostBindingGeneration struct { + state protoimpl.MessageState `protogen:"open.v1"` + Generation uint64 `protobuf:"varint,1,opt,name=generation,proto3" json:"generation,omitempty"` + // The canonical digest of the document, which is how a rewritten generation is + // told from a re-read of the applied one. + Digest string `protobuf:"bytes,2,opt,name=digest,proto3" json:"digest,omitempty"` + Document string `protobuf:"bytes,3,opt,name=document,proto3" json:"document,omitempty"` + At *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=at,proto3" json:"at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SolutionHostBindingGeneration) Reset() { + *x = SolutionHostBindingGeneration{} + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *PutSolutionRegistrationRequest) GetBackend() *SolutionBackendRegistration { +func (x *SolutionHostBindingGeneration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SolutionHostBindingGeneration) ProtoMessage() {} + +func (x *SolutionHostBindingGeneration) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[6] if x != nil { - if x, ok := x.Half.(*PutSolutionRegistrationRequest_Backend); ok { - return x.Backend + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) } -type isPutSolutionRegistrationRequest_Half interface { - isPutSolutionRegistrationRequest_Half() +// Deprecated: Use SolutionHostBindingGeneration.ProtoReflect.Descriptor instead. +func (*SolutionHostBindingGeneration) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{6} } -type PutSolutionRegistrationRequest_Frontend struct { - Frontend *SolutionFrontendRegistration `protobuf:"bytes,5,opt,name=frontend,proto3,oneof"` +func (x *SolutionHostBindingGeneration) GetGeneration() uint64 { + if x != nil { + return x.Generation + } + return 0 } -type PutSolutionRegistrationRequest_Backend struct { - Backend *SolutionBackendRegistration `protobuf:"bytes,6,opt,name=backend,proto3,oneof"` +func (x *SolutionHostBindingGeneration) GetDigest() string { + if x != nil { + return x.Digest + } + return "" } -func (*PutSolutionRegistrationRequest_Frontend) isPutSolutionRegistrationRequest_Half() {} +func (x *SolutionHostBindingGeneration) GetDocument() string { + if x != nil { + return x.Document + } + return "" +} -func (*PutSolutionRegistrationRequest_Backend) isPutSolutionRegistrationRequest_Half() {} +func (x *SolutionHostBindingGeneration) GetAt() *timestamppb.Timestamp { + if x != nil { + return x.At + } + return nil +} -// DeleteSolutionRegistrationRequest tombstones a record: both halves are -// cleared in the same statement that marks it deleted, so a reader that somehow -// ignored the tombstone still has no endpoint to route to. -type DeleteSolutionRegistrationRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SolutionId string `protobuf:"bytes,1,opt,name=solution_id,json=solutionId,proto3" json:"solution_id,omitempty"` - ExpectedRevision *int64 `protobuf:"varint,2,opt,name=expected_revision,json=expectedRevision,proto3,oneof" json:"expected_revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +// SolutionHostBindingAppliedGeneration is the generation this host reconciled, +// and what it reconciled it into. +type SolutionHostBindingAppliedGeneration struct { + state protoimpl.MessageState `protogen:"open.v1"` + Generation *SolutionHostBindingGeneration `protobuf:"bytes,1,opt,name=generation,proto3" json:"generation,omitempty"` + // A tombstone generation: the binding is declared absent. It is not an + // absence — the generation stays on the record, so a late or replayed older + // generation is still refused after a removal. + Removed bool `protobuf:"varint,2,opt,name=removed,proto3" json:"removed,omitempty"` + // The route aliases the applied generation holds. A tombstone holds none. + Routes []string `protobuf:"bytes,3,rep,name=routes,proto3" json:"routes,omitempty"` + // The registration record this generation reconciled into. A tombstone keeps + // the key it withdrew, which is the only way it knows what to withdraw. + SolutionId string `protobuf:"bytes,4,opt,name=solution_id,json=solutionId,proto3" json:"solution_id,omitempty"` + Release string `protobuf:"bytes,5,opt,name=release,proto3" json:"release,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *DeleteSolutionRegistrationRequest) Reset() { - *x = DeleteSolutionRegistrationRequest{} - mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[6] +func (x *SolutionHostBindingAppliedGeneration) Reset() { + *x = SolutionHostBindingAppliedGeneration{} + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DeleteSolutionRegistrationRequest) String() string { +func (x *SolutionHostBindingAppliedGeneration) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeleteSolutionRegistrationRequest) ProtoMessage() {} +func (*SolutionHostBindingAppliedGeneration) ProtoMessage() {} -func (x *DeleteSolutionRegistrationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[6] +func (x *SolutionHostBindingAppliedGeneration) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -650,52 +715,89 @@ func (x *DeleteSolutionRegistrationRequest) ProtoReflect() protoreflect.Message return mi.MessageOf(x) } -// Deprecated: Use DeleteSolutionRegistrationRequest.ProtoReflect.Descriptor instead. -func (*DeleteSolutionRegistrationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{6} +// Deprecated: Use SolutionHostBindingAppliedGeneration.ProtoReflect.Descriptor instead. +func (*SolutionHostBindingAppliedGeneration) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{7} +} + +func (x *SolutionHostBindingAppliedGeneration) GetGeneration() *SolutionHostBindingGeneration { + if x != nil { + return x.Generation + } + return nil } -func (x *DeleteSolutionRegistrationRequest) GetSolutionId() string { +func (x *SolutionHostBindingAppliedGeneration) GetRemoved() bool { + if x != nil { + return x.Removed + } + return false +} + +func (x *SolutionHostBindingAppliedGeneration) GetRoutes() []string { + if x != nil { + return x.Routes + } + return nil +} + +func (x *SolutionHostBindingAppliedGeneration) GetSolutionId() string { if x != nil { return x.SolutionId } return "" } -func (x *DeleteSolutionRegistrationRequest) GetExpectedRevision() int64 { - if x != nil && x.ExpectedRevision != nil { - return *x.ExpectedRevision +func (x *SolutionHostBindingAppliedGeneration) GetRelease() string { + if x != nil { + return x.Release } - return 0 + return "" } -// ListSolutionRegistrationsRequest reads the whole registry. There is one -// registry per deployment and it holds tens of records at most, so consumers -// rebuild their cache from a full snapshot rather than tailing a change feed. -type ListSolutionRegistrationsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Tombstones are omitted by default: a routing cache wants the live set. An - // operator diagnosing "was this deleted or never registered?" asks for them. - IncludeTombstoned bool `protobuf:"varint,1,opt,name=include_tombstoned,json=includeTombstoned,proto3" json:"include_tombstoned,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +// SolutionHostBindingState is one binding's whole state: desired, applied, and +// why they differ. Endpoint and manifest observations live on +// SolutionRegistration, separate from the declaration that grants presence. +type SolutionHostBindingState struct { + state protoimpl.MessageState `protogen:"open.v1"` + BindingId string `protobuf:"bytes,1,opt,name=binding_id,json=bindingId,proto3" json:"binding_id,omitempty"` + HostCoordinate string `protobuf:"bytes,2,opt,name=host_coordinate,json=hostCoordinate,proto3" json:"host_coordinate,omitempty"` + HostComponent string `protobuf:"bytes,3,opt,name=host_component,json=hostComponent,proto3" json:"host_component,omitempty"` + // The newest generation delivery has shown this host, whether or not it + // passed. + Desired *SolutionHostBindingGeneration `protobuf:"bytes,4,opt,name=desired,proto3,oneof" json:"desired,omitempty"` + // The generation this host applied. Absent means nothing has ever passed, so + // nothing about this binding is being served. + Applied *SolutionHostBindingAppliedGeneration `protobuf:"bytes,5,opt,name=applied,proto3,oneof" json:"applied,omitempty"` + // The generation delivery is showing that this host has not applied, or 0 when + // desired and applied agree. + PendingGeneration uint64 `protobuf:"varint,6,opt,name=pending_generation,json=pendingGeneration,proto3" json:"pending_generation,omitempty"` + // Why the pending generation was not applied. Empty when nothing is pending. + PendingReason string `protobuf:"bytes,7,opt,name=pending_reason,json=pendingReason,proto3" json:"pending_reason,omitempty"` + PendingSince *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=pending_since,json=pendingSince,proto3,oneof" json:"pending_since,omitempty"` + // The registration record this binding declares, when it has applied one. It + // carries endpoint and manifest observations and their derived status. + Registration *SolutionRegistration `protobuf:"bytes,9,opt,name=registration,proto3,oneof" json:"registration,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *ListSolutionRegistrationsRequest) Reset() { - *x = ListSolutionRegistrationsRequest{} - mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[7] +func (x *SolutionHostBindingState) Reset() { + *x = SolutionHostBindingState{} + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListSolutionRegistrationsRequest) String() string { +func (x *SolutionHostBindingState) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListSolutionRegistrationsRequest) ProtoMessage() {} +func (*SolutionHostBindingState) ProtoMessage() {} -func (x *ListSolutionRegistrationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[7] +func (x *SolutionHostBindingState) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -706,43 +808,105 @@ func (x *ListSolutionRegistrationsRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListSolutionRegistrationsRequest.ProtoReflect.Descriptor instead. -func (*ListSolutionRegistrationsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{7} +// Deprecated: Use SolutionHostBindingState.ProtoReflect.Descriptor instead. +func (*SolutionHostBindingState) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{8} } -func (x *ListSolutionRegistrationsRequest) GetIncludeTombstoned() bool { +func (x *SolutionHostBindingState) GetBindingId() string { if x != nil { - return x.IncludeTombstoned + return x.BindingId } - return false + return "" } -type ListSolutionRegistrationsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Registrations []*SolutionRegistration `protobuf:"bytes,1,rep,name=registrations,proto3" json:"registrations,omitempty"` - // Highest revision in the registry, tombstones included. Two consumers that - // report the same registry_revision have converged on the same state. - RegistryRevision int64 `protobuf:"varint,2,opt,name=registry_revision,json=registryRevision,proto3" json:"registry_revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *SolutionHostBindingState) GetHostCoordinate() string { + if x != nil { + return x.HostCoordinate + } + return "" } -func (x *ListSolutionRegistrationsResponse) Reset() { - *x = ListSolutionRegistrationsResponse{} - mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[8] +func (x *SolutionHostBindingState) GetHostComponent() string { + if x != nil { + return x.HostComponent + } + return "" +} + +func (x *SolutionHostBindingState) GetDesired() *SolutionHostBindingGeneration { + if x != nil { + return x.Desired + } + return nil +} + +func (x *SolutionHostBindingState) GetApplied() *SolutionHostBindingAppliedGeneration { + if x != nil { + return x.Applied + } + return nil +} + +func (x *SolutionHostBindingState) GetPendingGeneration() uint64 { + if x != nil { + return x.PendingGeneration + } + return 0 +} + +func (x *SolutionHostBindingState) GetPendingReason() string { + if x != nil { + return x.PendingReason + } + return "" +} + +func (x *SolutionHostBindingState) GetPendingSince() *timestamppb.Timestamp { + if x != nil { + return x.PendingSince + } + return nil +} + +func (x *SolutionHostBindingState) GetRegistration() *SolutionRegistration { + if x != nil { + return x.Registration + } + return nil +} + +func (x *SolutionHostBindingState) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +// ListSolutionHostBindingsRequest reads every declared binding. There is one set +// per deployment and it holds tens of bindings at most, so a consumer rebuilds +// its view from a full snapshot. +type ListSolutionHostBindingsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSolutionHostBindingsRequest) Reset() { + *x = ListSolutionHostBindingsRequest{} + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ListSolutionRegistrationsResponse) String() string { +func (x *ListSolutionHostBindingsRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListSolutionRegistrationsResponse) ProtoMessage() {} +func (*ListSolutionHostBindingsRequest) ProtoMessage() {} -func (x *ListSolutionRegistrationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[8] +func (x *ListSolutionHostBindingsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -753,41 +917,69 @@ func (x *ListSolutionRegistrationsResponse) ProtoReflect() protoreflect.Message return mi.MessageOf(x) } -// Deprecated: Use ListSolutionRegistrationsResponse.ProtoReflect.Descriptor instead. -func (*ListSolutionRegistrationsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{8} +// Deprecated: Use ListSolutionHostBindingsRequest.ProtoReflect.Descriptor instead. +func (*ListSolutionHostBindingsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{9} } -func (x *ListSolutionRegistrationsResponse) GetRegistrations() []*SolutionRegistration { +type ListSolutionHostBindingsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Bindings []*SolutionHostBindingState `protobuf:"bytes,1,rep,name=bindings,proto3" json:"bindings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSolutionHostBindingsResponse) Reset() { + *x = ListSolutionHostBindingsResponse{} + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSolutionHostBindingsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSolutionHostBindingsResponse) ProtoMessage() {} + +func (x *ListSolutionHostBindingsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_solution_registry_proto_msgTypes[10] if x != nil { - return x.Registrations + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return nil + return mi.MessageOf(x) } -func (x *ListSolutionRegistrationsResponse) GetRegistryRevision() int64 { +// Deprecated: Use ListSolutionHostBindingsResponse.ProtoReflect.Descriptor instead. +func (*ListSolutionHostBindingsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_solution_registry_proto_rawDescGZIP(), []int{10} +} + +func (x *ListSolutionHostBindingsResponse) GetBindings() []*SolutionHostBindingState { if x != nil { - return x.RegistryRevision + return x.Bindings } - return 0 + return nil } var File_saas_accounts_v1_solution_registry_proto protoreflect.FileDescriptor const file_saas_accounts_v1_solution_registry_proto_rawDesc = "" + "\n" + - "(saas/accounts/v1/solution_registry.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc2\x01\n" + + "(saas/accounts/v1/solution_registry.proto\x12\x10saas.accounts.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x94\x01\n" + "\x17SolutionFrontendBinding\x12\x1a\n" + "\brevision\x18\x01 \x01(\x03R\brevision\x12\x1a\n" + "\bmanifest\x18\x02 \x01(\tR\bmanifest\x12)\n" + - "\x10contract_version\x18\x03 \x01(\tR\x0fcontractVersion\x12D\n" + - "\x10lease_expires_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x0eleaseExpiresAt\"\xe6\x01\n" + + "\x10contract_version\x18\x03 \x01(\tR\x0fcontractVersionJ\x04\b\x04\x10\x05R\x10lease_expires_at\"\xb8\x01\n" + "\x16SolutionBackendBinding\x12\x1a\n" + "\brevision\x18\x01 \x01(\x03R\brevision\x12\x1a\n" + "\bupstream\x18\x02 \x01(\tR\bupstream\x12#\n" + "\rservice_alias\x18\x03 \x01(\tR\fserviceAlias\x12)\n" + - "\x10contract_version\x18\x04 \x01(\tR\x0fcontractVersion\x12D\n" + - "\x10lease_expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\x0eleaseExpiresAt\"\xa3\x04\n" + + "\x10contract_version\x18\x04 \x01(\tR\x0fcontractVersionJ\x04\b\x05\x10\x06R\x10lease_expires_at\"\xe9\x04\n" + "\x14SolutionRegistration\x12\x1f\n" + "\vsolution_id\x18\x01 \x01(\tR\n" + "solutionId\x12\x1c\n" + @@ -798,51 +990,78 @@ const file_saas_accounts_v1_solution_registry_proto_rawDesc = "" + "\abackend\x18\x06 \x01(\v2(.saas.accounts.v1.SolutionBackendBindingH\x01R\abackend\x88\x01\x01\x129\n" + "\n" + "updated_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12D\n" + - "\rtombstoned_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x02R\ftombstonedAt\x88\x01\x01\x12)\n" + - "\x10runtime_boundary\x18\t \x01(\tR\x0fruntimeBoundaryB\v\n" + + "\rtombstoned_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x02R\ftombstonedAt\x88\x01\x01\x12J\n" + + "\bdeclared\x18\n" + + " \x01(\v2).saas.accounts.v1.SolutionDeclaredBindingH\x03R\bdeclared\x88\x01\x01B\v\n" + "\t_frontendB\n" + "\n" + "\b_backendB\x10\n" + - "\x0e_tombstoned_at\"|\n" + - "\x1cSolutionFrontendRegistration\x12'\n" + - "\bmanifest\x18\x01 \x01(\tB\v\xbaH\br\x06\x10\x02\x18\x80\x80\x10R\bmanifest\x123\n" + - "\x10contract_version\x18\x02 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x01R\x0fcontractVersion\"\xae\x01\n" + - "\x1bSolutionBackendRegistration\x12)\n" + - "\bupstream\x18\x01 \x01(\tB\r\xbaH\n" + - "r\b\x10\x01\x18\x80\x10\x88\x01\x01R\bupstream\x12/\n" + - "\rservice_alias\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x01R\fserviceAlias\x123\n" + - "\x10contract_version\x18\x03 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x01R\x0fcontractVersion\"\xc5\x03\n" + - "\x1ePutSolutionRegistrationRequest\x12O\n" + - "\vsolution_id\x18\x01 \x01(\tB.\xbaH+r)\x10\x01\x18\x80\x012\"^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$R\n" + - "solutionId\x12(\n" + - "\tpublisher\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x02R\tpublisher\x129\n" + - "\x11expected_revision\x18\x03 \x01(\x03B\a\xbaH\x04\"\x02 \x00H\x01R\x10expectedRevision\x88\x01\x01\x12/\n" + - "\rlease_seconds\x18\x04 \x01(\rB\n" + - "\xbaH\a*\x05\x18\x90\x1c(\x1eR\fleaseSeconds\x12L\n" + - "\bfrontend\x18\x05 \x01(\v2..saas.accounts.v1.SolutionFrontendRegistrationH\x00R\bfrontend\x12I\n" + - "\abackend\x18\x06 \x01(\v2-.saas.accounts.v1.SolutionBackendRegistrationH\x00R\abackendB\r\n" + - "\x04half\x12\x05\xbaH\x02\b\x01B\x14\n" + - "\x12_expected_revision\"\xa1\x01\n" + - "!DeleteSolutionRegistrationRequest\x12+\n" + - "\vsolution_id\x18\x01 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x01R\n" + - "solutionId\x129\n" + - "\x11expected_revision\x18\x02 \x01(\x03B\a\xbaH\x04\"\x02 \x00H\x00R\x10expectedRevision\x88\x01\x01B\x14\n" + - "\x12_expected_revision\"Q\n" + + "\x0e_tombstoned_atB\v\n" + + "\t_declaredJ\x04\b\t\x10\n" + + "R\x10runtime_boundary\"Q\n" + " ListSolutionRegistrationsRequest\x12-\n" + "\x12include_tombstoned\x18\x01 \x01(\bR\x11includeTombstoned\"\x9e\x01\n" + "!ListSolutionRegistrationsResponse\x12L\n" + "\rregistrations\x18\x01 \x03(\v2&.saas.accounts.v1.SolutionRegistrationR\rregistrations\x12+\n" + - "\x11registry_revision\x18\x02 \x01(\x03R\x10registryRevision*\xa3\x02\n" + + "\x11registry_revision\x18\x02 \x01(\x03R\x10registryRevision\"\xcb\x01\n" + + "\x17SolutionDeclaredBinding\x12\x1d\n" + + "\n" + + "binding_id\x18\x01 \x01(\tR\tbindingId\x12\x1e\n" + + "\n" + + "generation\x18\x02 \x01(\x04R\n" + + "generation\x12\x18\n" + + "\arelease\x18\x03 \x01(\tR\arelease\x12\x1b\n" + + "\ttarget_id\x18\x04 \x01(\tR\btargetId\x12:\n" + + "\x04kind\x18\x05 \x01(\x0e2&.saas.accounts.v1.SolutionDeclaredKindR\x04kind\"\x9f\x01\n" + + "\x1dSolutionHostBindingGeneration\x12\x1e\n" + + "\n" + + "generation\x18\x01 \x01(\x04R\n" + + "generation\x12\x16\n" + + "\x06digest\x18\x02 \x01(\tR\x06digest\x12\x1a\n" + + "\bdocument\x18\x03 \x01(\tR\bdocument\x12*\n" + + "\x02at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x02at\"\xe4\x01\n" + + "$SolutionHostBindingAppliedGeneration\x12O\n" + + "\n" + + "generation\x18\x01 \x01(\v2/.saas.accounts.v1.SolutionHostBindingGenerationR\n" + + "generation\x12\x18\n" + + "\aremoved\x18\x02 \x01(\bR\aremoved\x12\x16\n" + + "\x06routes\x18\x03 \x03(\tR\x06routes\x12\x1f\n" + + "\vsolution_id\x18\x04 \x01(\tR\n" + + "solutionId\x12\x18\n" + + "\arelease\x18\x05 \x01(\tR\arelease\"\x93\x05\n" + + "\x18SolutionHostBindingState\x12\x1d\n" + + "\n" + + "binding_id\x18\x01 \x01(\tR\tbindingId\x12'\n" + + "\x0fhost_coordinate\x18\x02 \x01(\tR\x0ehostCoordinate\x12%\n" + + "\x0ehost_component\x18\x03 \x01(\tR\rhostComponent\x12N\n" + + "\adesired\x18\x04 \x01(\v2/.saas.accounts.v1.SolutionHostBindingGenerationH\x00R\adesired\x88\x01\x01\x12U\n" + + "\aapplied\x18\x05 \x01(\v26.saas.accounts.v1.SolutionHostBindingAppliedGenerationH\x01R\aapplied\x88\x01\x01\x12-\n" + + "\x12pending_generation\x18\x06 \x01(\x04R\x11pendingGeneration\x12%\n" + + "\x0epending_reason\x18\a \x01(\tR\rpendingReason\x12D\n" + + "\rpending_since\x18\b \x01(\v2\x1a.google.protobuf.TimestampH\x02R\fpendingSince\x88\x01\x01\x12O\n" + + "\fregistration\x18\t \x01(\v2&.saas.accounts.v1.SolutionRegistrationH\x03R\fregistration\x88\x01\x01\x129\n" + + "\n" + + "updated_at\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAtB\n" + + "\n" + + "\b_desiredB\n" + + "\n" + + "\b_appliedB\x10\n" + + "\x0e_pending_sinceB\x0f\n" + + "\r_registration\"!\n" + + "\x1fListSolutionHostBindingsRequest\"j\n" + + " ListSolutionHostBindingsResponse\x12F\n" + + "\bbindings\x18\x01 \x03(\v2*.saas.accounts.v1.SolutionHostBindingStateR\bbindings*\xa5\x02\n" + "\x1aSolutionRegistrationStatus\x12,\n" + "(SOLUTION_REGISTRATION_STATUS_UNSPECIFIED\x10\x00\x12'\n" + "#SOLUTION_REGISTRATION_STATUS_ACTIVE\x10\x01\x12(\n" + - "$SOLUTION_REGISTRATION_STATUS_PENDING\x10\x02\x12(\n" + - "$SOLUTION_REGISTRATION_STATUS_EXPIRED\x10\x03\x12-\n" + + "$SOLUTION_REGISTRATION_STATUS_PENDING\x10\x02\x12-\n" + ")SOLUTION_REGISTRATION_STATUS_INCOMPATIBLE\x10\x04\x12+\n" + - "'SOLUTION_REGISTRATION_STATUS_TOMBSTONED\x10\x05B\xd3\x01\n" + + "'SOLUTION_REGISTRATION_STATUS_TOMBSTONED\x10\x05\"\x04\b\x03\x10\x03*$SOLUTION_REGISTRATION_STATUS_EXPIRED*\x86\x01\n" + + "\x14SolutionDeclaredKind\x12&\n" + + "\"SOLUTION_DECLARED_KIND_UNSPECIFIED\x10\x00\x12#\n" + + "\x1fSOLUTION_DECLARED_KIND_SOLUTION\x10\x01\x12!\n" + + "\x1dSOLUTION_DECLARED_KIND_MODULE\x10\x02B\xd3\x01\n" + "\x14com.saas.accounts.v1B\x15SolutionRegistryProtoP\x01ZBgithub.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" var ( @@ -857,37 +1076,46 @@ func file_saas_accounts_v1_solution_registry_proto_rawDescGZIP() []byte { return file_saas_accounts_v1_solution_registry_proto_rawDescData } -var file_saas_accounts_v1_solution_registry_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_saas_accounts_v1_solution_registry_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_saas_accounts_v1_solution_registry_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_saas_accounts_v1_solution_registry_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_saas_accounts_v1_solution_registry_proto_goTypes = []any{ - (SolutionRegistrationStatus)(0), // 0: saas.accounts.v1.SolutionRegistrationStatus - (*SolutionFrontendBinding)(nil), // 1: saas.accounts.v1.SolutionFrontendBinding - (*SolutionBackendBinding)(nil), // 2: saas.accounts.v1.SolutionBackendBinding - (*SolutionRegistration)(nil), // 3: saas.accounts.v1.SolutionRegistration - (*SolutionFrontendRegistration)(nil), // 4: saas.accounts.v1.SolutionFrontendRegistration - (*SolutionBackendRegistration)(nil), // 5: saas.accounts.v1.SolutionBackendRegistration - (*PutSolutionRegistrationRequest)(nil), // 6: saas.accounts.v1.PutSolutionRegistrationRequest - (*DeleteSolutionRegistrationRequest)(nil), // 7: saas.accounts.v1.DeleteSolutionRegistrationRequest - (*ListSolutionRegistrationsRequest)(nil), // 8: saas.accounts.v1.ListSolutionRegistrationsRequest - (*ListSolutionRegistrationsResponse)(nil), // 9: saas.accounts.v1.ListSolutionRegistrationsResponse - (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp + (SolutionRegistrationStatus)(0), // 0: saas.accounts.v1.SolutionRegistrationStatus + (SolutionDeclaredKind)(0), // 1: saas.accounts.v1.SolutionDeclaredKind + (*SolutionFrontendBinding)(nil), // 2: saas.accounts.v1.SolutionFrontendBinding + (*SolutionBackendBinding)(nil), // 3: saas.accounts.v1.SolutionBackendBinding + (*SolutionRegistration)(nil), // 4: saas.accounts.v1.SolutionRegistration + (*ListSolutionRegistrationsRequest)(nil), // 5: saas.accounts.v1.ListSolutionRegistrationsRequest + (*ListSolutionRegistrationsResponse)(nil), // 6: saas.accounts.v1.ListSolutionRegistrationsResponse + (*SolutionDeclaredBinding)(nil), // 7: saas.accounts.v1.SolutionDeclaredBinding + (*SolutionHostBindingGeneration)(nil), // 8: saas.accounts.v1.SolutionHostBindingGeneration + (*SolutionHostBindingAppliedGeneration)(nil), // 9: saas.accounts.v1.SolutionHostBindingAppliedGeneration + (*SolutionHostBindingState)(nil), // 10: saas.accounts.v1.SolutionHostBindingState + (*ListSolutionHostBindingsRequest)(nil), // 11: saas.accounts.v1.ListSolutionHostBindingsRequest + (*ListSolutionHostBindingsResponse)(nil), // 12: saas.accounts.v1.ListSolutionHostBindingsResponse + (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp } var file_saas_accounts_v1_solution_registry_proto_depIdxs = []int32{ - 10, // 0: saas.accounts.v1.SolutionFrontendBinding.lease_expires_at:type_name -> google.protobuf.Timestamp - 10, // 1: saas.accounts.v1.SolutionBackendBinding.lease_expires_at:type_name -> google.protobuf.Timestamp - 0, // 2: saas.accounts.v1.SolutionRegistration.status:type_name -> saas.accounts.v1.SolutionRegistrationStatus - 1, // 3: saas.accounts.v1.SolutionRegistration.frontend:type_name -> saas.accounts.v1.SolutionFrontendBinding - 2, // 4: saas.accounts.v1.SolutionRegistration.backend:type_name -> saas.accounts.v1.SolutionBackendBinding - 10, // 5: saas.accounts.v1.SolutionRegistration.updated_at:type_name -> google.protobuf.Timestamp - 10, // 6: saas.accounts.v1.SolutionRegistration.tombstoned_at:type_name -> google.protobuf.Timestamp - 4, // 7: saas.accounts.v1.PutSolutionRegistrationRequest.frontend:type_name -> saas.accounts.v1.SolutionFrontendRegistration - 5, // 8: saas.accounts.v1.PutSolutionRegistrationRequest.backend:type_name -> saas.accounts.v1.SolutionBackendRegistration - 3, // 9: saas.accounts.v1.ListSolutionRegistrationsResponse.registrations:type_name -> saas.accounts.v1.SolutionRegistration - 10, // [10:10] is the sub-list for method output_type - 10, // [10:10] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 0, // 0: saas.accounts.v1.SolutionRegistration.status:type_name -> saas.accounts.v1.SolutionRegistrationStatus + 2, // 1: saas.accounts.v1.SolutionRegistration.frontend:type_name -> saas.accounts.v1.SolutionFrontendBinding + 3, // 2: saas.accounts.v1.SolutionRegistration.backend:type_name -> saas.accounts.v1.SolutionBackendBinding + 13, // 3: saas.accounts.v1.SolutionRegistration.updated_at:type_name -> google.protobuf.Timestamp + 13, // 4: saas.accounts.v1.SolutionRegistration.tombstoned_at:type_name -> google.protobuf.Timestamp + 7, // 5: saas.accounts.v1.SolutionRegistration.declared:type_name -> saas.accounts.v1.SolutionDeclaredBinding + 4, // 6: saas.accounts.v1.ListSolutionRegistrationsResponse.registrations:type_name -> saas.accounts.v1.SolutionRegistration + 1, // 7: saas.accounts.v1.SolutionDeclaredBinding.kind:type_name -> saas.accounts.v1.SolutionDeclaredKind + 13, // 8: saas.accounts.v1.SolutionHostBindingGeneration.at:type_name -> google.protobuf.Timestamp + 8, // 9: saas.accounts.v1.SolutionHostBindingAppliedGeneration.generation:type_name -> saas.accounts.v1.SolutionHostBindingGeneration + 8, // 10: saas.accounts.v1.SolutionHostBindingState.desired:type_name -> saas.accounts.v1.SolutionHostBindingGeneration + 9, // 11: saas.accounts.v1.SolutionHostBindingState.applied:type_name -> saas.accounts.v1.SolutionHostBindingAppliedGeneration + 13, // 12: saas.accounts.v1.SolutionHostBindingState.pending_since:type_name -> google.protobuf.Timestamp + 4, // 13: saas.accounts.v1.SolutionHostBindingState.registration:type_name -> saas.accounts.v1.SolutionRegistration + 13, // 14: saas.accounts.v1.SolutionHostBindingState.updated_at:type_name -> google.protobuf.Timestamp + 10, // 15: saas.accounts.v1.ListSolutionHostBindingsResponse.bindings:type_name -> saas.accounts.v1.SolutionHostBindingState + 16, // [16:16] is the sub-list for method output_type + 16, // [16:16] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name } func init() { file_saas_accounts_v1_solution_registry_proto_init() } @@ -896,18 +1124,14 @@ func file_saas_accounts_v1_solution_registry_proto_init() { return } file_saas_accounts_v1_solution_registry_proto_msgTypes[2].OneofWrappers = []any{} - file_saas_accounts_v1_solution_registry_proto_msgTypes[5].OneofWrappers = []any{ - (*PutSolutionRegistrationRequest_Frontend)(nil), - (*PutSolutionRegistrationRequest_Backend)(nil), - } - file_saas_accounts_v1_solution_registry_proto_msgTypes[6].OneofWrappers = []any{} + file_saas_accounts_v1_solution_registry_proto_msgTypes[8].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_solution_registry_proto_rawDesc), len(file_saas_accounts_v1_solution_registry_proto_rawDesc)), - NumEnums: 1, - NumMessages: 9, + NumEnums: 2, + NumMessages: 11, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/saas/accounts/v1/solution_registry_service.pb.go b/gen/saas/accounts/v1/solution_registry_service.pb.go index 78585f2..7363473 100644 --- a/gen/saas/accounts/v1/solution_registry_service.pb.go +++ b/gen/saas/accounts/v1/solution_registry_service.pb.go @@ -25,31 +25,25 @@ var File_saas_accounts_v1_solution_registry_service_proto protoreflect.FileDescr const file_saas_accounts_v1_solution_registry_service_proto_rawDesc = "" + "\n" + - "0saas/accounts/v1/solution_registry_service.proto\x12\x10saas.accounts.v1\x1a(saas/accounts/v1/solution_registry.proto\x1a\x1csaas/policy/v1/options.proto2\xa8\x04\n" + - "\x17SolutionRegistryService\x12\xb1\x01\n" + - "\x17PutSolutionRegistration\x120.saas.accounts.v1.PutSolutionRegistrationRequest\x1a&.saas.accounts.v1.SolutionRegistration\"<\xc2\xf3\x188\b\x03\x10\x010\x01:&\n" + - "\"saas.solution.registration_updated\x10\x02@\x01H\aP\x03X\x03`\x01\x12\xb7\x01\n" + - "\x1aDeleteSolutionRegistration\x123.saas.accounts.v1.DeleteSolutionRegistrationRequest\x1a&.saas.accounts.v1.SolutionRegistration\"<\xc2\xf3\x188\b\x03\x10\x010\x01:&\n" + - "\"saas.solution.registration_deleted\x10\x02@\x01H\aP\x03X\x03`\x01\x12\x9e\x01\n" + + "0saas/accounts/v1/solution_registry_service.proto\x12\x10saas.accounts.v1\x1a(saas/accounts/v1/solution_registry.proto\x1a\x1csaas/policy/v1/options.proto2\xd8\x02\n" + + "\x17SolutionRegistryService\x12\x9b\x01\n" + + "\x18ListSolutionHostBindings\x121.saas.accounts.v1.ListSolutionHostBindingsRequest\x1a2.saas.accounts.v1.ListSolutionHostBindingsResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\x9e\x01\n" + "\x19ListSolutionRegistrations\x122.saas.accounts.v1.ListSolutionRegistrationsRequest\x1a3.saas.accounts.v1.ListSolutionRegistrationsResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01B\xda\x01\n" + "\x14com.saas.accounts.v1B\x1cSolutionRegistryServiceProtoP\x01ZBgithub.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" var file_saas_accounts_v1_solution_registry_service_proto_goTypes = []any{ - (*PutSolutionRegistrationRequest)(nil), // 0: saas.accounts.v1.PutSolutionRegistrationRequest - (*DeleteSolutionRegistrationRequest)(nil), // 1: saas.accounts.v1.DeleteSolutionRegistrationRequest - (*ListSolutionRegistrationsRequest)(nil), // 2: saas.accounts.v1.ListSolutionRegistrationsRequest - (*SolutionRegistration)(nil), // 3: saas.accounts.v1.SolutionRegistration - (*ListSolutionRegistrationsResponse)(nil), // 4: saas.accounts.v1.ListSolutionRegistrationsResponse + (*ListSolutionHostBindingsRequest)(nil), // 0: saas.accounts.v1.ListSolutionHostBindingsRequest + (*ListSolutionRegistrationsRequest)(nil), // 1: saas.accounts.v1.ListSolutionRegistrationsRequest + (*ListSolutionHostBindingsResponse)(nil), // 2: saas.accounts.v1.ListSolutionHostBindingsResponse + (*ListSolutionRegistrationsResponse)(nil), // 3: saas.accounts.v1.ListSolutionRegistrationsResponse } var file_saas_accounts_v1_solution_registry_service_proto_depIdxs = []int32{ - 0, // 0: saas.accounts.v1.SolutionRegistryService.PutSolutionRegistration:input_type -> saas.accounts.v1.PutSolutionRegistrationRequest - 1, // 1: saas.accounts.v1.SolutionRegistryService.DeleteSolutionRegistration:input_type -> saas.accounts.v1.DeleteSolutionRegistrationRequest - 2, // 2: saas.accounts.v1.SolutionRegistryService.ListSolutionRegistrations:input_type -> saas.accounts.v1.ListSolutionRegistrationsRequest - 3, // 3: saas.accounts.v1.SolutionRegistryService.PutSolutionRegistration:output_type -> saas.accounts.v1.SolutionRegistration - 3, // 4: saas.accounts.v1.SolutionRegistryService.DeleteSolutionRegistration:output_type -> saas.accounts.v1.SolutionRegistration - 4, // 5: saas.accounts.v1.SolutionRegistryService.ListSolutionRegistrations:output_type -> saas.accounts.v1.ListSolutionRegistrationsResponse - 3, // [3:6] is the sub-list for method output_type - 0, // [0:3] is the sub-list for method input_type + 0, // 0: saas.accounts.v1.SolutionRegistryService.ListSolutionHostBindings:input_type -> saas.accounts.v1.ListSolutionHostBindingsRequest + 1, // 1: saas.accounts.v1.SolutionRegistryService.ListSolutionRegistrations:input_type -> saas.accounts.v1.ListSolutionRegistrationsRequest + 2, // 2: saas.accounts.v1.SolutionRegistryService.ListSolutionHostBindings:output_type -> saas.accounts.v1.ListSolutionHostBindingsResponse + 3, // 3: saas.accounts.v1.SolutionRegistryService.ListSolutionRegistrations:output_type -> saas.accounts.v1.ListSolutionRegistrationsResponse + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name diff --git a/go.mod b/go.mod index 33c701a..6a023e1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.27.0 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 connectrpc.com/connect v1.21.0 - github.com/codefly-dev/core v0.3.32 + github.com/codefly-dev/core v0.14.0 github.com/codefly-dev/sdk-go/workcontext v0.0.0-20260927024316-b3fb40d43099 github.com/google/uuid v1.6.0 golang.org/x/tools v0.51.0 diff --git a/go.sum b/go.sum index a7c8940..08fd755 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ connectrpc.com/connect v1.21.0 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs= connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/codefly-dev/core v0.3.32 h1:zVgGAljUERRjHe5ONhI2uBuQg0eayZzkFSm0murp5NI= -github.com/codefly-dev/core v0.3.32/go.mod h1:f5LY9gRu5lpBQ0tNKujP0q6MF9K8fUnbYvItuHaUWlY= +github.com/codefly-dev/core v0.14.0 h1:rpLFxeHSf2PrbxsRSp/NcZt8htMRM38JV2Oc8RKievU= +github.com/codefly-dev/core v0.14.0/go.mod h1:rp4YZkCJEOufliWQAgK3sv8YlO7Xcva7+cN7hIlAR0I= github.com/codefly-dev/sdk-go/workcontext v0.0.0-20260927024316-b3fb40d43099 h1:MEzUboHy6HzBex1VoAOo3JqBgjTjSYs3jQGyqIwHOhQ= github.com/codefly-dev/sdk-go/workcontext v0.0.0-20260927024316-b3fb40d43099/go.mod h1:BbqUHTJEnmH3oxYblfjS3Oh9AUw14YaEFVV4Y19yQPY= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= diff --git a/moduleauthority/host_fake_test.go b/moduleauthority/host_fake_test.go index 4d54fb2..d298a88 100644 --- a/moduleauthority/host_fake_test.go +++ b/moduleauthority/host_fake_test.go @@ -48,6 +48,7 @@ const ( testTenant = "11111111-1111-4111-8111-111111111111" testPrincipal = "00000000-0000-4000-8000-00000000beef" testOwner = "44444444-4444-4444-8444-444444444444" + testBindingID = "binding-example-1" testDelegation = "33333333-3333-4333-8333-333333333333" exampleSource = "55555555-5555-4555-8555-555555555555" ) @@ -352,14 +353,20 @@ func (a *fakeAuthority) handle(_ any, stream grpc.ServerStream) error { if call.installation.ParentWorkContextToken == "denied.token" { return status.Error(codes.PermissionDenied, "current authority denied") } - out := &v1.ModuleCurrentInstallationResponse{InstallationId: call.installation.InstallationId, TenantId: testTenant, SolutionIdentifier: "acme/example"} + out := &v1.ModuleCurrentInstallationResponse{InstallationId: call.installation.InstallationId, TenantId: testTenant, TargetId: testOwner, BindingId: testBindingID} switch call.installation.ParentWorkContextToken { case "wrong-id.token": out.InstallationId = testOwner case "missing-tenant.token": out.TenantId = "" - case "empty-source.token": - out.SolutionIdentifier = "" + case "empty-target.token": + out.TargetId = "" + case "invalid-target.token": + out.TargetId = "invalid" + case "empty-binding.token": + out.BindingId = "" + case "invalid-binding.token": + out.BindingId = " " } return stream.SendMsg(out) } diff --git a/moduleauthority/installation.go b/moduleauthority/installation.go index 2b8d9e3..03065a2 100644 --- a/moduleauthority/installation.go +++ b/moduleauthority/installation.go @@ -24,9 +24,10 @@ type CurrentInstallationRequest struct { // CurrentInstallation is a current active, non-revoked identity observation. // It is not executable approval, a grant, or a durable proof of liveness. type CurrentInstallation struct { - InstallationID string - TenantID string - SolutionIdentifier string + InstallationID string + TenantID string + TargetID string + BindingID string } // GetCurrentInstallation preserves the host's organization-member metadata read. @@ -40,8 +41,8 @@ func (c *Client) GetCurrentInstallation(ctx context.Context, req CurrentInstalla if err != nil { return CurrentInstallation{}, fmt.Errorf("module authority: current installation: %w", err) } - if out == nil || out.InstallationId != req.InstallationID || !artifactUUID.MatchString(out.TenantId) || strings.TrimSpace(out.SolutionIdentifier) == "" || !utf8.ValidString(out.SolutionIdentifier) { + if out == nil || out.InstallationId != req.InstallationID || !artifactUUID.MatchString(out.TenantId) || !artifactUUID.MatchString(out.TargetId) || strings.TrimSpace(out.BindingId) == "" || !utf8.ValidString(out.BindingId) { return CurrentInstallation{}, ErrInvalidInstallation } - return CurrentInstallation{InstallationID: out.InstallationId, TenantID: out.TenantId, SolutionIdentifier: out.SolutionIdentifier}, nil + return CurrentInstallation{InstallationID: out.InstallationId, TenantID: out.TenantId, TargetID: out.TargetId, BindingID: out.BindingId}, nil } diff --git a/moduleauthority/installation_test.go b/moduleauthority/installation_test.go index 95a7d13..c906b41 100644 --- a/moduleauthority/installation_test.go +++ b/moduleauthority/installation_test.go @@ -14,7 +14,7 @@ func TestCurrentInstallationUsesIndependentCredentialsAndExactSelector(t *testin if err != nil { t.Fatal(err) } - if out.InstallationID != exampleSource || out.TenantID != testTenant || out.SolutionIdentifier != "acme/example" { + if out.InstallationID != exampleSource || out.TenantID != testTenant || out.TargetID != testOwner || out.BindingID != testBindingID { t.Fatal("wrong identity", out) } calls := h.authority.snapshot() @@ -37,7 +37,7 @@ func TestCurrentInstallationUsesIndependentCredentialsAndExactSelector(t *testin func TestCurrentInstallationRefusesBadResponsesAndDoesNotFallback(t *testing.T) { h := newHost(t) client := h.client(t, moduleCredentials) - for _, parent := range []string{"wrong-id.token", "missing-tenant.token", "empty-source.token"} { + for _, parent := range []string{"wrong-id.token", "missing-tenant.token", "empty-target.token", "invalid-target.token", "empty-binding.token", "invalid-binding.token"} { _, err := client.GetCurrentInstallation(t.Context(), CurrentInstallationRequest{Parent: token(t, parent), InstallationID: exampleSource}) if !errors.Is(err, ErrInvalidInstallation) { t.Fatalf("%s: %v", parent, err) From ac9c580284ea805f6db447affc7ed834dbc0b59e Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:50:20 -0400 Subject: [PATCH 04/12] fix(datasource): preserve effect outcomes and recovery identity --- .claude/skills/refresh-generated-api/SKILL.md | 8 +- CHANGELOG.md | 20 +- README.md | 68 +++++-- datasource/doc.go | 8 +- datasource/doc_test.go | 6 +- datasource/errors.go | 67 +++++-- datasource/errors_test.go | 30 ++- datasource/invoke_options.go | 11 +- datasource/json.go | 3 + datasource/json_test.go | 9 + datasource/operations.go | 69 +++++-- datasource/operations_test.go | 178 +++++++++++++++++- datasource/types.go | 12 +- go.mod | 2 +- go.sum | 4 +- 15 files changed, 421 insertions(+), 74 deletions(-) diff --git a/.claude/skills/refresh-generated-api/SKILL.md b/.claude/skills/refresh-generated-api/SKILL.md index 6e6aee8..d791682 100644 --- a/.claude/skills/refresh-generated-api/SKILL.md +++ b/.claude/skills/refresh-generated-api/SKILL.md @@ -45,9 +45,11 @@ Three properties of that template matter: - **The inputs select the complete host-owned `saas/` schema tree.** The vendored `codefly/` closure is import-only: its `go_package_prefix` resolves - to `github.com/codefly-dev/core/generated/go`, and the Go dependency must - match the host's vendored contract version. Generating another copy of Core's - descriptors here creates duplicate protobuf registrations at initialization. + to `github.com/codefly-dev/core/generated/go`. Require a compatible Core + version that provides those packages; validate the dependency floor against + SDK and consumer builds instead of copying the host's dependency generation. + Generating another copy of Core's descriptors here creates duplicate protobuf + registrations at initialization. - **`clean: true` wipes `gen/` first.** So a proto deleted upstream disappears here rather than lingering — which is how `AuditExportService` left in 0.1.0. diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f0223..43a5a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,20 @@ Python facade. Add operations, JSON output, receipts, UUIDv7 effect IDs and per-call deadlines; the host alone owns replay and provider dispatch. - Add `ErrNotPermitted`, `ErrUnknownOperation`, `InputError`, `RateLimited`, - `ProviderRefused` and `ErrOutcomeUnknown`. Unknown retains the effect ID and - never exposes a retryable transport error. Host messages remain unchanged. + `ProviderRefused`, `OperationRefused`, `ErrEffectReused`, `ErrEffectNotFound` + and `ErrOutcomeUnknown`. The host's `SOURCE_OPERATION_OUTCOME_UNKNOWN` + reason retains the effect ID and never exposes a retryable transport error. + `*OutcomeUnknown.Cause()` provides the original diagnostic explicitly for + logging and metrics without enabling generic transport retries. + Only `SOURCE_PROVIDER_REFUSED` maps to a provider refusal; its status remains + unavailable at this host ref. Host messages remain unchanged. +- Lookup misses return the original ID with `NOT_ATTEMPTED`; callers may + explicitly reinvoke with the same ID. Committed receipts include `Output`, + so recovery can retrieve output without another invoke. Receipt statuses are + `COMMITTED`, `UNKNOWN` and `NOT_ATTEMPTED`. +- Validate effect IDs and reject an already-canceled context or expired + deadline before dispatch. Add operation descriptions and host-computed + digests to declaration/list round-tripping. - Document the call and retry contracts in `README.md` and package examples. - Invoke payloads use `input_json` / `output_json` strings containing exactly one JSON object, preserving numeric precision. Declaration schemas retain @@ -22,8 +34,8 @@ waits for the host tag and regeneration from its commit. - Re-export the OAuth2 grant type/constants reachable from source reads. The generation template emits all host-owned `saas/` schemas and imports Core's - Runnable descriptors from Core v0.14.0, matching the host's vendor closure; - plugin versions are unchanged. + Runnable descriptors from Core v0.3.41, which supplies the required package + while preserving consumer compatibility; plugin versions are unchanged. ### Removed / changed (breaking, inherited host contract) diff --git a/README.md b/README.md index 73b8f60..900b13a 100644 --- a/README.md +++ b/README.md @@ -60,31 +60,47 @@ The SDK surfaces are: payloads never pass through protobuf `Struct`; declaration schemas still use the host's `Struct` fields. `WithDeadline(time.Time)` bounds only this call; an earlier context deadline still wins. `WithEffectID(id)` - supplies an effect ID; when absent or empty, the SDK mints a UUIDv7. Once a - call is attempted, even an error returns a non-nil `Result` containing that + supplies an effect ID of 1–128 UTF-8 bytes without control bytes or surrounding + spaces (which HTTP would trim); interior spaces and Unicode are supported; + when omitted, the SDK mints a UUIDv7. Invalid IDs return `*InputError` before + dispatch. An already-canceled context or expired deadline returns the plain + context error before dispatch. Once a call is attempted, even an error + returns a non-nil `Result` containing that ID in `Receipt.EffectID`. Persist a caller-owned ID **before** invoking a mutation if it must survive a process crash. - Repeating the same effect ID and input asks the **host** to replay its - receipt. Different input under the same ID is refused. Every explicit + Repeating the same effect ID and **byte-identical `input_json`** asks the + **host** to replay its receipt. Persist the encoded input with the ID and + reuse it as `json.RawMessage`: rebuilding an equivalent object with a + different field order can be refused. Every valid explicit `Invoke` reaches the host: the SDK has no retry loop, deduplication or receipt cache. Do not change the ID to recover an uncertain mutation. - `Lookup(ctx, orgID, sourceID, effectID)` returns the receipt without - invoking the provider. The shared SDK signature keeps `orgID` and + `Lookup(ctx, orgID, sourceID, effectID)` returns the receipt and its committed + `Output` without invoking the provider. The shared SDK signature keeps `orgID` and `sourceID`, but the wire request sends **only `effect_id`**: the host derives the tenant from the gateway's Work Context and recovers the source from the stored effect binding. These two arguments cannot select or override lookup - authority. An unknown receipt is returned alongside + authority. A lookup miss returns `ErrEffectNotFound` and a receipt carrying + the same ID with status `NOT_ATTEMPTED`; an explicit reinvoke with that ID is + safe, and the SDK never invokes automatically. At the recorded host ref, + NotFound can also mean the declaration disappeared after an attempt; it is + not proof the provider never acted. Keep the original ID in either case. + An unknown receipt is returned alongside `ErrOutcomeUnknown`; check it with `errors.Is`. **Unknown is an outcome, not evidence of failure or permission to repeat an effect.** Invoke also - reports transport loss or deadline expiry conservatively as unknown, since - it cannot prove the operation was read-only. That outcome does not unwrap + reports transport loss or deadline expiry after dispatch conservatively as + unknown, since it cannot prove the operation was read-only. That outcome does not unwrap to a retryable Connect transport error. An unavailable lookup also returns an unknown receipt: it cannot establish what happened to the effect. Use a fresh bounded context to look up an effect after the original context has expired. + For logging and metrics, `errors.As(err, &outcome)` with + `var outcome *datasource.OutcomeUnknown` exposes `outcome.Cause()` and its + original Connect code. This explicit diagnostic access does not put a + retryable transport error in the outcome's unwrap chain. + `DeclareOperations(ctx, orgID, sourceID, []datasource.Operation)` replaces the source's declaration set atomically under the host's administrator check. `ListOperations(ctx, orgID, sourceID)` reads it. For example, an API @@ -93,13 +109,16 @@ The SDK surfaces are: ```go err := ds.DeclareOperations(ctx, orgID, sourceID, []datasource.Operation{{ Name: "list_invoices", Method: "GET", Path: "/invoices", Query: []string{"limit"}, + Description: "List available invoices", Input: json.RawMessage(`{"type":"object","properties":{"limit":{"type":"integer"}},"additionalProperties":false}`), Output: json.RawMessage(`{"type":"object"}`), Effect: datasource.EffectReadOnly, MaxOutputBytes: 65536, }}) ``` - The host admits the schemas, route, effect and output cap. Invocation never + `ListOperations` includes each operation's description and host-computed + `Digest`; declarations ignore a caller-supplied digest. The host admits the + schemas, route, effect and output cap. Invocation never accepts a provider URL or credential. The gateway's HTTP client carries the person's Work Context unchanged; this facade adds only `x-codefly-effect-id` to the normal Connect request, matching the request's @@ -108,19 +127,32 @@ The SDK surfaces are: | Host response | Go result | | --- | --- | | PermissionDenied | `ErrNotPermitted` (`errors.Is`) | - | NotFound | `ErrUnknownOperation` (`errors.Is`) | - | InvalidArgument + BadRequest | `*InputError`, with the first field violation's JSON `Pointer` | + | Invoke NotFound | `ErrUnknownOperation` (`errors.Is`) | + | Lookup NotFound | receipt plus `ErrEffectNotFound` (`errors.Is`); keep the ID for an explicit reinvoke | + | InvalidArgument | `*InputError`; `Pointer` is populated only from a structured BadRequest field violation | | ResourceExhausted + DATASOURCE_RATE_LIMITED | `*RateLimited`, with `ResetAt` from `reset_at` or `RetryInfo` | - | FailedPrecondition | `*ProviderRefused`, with the host's `provider_status` as `Status` (zero if absent) | + | FailedPrecondition + SOURCE_OPERATION_OUTCOME_UNKNOWN | receipt plus `ErrOutcomeUnknown` (`errors.Is`); the mutation may have happened | + | FailedPrecondition + SOURCE_PROVIDER_REFUSED | `*ProviderRefused` | + | FailedPrecondition + SOURCE_EFFECT_REUSED | `ErrEffectReused` (`errors.Is`) | + | Other FailedPrecondition | `*OperationRefused`, with the structured `Reason` or an empty string | | Ambiguous Invoke / unknown receipt | receipt plus `ErrOutcomeUnknown` (`errors.Is`) | + The recorded host ref does not emit `BadRequest.FieldViolation` or provider + status metadata on errors: `InputError.Pointer` is empty and + `ProviderRefused.Status` stays zero. Those details belong to the host's + [#1049 contract](https://github.com/codefly-dev/module-saas-starter/issues/1049). + Its receipt guard also emits detail-free precondition refusals, represented + as `OperationRefused` with an empty reason. A precondition refusal does not + establish the outcome of an earlier attempt under that ID. + Other errors retain their Connect identity. Host diagnostic messages are preserved, including credential-looking text: **the host is the disclosure guard**. The SDK neither redacts messages nor treats their prose as status metadata. These names mirror the Python facade's `invoke`, `lookup`, `declare_operations`, `list_operations`, `Operation`, `Result`, `Receipt`, - `NotPermitted`, `UnknownOperation`, `InputError`, `RateLimited`, - `ProviderRefused` and `OutcomeUnknown`. + `NotPermitted`, `UnknownOperation`, `EffectNotFound`, `EffectReused`, + `InputError`, `RateLimited`, `ProviderRefused`, `OperationRefused` and + `OutcomeUnknown`. This call surface uses the four host RPCs from [module-saas-starter#1052](https://github.com/codefly-dev/module-saas-starter/pull/1052). @@ -298,8 +330,10 @@ The template generates the complete host-owned `saas/` schema tree. Its vendored `codefly/` import closure is compiled for Runnable annotations, with imports resolved to `github.com/codefly-dev/core/generated/go`; Core owns those Go descriptors. Emitting a second copy in this SDK would conflict at -protobuf initialization. The Core dependency matches the host's vendored -contract version, and the generation plugin versions remain pinned. +protobuf initialization. Core v0.3.41 supplies the required Runnable package; +the SDK need not adopt the host's entire dependency generation. Verify the +compatible dependency floor with SDK and consumer builds when refreshing it. +The generation plugin versions remain pinned. > Wiring this regen into module-saas-starter's release (so a saas tag publishes a > matching SDK tag) is tracked in the solutions EPIC (obin-ai/lodestar#53, item 4). diff --git a/datasource/doc.go b/datasource/doc.go index aeba048..6e71731 100644 --- a/datasource/doc.go +++ b/datasource/doc.go @@ -17,11 +17,15 @@ // Invoke mints a UUIDv7 when WithEffectID is absent. Once a call is attempted, // Result.Receipt.EffectID remains available even on error. Persist a caller-owned // ID before a mutation if recovery must survive a process crash. The SDK never -// retries or deduplicates: the host alone replays identical input under the same -// ID and refuses different input. WithDeadline bounds the call, not the effect. +// retries or deduplicates: the host alone replays byte-identical input_json under +// the same ID and refuses different input. Persist the encoded input with the +// ID and reuse json.RawMessage for replay. WithDeadline bounds the call, not +// the effect. // // ErrOutcomeUnknown is a typed outcome: the provider may have acted. Lookup // returns the receipt alongside that sentinel, tested with errors.Is. Retain the // ID and look it up with a live context; do not infer a failed effect or mint a // new ID to retry. See Example_invoke for the complete compiled example. +// Lookup's ErrEffectNotFound retains the ID for a safe explicit same-ID invoke; +// committed receipts include Output without another invocation. package datasource diff --git a/datasource/doc_test.go b/datasource/doc_test.go index 24e254b..f1b2143 100644 --- a/datasource/doc_test.go +++ b/datasource/doc_test.go @@ -26,11 +26,15 @@ func Example_invoke() { fmt.Println("outcome still unknown", receipt.EffectID) return // no repeated effect, and no newly minted ID } + if errors.Is(lookupErr, datasource.ErrEffectNotFound) { + fmt.Println("effect not found; an explicit same-ID invoke is safe", receipt.EffectID) + return + } if lookupErr != nil { fmt.Println(lookupErr) return } - fmt.Println(receipt.Status) + fmt.Println(receipt.Status, string(receipt.Output)) return } if err != nil { diff --git a/datasource/errors.go b/datasource/errors.go index 73f2979..c8f9bfa 100644 --- a/datasource/errors.go +++ b/datasource/errors.go @@ -2,7 +2,6 @@ package datasource import ( "errors" - "strconv" "time" "connectrpc.com/connect" @@ -12,13 +11,18 @@ import ( var ( ErrNotPermitted = errors.New("datasource: not permitted") ErrUnknownOperation = errors.New("datasource: unknown operation") + // ErrEffectNotFound is Lookup's absent-effect outcome. A caller may explicitly + // invoke again with the same effect ID; the SDK never does so automatically. + ErrEffectNotFound = errors.New("datasource: effect not found") + // ErrEffectReused means the host rejected changed input under an existing ID. + ErrEffectReused = errors.New("datasource: effect ID reused") // ErrOutcomeUnknown means the effect may have happened. Inspect the receipt // with Lookup; never interpret this outcome as permission to repeat it. ErrOutcomeUnknown = errors.New("datasource: outcome unknown; look up the effect") ) // InputError reports a rejected input. Pointer is the host's JSON Pointer from -// BadRequest.FieldViolation.Field, or empty for a local JSON encoding failure. +// BadRequest.FieldViolation.Field, or empty when no structured pointer is supplied. type InputError struct { Pointer string cause error @@ -47,8 +51,8 @@ func (e *RateLimited) Error() string { } func (e *RateLimited) Unwrap() error { return e.cause } -// ProviderRefused reports a provider refusal. Status is the provider's HTTP -// status, or zero when the host supplies none. The original message is retained. +// ProviderRefused reports SOURCE_PROVIDER_REFUSED. Status is reserved and stays +// zero: the recorded host contract does not emit a provider status on errors. type ProviderRefused struct { Status int cause error @@ -62,6 +66,22 @@ func (e *ProviderRefused) Error() string { } func (e *ProviderRefused) Unwrap() error { return e.cause } +// OperationRefused reports a host precondition refusal. Reason is the structured +// ErrorInfo reason, or empty when the host supplies none; prose is not parsed. +// A refusal of this request does not establish the outcome of an earlier attempt. +type OperationRefused struct { + Reason string + cause error +} + +func (e *OperationRefused) Error() string { + if e.cause == nil { + return "datasource: operation refused" + } + return e.cause.Error() +} +func (e *OperationRefused) Unwrap() error { return e.cause } + type hostError struct { kind error cause error @@ -71,12 +91,26 @@ func (e *hostError) Error() string { return e.cause.Error() } func (e *hostError) Unwrap() error { return e.cause } func (e *hostError) Is(target error) bool { return target == e.kind } -// An unknown outcome deliberately does not unwrap to a retryable Connect -// Unavailable/DeadlineExceeded error. Its diagnostic message remains unchanged. -type unknownOutcome struct{ cause error } +// OutcomeUnknown means the provider may have acted. Receipt retains the effect +// ID. It deliberately does not unwrap to a retryable Connect transport error; +// Cause exposes that diagnostic explicitly for logging and metrics only. +// Test the outcome with errors.Is(err, ErrOutcomeUnknown). +type OutcomeUnknown struct { + Receipt Receipt + cause error +} + +func (e *OutcomeUnknown) Error() string { + if e.cause == nil { + return ErrOutcomeUnknown.Error() + } + return e.cause.Error() +} +func (e *OutcomeUnknown) Is(target error) bool { return target == ErrOutcomeUnknown } -func (e *unknownOutcome) Error() string { return e.cause.Error() } -func (e *unknownOutcome) Is(target error) bool { return target == ErrOutcomeUnknown } +// Cause returns the original diagnostic, if any. Its transport code does not +// establish whether the effect happened and must not drive an effect retry. +func (e *OutcomeUnknown) Cause() error { return e.cause } // mapHostError does not infer provider status or retry policy from free text. // A transport loss during Invoke is conservatively unknown: this client does @@ -130,14 +164,19 @@ func mapHostError(err error, effectOutcome bool, now time.Time) error { } return &RateLimited{ResetAt: reset, cause: err} case connect.CodeFailedPrecondition: - status, parseErr := strconv.Atoi(info.GetMetadata()["provider_status"]) - if parseErr != nil || status < 100 || status > 599 { - status = 0 + switch info.GetReason() { + case "SOURCE_OPERATION_OUTCOME_UNKNOWN": + return &OutcomeUnknown{cause: err} + case "SOURCE_PROVIDER_REFUSED": + return &ProviderRefused{cause: err} + case "SOURCE_EFFECT_REUSED": + return &hostError{kind: ErrEffectReused, cause: err} + default: + return &OperationRefused{Reason: info.GetReason(), cause: err} } - return &ProviderRefused{Status: status, cause: err} case connect.CodeUnavailable, connect.CodeDeadlineExceeded, connect.CodeCanceled: if effectOutcome { - return &unknownOutcome{cause: err} + return &OutcomeUnknown{cause: err} } } return err diff --git a/datasource/errors_test.go b/datasource/errors_test.go index 86f7e99..09a99b3 100644 --- a/datasource/errors_test.go +++ b/datasource/errors_test.go @@ -36,6 +36,8 @@ func TestHostErrorMapping(t *testing.T) { {"unavailable", detailedError(t, connect.CodeUnavailable), ErrOutcomeUnknown}, {"deadline", detailedError(t, connect.CodeDeadlineExceeded), ErrOutcomeUnknown}, {"canceled", detailedError(t, connect.CodeCanceled), ErrOutcomeUnknown}, + {"lost mutation", detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_OPERATION_OUTCOME_UNKNOWN"}), ErrOutcomeUnknown}, + {"effect reuse", detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_EFFECT_REUSED"}), ErrEffectReused}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -66,15 +68,37 @@ func TestHostErrorMapping(t *testing.T) { t.Fatalf("input = %#v", input) } }) - t.Run("provider status", func(t *testing.T) { + t.Run("provider refusal without status", func(t *testing.T) { err := detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{ - Reason: "DATASOURCE_PROVIDER_REFUSED", Metadata: map[string]string{"provider_status": "403"}, + Reason: "SOURCE_PROVIDER_REFUSED", }) var refused *ProviderRefused - if !errors.As(mapHostError(err, true, now), &refused) || refused.Status != 403 || refused.Error() != err.Error() { + if !errors.As(mapHostError(err, true, now), &refused) || refused.Status != 0 || refused.Error() != err.Error() { t.Fatalf("refusal = %#v", refused) } }) + for _, reason := range []string{"", "SOURCE_DECLARATION_CHANGED", "SOURCE_REAUTH_REQUIRED", "SOURCE_OUTPUT_REFUSED", + "SOURCE_EFFECT_REQUIRED", "SOURCE_OPERATIONS_UNAVAILABLE", "SOURCE_DECLARATION_INVALID", "SOURCE_EFFECT_BINDING_CHANGED", "FUTURE_REASON"} { + t.Run("precondition/"+reason, func(t *testing.T) { + err := detailedError(t, connect.CodeFailedPrecondition) + if reason != "" { + err = detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: reason}) + } + got := mapHostError(err, true, now) + var refused *OperationRefused + var provider *ProviderRefused + if !errors.As(got, &refused) || refused.Reason != reason || !errors.Is(got, err) || errors.As(got, &provider) { + t.Fatalf("host precondition = %v", got) + } + }) + } + t.Run("input without structured pointer", func(t *testing.T) { + err := connect.NewError(connect.CodeInvalidArgument, errors.New("source operation: invalid at /invoice/id")) + var invalid *InputError + if !errors.As(mapHostError(err, true, now), &invalid) || invalid.Pointer != "" || invalid.Error() != err.Error() { + t.Fatalf("input = %#v", invalid) + } + }) for _, code := range []connect.Code{connect.CodeUnknown, connect.CodeUnauthenticated, connect.CodeAlreadyExists, connect.CodeAborted, connect.CodeOutOfRange, connect.CodeUnimplemented, connect.CodeInternal, connect.CodeDataLoss, connect.CodeResourceExhausted} { diff --git a/datasource/invoke_options.go b/datasource/invoke_options.go index 99894d2..e0c81d9 100644 --- a/datasource/invoke_options.go +++ b/datasource/invoke_options.go @@ -6,14 +6,17 @@ import "time" type InvokeOption func(*invokeOptions) type invokeOptions struct { - effectID string - deadline time.Time + effectID string + effectIDSet bool + deadline time.Time } // WithEffectID reuses a caller-owned effect ID. The host replays an identical -// request and refuses different input under the same ID. An empty ID mints one. +// request and refuses different input under the same ID. IDs must contain 1–128 +// UTF-8 bytes without control bytes or surrounding spaces, so the header and +// request field remain identical. Omit this option to mint an ID. func WithEffectID(id string) InvokeOption { - return func(o *invokeOptions) { o.effectID = id } + return func(o *invokeOptions) { o.effectID, o.effectIDSet = id, true } } // WithDeadline bounds the call by an absolute deadline. An earlier deadline on diff --git a/datasource/json.go b/datasource/json.go index 4072bd9..a6ba0ee 100644 --- a/datasource/json.go +++ b/datasource/json.go @@ -13,6 +13,9 @@ import ( // Payloads remain JSON text, preserving integer precision and number spelling. // Only operation declaration schemas cross the Struct boundary below. func inputJSON(input any) (string, error) { + if _, ok := input.([]byte); ok { + return "", &InputError{cause: errors.New("datasource: use json.RawMessage for an encoded JSON object, not []byte")} + } data, err := json.Marshal(input) if err != nil { return "", &InputError{cause: err} diff --git a/datasource/json_test.go b/datasource/json_test.go index 20bab5f..aa29b9c 100644 --- a/datasource/json_test.go +++ b/datasource/json_test.go @@ -5,6 +5,7 @@ import ( "errors" "math" "reflect" + "strings" "testing" ) @@ -46,6 +47,14 @@ func TestInputJSONObject(t *testing.T) { } } +func TestByteInputExplainsRawMessage(t *testing.T) { + _, err := inputJSON([]byte(`{"count":2}`)) + var invalid *InputError + if !errors.As(err, &invalid) || !strings.Contains(err.Error(), "json.RawMessage") { + t.Fatalf("byte input = %v", err) + } +} + func TestObjectJSONRejectsNonObjectAndTrailingData(t *testing.T) { for _, value := range []string{"", "null", "[]", "42", `"text"`, "{} {}", "{} trailing", "{", "{\"x\":\"\xff\"}"} { if _, err := objectJSON(value); err == nil { diff --git a/datasource/operations.go b/datasource/operations.go index 99617f3..893eb2c 100644 --- a/datasource/operations.go +++ b/datasource/operations.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "time" + "unicode/utf8" "connectrpc.com/connect" "github.com/google/uuid" @@ -25,7 +26,11 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, for _, option := range opts { option(&options) } - if options.effectID == "" { + if options.effectIDSet { + if err := validateEffectID(options.effectID); err != nil { + return nil, err + } + } else { id, err := uuid.NewV7() if err != nil { return nil, fmt.Errorf("datasource: mint effect ID: %w", err) @@ -37,24 +42,34 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, ctx, cancel = context.WithDeadline(ctx, options.deadline) defer cancel() } - result := &Result{Receipt: Receipt{EffectID: options.effectID, Status: ReceiptStatusUnknown}} + result := &Result{Receipt: Receipt{EffectID: options.effectID, Status: ReceiptStatusNotAttempted}} + if err := ctx.Err(); err != nil { + return result, err + } request := connect.NewRequest(&v1.InvokeSourceOperationRequest{ OrgId: orgID, SourceId: sourceID, Operation: operation, InputJson: value, EffectId: options.effectID, }) request.Header().Set("x-codefly-effect-id", options.effectID) + result.Receipt.Status = ReceiptStatusUnknown response, err := c.inner.InvokeSourceOperation(ctx, request) if err != nil { - return result, mapHostError(err, true, time.Now()) + mapped := mapHostError(err, true, time.Now()) + var unknown *OutcomeUnknown + if errors.As(mapped, &unknown) { + unknown.Receipt = result.Receipt + } + return result, mapped } receipt, err := operationReceipt(response.Msg.GetReceipt(), options.effectID) if err != nil { - return result, &unknownOutcome{cause: err} + return result, &OutcomeUnknown{Receipt: result.Receipt, cause: err} } result.Receipt = *receipt if receipt.Status == ReceiptStatusUnknown { - return result, ErrOutcomeUnknown + return result, &OutcomeUnknown{Receipt: *receipt} } result.Output, err = objectJSON(response.Msg.GetOutputJson()) + result.Receipt.Output = result.Output return result, err } @@ -62,27 +77,40 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, // unknown outcome is returned alongside ErrOutcomeUnknown (errors.Is works). // An unavailable lookup also leaves the outcome unknown; it is never a reason // to dispatch the effect again. +// A NotFound response returns ErrEffectNotFound and the original ID for an +// explicit same-ID invoke. At the recorded host ref, a removed declaration can +// also produce NotFound; it is not proof that the provider never acted. // orgID and sourceID preserve the shared SDK signature; the host derives their // authority from the gateway's Work Context and the stored effect binding. func (c *Client) Lookup(ctx context.Context, orgID, sourceID, effectID string) (*Receipt, error) { + if err := validateEffectID(effectID); err != nil { + return nil, err + } response, err := c.inner.LookupInvokeSourceOperation(ctx, connect.NewRequest(&v1.LookupInvokeSourceOperationRequest{ EffectId: effectID, })) if err != nil { + if connect.CodeOf(err) == connect.CodeNotFound { + return &Receipt{EffectID: effectID, Status: ReceiptStatusNotAttempted}, &hostError{kind: ErrEffectNotFound, cause: err} + } mapped := mapHostError(err, connect.CodeOf(err) == connect.CodeUnavailable, time.Now()) - if errors.Is(mapped, ErrOutcomeUnknown) { - return &Receipt{EffectID: effectID, Status: ReceiptStatusUnknown}, mapped + var unknown *OutcomeUnknown + if errors.As(mapped, &unknown) { + unknown.Receipt = Receipt{EffectID: effectID, Status: ReceiptStatusUnknown} + return &unknown.Receipt, mapped } return nil, mapped } receipt, err := operationReceipt(response.Msg.GetReceipt(), effectID) if err != nil { - return nil, err + receipt = &Receipt{EffectID: effectID, Status: ReceiptStatusUnknown} + return receipt, &OutcomeUnknown{Receipt: *receipt, cause: err} } if receipt.Status == ReceiptStatusUnknown { - return receipt, ErrOutcomeUnknown + return receipt, &OutcomeUnknown{Receipt: *receipt} } - return receipt, nil + receipt.Output, err = objectJSON(response.Msg.GetOutputJson()) + return receipt, err } // DeclareOperations asks the host to replace the source's declarations @@ -108,7 +136,7 @@ func (c *Client) DeclareOperations(ctx context.Context, orgID, sourceID string, return &InputError{cause: fmt.Errorf("datasource: invalid operation effect %q", operation.Effect)} } declared = append(declared, &v1.SourceOperation{ - Name: operation.Name, Method: operation.Method, Path: operation.Path, Query: operation.Query, + Name: operation.Name, Description: operation.Description, Method: operation.Method, Path: operation.Path, Query: operation.Query, InputSchema: input, OutputSchema: output, Effect: effect, MaxOutputBytes: operation.MaxOutputBytes, }) } @@ -146,7 +174,8 @@ func (c *Client) ListOperations(ctx context.Context, orgID, sourceID string) ([] return nil, fmt.Errorf("datasource: unsupported host operation effect %v", value.GetEffect()) } operations = append(operations, Operation{ - Name: value.GetName(), Method: value.GetMethod(), Path: value.GetPath(), Query: value.GetQuery(), + Name: value.GetName(), Description: value.GetDescription(), Digest: value.GetDigest(), + Method: value.GetMethod(), Path: value.GetPath(), Query: value.GetQuery(), Input: input, Output: output, Effect: effect, MaxOutputBytes: value.GetMaxOutputBytes(), }) } @@ -175,3 +204,19 @@ func operationReceipt(value *v1.SourceOperationReceipt, effectID string) (*Recei } return receipt, nil } + +func validateEffectID(id string) error { + valid := len(id) > 0 && len(id) <= 128 && utf8.ValidString(id) + // net/http trims surrounding ASCII spaces from header values, which would + // make the effect header differ from the protobuf field at the host. + if valid { + valid = id[0] != ' ' && id[len(id)-1] != ' ' + } + for i := 0; i < len(id) && valid; i++ { + valid = id[i] >= 0x20 && id[i] != 0x7f + } + if !valid { + return &InputError{cause: errors.New("datasource: effect ID must be 1–128 UTF-8 bytes without control bytes or surrounding spaces")} + } + return nil +} diff --git a/datasource/operations_test.go b/datasource/operations_test.go index 60173dc..82489d6 100644 --- a/datasource/operations_test.go +++ b/datasource/operations_test.go @@ -157,7 +157,7 @@ func TestInvokeChangedInputReachesHostAndIsRefused(t *testing.T) { h := &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { calls.Add(1) if first != "" && first != req.Msg.GetInputJson() { - return nil, connect.NewError(connect.CodeFailedPrecondition, errors.New("effect ID has different input")) + return nil, connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_EFFECT_REUSED"}) } first = req.Msg.GetInputJson() return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: committedReceipt(req.Msg.GetEffectId())}), nil @@ -168,8 +168,7 @@ func TestInvokeChangedInputReachesHostAndIsRefused(t *testing.T) { t.Fatal(err) } _, err = c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{"amount": 2}, datasource.WithEffectID(result.Receipt.EffectID)) - var refusal *datasource.ProviderRefused - if !errors.As(err, &refusal) || calls.Load() != 2 { + if !errors.Is(err, datasource.ErrEffectReused) || calls.Load() != 2 { t.Fatalf("refusal = %v, host calls = %d", err, calls.Load()) } } @@ -196,6 +195,8 @@ func TestInvokeHostErrorsOverConnect(t *testing.T) { {"permission", connectError(t, connect.CodePermissionDenied), checkSentinel(datasource.ErrNotPermitted)}, {"operation", connectError(t, connect.CodeNotFound), checkSentinel(datasource.ErrUnknownOperation)}, {"unknown", connectError(t, connect.CodeUnavailable), checkSentinel(datasource.ErrOutcomeUnknown)}, + {"lost mutation", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_OPERATION_OUTCOME_UNKNOWN"}), checkSentinel(datasource.ErrOutcomeUnknown)}, + {"reused effect", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_EFFECT_REUSED"}), checkSentinel(datasource.ErrEffectReused)}, {"input", connectError(t, connect.CodeInvalidArgument, &errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{{Field: "/invoice/id"}}}), func(t *testing.T, err error, _, _ time.Time) { var value *datasource.InputError @@ -210,10 +211,10 @@ func TestInvokeHostErrorsOverConnect(t *testing.T) { t.Errorf("rate limit = %#v", value) } }}, - {"provider", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "DATASOURCE_PROVIDER_REFUSED", Metadata: map[string]string{"provider_status": "401"}}), + {"provider", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_PROVIDER_REFUSED"}), func(t *testing.T, err error, _, _ time.Time) { var value *datasource.ProviderRefused - if !errors.As(err, &value) || value.Status != 401 { + if !errors.As(err, &value) || value.Status != 0 { t.Errorf("provider refusal = %v", err) } }}, @@ -230,6 +231,18 @@ func TestInvokeHostErrorsOverConnect(t *testing.T) { if result == nil || result.Receipt.EffectID == "" { t.Fatal("RPC error lost the minted effect ID") } + if errors.Is(err, datasource.ErrOutcomeUnknown) { + var refused *datasource.ProviderRefused + var transport *connect.Error + var outcome *datasource.OutcomeUnknown + if errors.As(err, &refused) || errors.As(err, &transport) || result.Receipt.Status != datasource.ReceiptStatusUnknown { + t.Fatalf("unknown outcome misclassified: %+v, %v", result, err) + } + if !errors.As(err, &outcome) || outcome.Receipt.EffectID != result.Receipt.EffectID || + connect.CodeOf(outcome.Cause()) != connect.CodeOf(tt.err) || outcome.Cause().Error() != tt.err.Error() { + t.Fatal("explicit outcome diagnostics lost the Connect code, message, or effect ID") + } + } if err.Error() != tt.err.Error() { t.Errorf("host error message was changed: %q", err) } @@ -298,6 +311,12 @@ func TestOperationDeclarationsRoundTrip(t *testing.T) { declare: func(_ context.Context, req *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) { check(req.Msg.GetOrgId(), req.Msg.GetSourceId(), req.Header()) declared = req.Msg.GetOperations() + for _, operation := range declared { + if operation.GetDigest() != "" { + t.Error("declaration sent a caller-supplied digest") + } + operation.Digest = "host-digest" + } return connect.NewResponse(&v1.DeclareSourceOperationsResponse{}), nil }, list: func(_ context.Context, req *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) { @@ -309,12 +328,14 @@ func TestOperationDeclarationsRoundTrip(t *testing.T) { for _, effect := range []datasource.Effect{datasource.EffectReadOnly, datasource.EffectMutation} { want := datasource.Operation{ Name: "list_invoices", Method: "GET", Path: "/accounts/{id}/invoices", Query: []string{"limit"}, + Description: "List available invoices", Digest: "caller-digest-is-ignored", Input: json.RawMessage(`{"type":"object","additionalProperties":false}`), Output: json.RawMessage(`{"type":"object"}`), Effect: effect, MaxOutputBytes: 65536, } if err := c.DeclareOperations(ctx, "org", "source", []datasource.Operation{want}); err != nil { t.Fatal(err) } + want.Digest = "host-digest" got, err := c.ListOperations(ctx, "org", "source") if err != nil || len(got) != 1 { t.Fatalf("list = %+v, %v", got, err) @@ -356,7 +377,7 @@ func TestInvokeDeadlineRetainsEffectID(t *testing.T) { } } -func TestInvokeUnknownAndMalformedReceiptsRetainOriginalEffectID(t *testing.T) { +func TestUnknownAndMalformedReceiptsRetainOriginalEffectID(t *testing.T) { for _, tt := range []struct { name string receipt *v1.SourceOperationReceipt @@ -369,14 +390,25 @@ func TestInvokeUnknownAndMalformedReceiptsRetainOriginalEffectID(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { var calls atomic.Int32 - c := newOperationsClient(t, &operationsHandler{invoke: func(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { - calls.Add(1) - return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: tt.receipt}), nil - }}) + c := newOperationsClient(t, &operationsHandler{ + invoke: func(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + calls.Add(1) + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: tt.receipt}), nil + }, + lookup: func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: tt.receipt}), nil + }, + }) result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{}, datasource.WithEffectID("effect")) if !errors.Is(err, datasource.ErrOutcomeUnknown) || result == nil || result.Receipt.EffectID != "effect" || calls.Load() != 1 { t.Fatalf("result = %+v, %v, host calls = %d", result, err, calls.Load()) } + receipt, err := c.Lookup(context.Background(), "org", "source", "effect") + var outcome *datasource.OutcomeUnknown + if !errors.As(err, &outcome) || !errors.Is(err, datasource.ErrOutcomeUnknown) || receipt == nil || receipt.EffectID != "effect" || + outcome.Receipt.EffectID != "effect" || calls.Load() != 1 { + t.Fatalf("lookup = %+v, %v, invoke calls = %d", receipt, err, calls.Load()) + } }) } } @@ -494,3 +526,129 @@ func TestInvokeInvalidHostOutputRetainsCommittedReceipt(t *testing.T) { } } } + +func TestLookupHostOutcomes(t *testing.T) { + for _, tt := range []struct { + name string + hostError error + want error + status datasource.ReceiptStatus + }{ + {"not attempted", connect.NewError(connect.CodeNotFound, errors.New("source effect not found")), datasource.ErrEffectNotFound, datasource.ReceiptStatusNotAttempted}, + {"lost mutation", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_OPERATION_OUTCOME_UNKNOWN"}), datasource.ErrOutcomeUnknown, datasource.ReceiptStatusUnknown}, + } { + t.Run(tt.name, func(t *testing.T) { + var lookups, invokes atomic.Int32 + c := newOperationsClient(t, &operationsHandler{ + lookup: func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + lookups.Add(1) + return nil, tt.hostError + }, + invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + invokes.Add(1) + if req.Msg.GetEffectId() != "persisted-effect" { + t.Error("reinvoke changed the effect ID") + } + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: committedReceipt(req.Msg.GetEffectId())}), nil + }, + }) + receipt, err := c.Lookup(context.Background(), "org", "source", "persisted-effect") + var refused *datasource.ProviderRefused + if !errors.Is(err, tt.want) || errors.Is(err, datasource.ErrUnknownOperation) || errors.As(err, &refused) || + receipt == nil || receipt.EffectID != "persisted-effect" || receipt.Status != tt.status || receipt.Output != nil { + t.Fatalf("lookup = %+v, %v", receipt, err) + } + if lookups.Load() != 1 || invokes.Load() != 0 || err.Error() != tt.hostError.Error() { + t.Fatal("lookup retried, invoked, or changed the host message") + } + if errors.Is(err, datasource.ErrEffectNotFound) { + _, err = c.Invoke(context.Background(), "org", "source", "create_invoice", json.RawMessage(`{}`), datasource.WithEffectID(receipt.EffectID)) + if err != nil || invokes.Load() != 1 { + t.Fatalf("explicit same-ID invoke = %v, calls = %d", err, invokes.Load()) + } + } + }) + } +} + +func TestLookupReturnsCommittedOutputWithoutInvoking(t *testing.T) { + const output = `{"id":18446744073709551615,"amount":1.234567890123456789}` + c := newOperationsClient(t, &operationsHandler{lookup: func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + receipt := committedReceipt("effect") + receipt.OutputJson = output + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: output, Receipt: receipt}), nil + }}) + receipt, err := c.Lookup(context.Background(), "org", "source", "effect") + if err != nil || receipt.Status != datasource.ReceiptStatusCommitted || string(receipt.Output) != output { + t.Fatalf("lookup = %+v, %v", receipt, err) + } +} + +func TestInvalidEffectIDNeverDispatches(t *testing.T) { + var calls atomic.Int32 + interceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + calls.Add(1) + return next(ctx, req) + } + }) + c := newOperationsClient(t, &operationsHandler{}, connect.WithInterceptors(interceptor)) + for _, id := range []string{"", "bad\nid", "bad\rid", "bad\tid", " leading", "trailing ", "bad\x00id", "bad\x7fid", "\xff", strings.Repeat("x", 129), strings.Repeat("é", 65)} { + var input *datasource.InputError + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", json.RawMessage(`{}`), datasource.WithEffectID(id)) + if result != nil || !errors.As(err, &input) || errors.Is(err, datasource.ErrOutcomeUnknown) { + t.Fatalf("invoke ID %q = %+v, %v", id, result, err) + } + receipt, err := c.Lookup(context.Background(), "org", "source", id) + if receipt != nil || !errors.As(err, &input) { + t.Fatalf("lookup ID %q = %+v, %v", id, receipt, err) + } + } + if calls.Load() != 0 { + t.Fatalf("invalid IDs dispatched %d RPCs", calls.Load()) + } +} + +func TestEffectIDBoundaryReachesHostUnchanged(t *testing.T) { + for _, id := range []string{strings.Repeat("x", 126) + "!~", "effect with spaces", "facture-é", "发票", strings.Repeat("é", 64)} { + c := newOperationsClient(t, &operationsHandler{invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + if req.Msg.GetEffectId() != id || req.Header().Get("x-codefly-effect-id") != id { + t.Error("effect ID changed") + } + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: `{}`, Receipt: committedReceipt(id)}), nil + }}) + if _, err := c.Invoke(context.Background(), "org", "source", "list_invoices", json.RawMessage(`{}`), datasource.WithEffectID(id)); err != nil { + t.Fatal(err) + } + } +} + +func TestCanceledBeforeDispatchIsNotUnknown(t *testing.T) { + var calls atomic.Int32 + interceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + calls.Add(1) + return next(ctx, req) + } + }) + c := newOperationsClient(t, &operationsHandler{}, connect.WithInterceptors(interceptor)) + canceled, cancel := context.WithCancel(context.Background()) + cancel() + for _, tt := range []struct { + ctx context.Context + opts []datasource.InvokeOption + want error + }{ + {canceled, nil, context.Canceled}, + {context.Background(), []datasource.InvokeOption{datasource.WithDeadline(time.Now().Add(-time.Second))}, context.DeadlineExceeded}, + } { + result, err := c.Invoke(tt.ctx, "org", "source", "create_invoice", json.RawMessage(`{}`), tt.opts...) + if !errors.Is(err, tt.want) || errors.Is(err, datasource.ErrOutcomeUnknown) || result == nil || + result.Receipt.EffectID == "" || result.Receipt.Status != datasource.ReceiptStatusNotAttempted { + t.Fatalf("preflight = %+v, %v", result, err) + } + } + if calls.Load() != 0 { + t.Fatalf("canceled call dispatched %d RPCs", calls.Load()) + } +} diff --git a/datasource/types.go b/datasource/types.go index 40da53a..6e78ead 100644 --- a/datasource/types.go +++ b/datasource/types.go @@ -19,6 +19,7 @@ const ( // admission, JSON Schema validation, routing, credentials and output limits. type Operation struct { Name string + Description string Method string Path string Query []string @@ -26,6 +27,9 @@ type Operation struct { Output json.RawMessage Effect Effect MaxOutputBytes uint32 + // Digest is read from ListOperations; DeclareOperations ignores it because + // the host computes the admitted declaration's digest. + Digest string } // ReceiptStatus is the host's observation of an effect, not a retry decision. @@ -33,8 +37,11 @@ type ReceiptStatus string const ( ReceiptStatusCommitted ReceiptStatus = "COMMITTED" - ReceiptStatusRefused ReceiptStatus = "REFUSED" ReceiptStatusUnknown ReceiptStatus = "UNKNOWN" + // ReceiptStatusNotAttempted labels a Lookup miss or local preflight rejection. + // The host can also return NotFound for a removed declaration, so a lookup + // miss is not historical proof that the provider never acted. Keep the ID. + ReceiptStatusNotAttempted ReceiptStatus = "NOT_ATTEMPTED" ) // Receipt identifies an effect for Lookup. Unknown means its outcome is not @@ -45,6 +52,9 @@ type Receipt struct { Status ReceiptStatus CommittedAt time.Time ProviderStatus int + // Output is the committed output, also available after Lookup. It is nil + // when there is no committed output. + Output json.RawMessage } // Result carries the provider output and the host receipt. Invoke also returns diff --git a/go.mod b/go.mod index 6a023e1..4e2a4df 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.27.0 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 connectrpc.com/connect v1.21.0 - github.com/codefly-dev/core v0.14.0 + github.com/codefly-dev/core v0.3.41 github.com/codefly-dev/sdk-go/workcontext v0.0.0-20260927024316-b3fb40d43099 github.com/google/uuid v1.6.0 golang.org/x/tools v0.51.0 diff --git a/go.sum b/go.sum index 08fd755..5448653 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ connectrpc.com/connect v1.21.0 h1:LhqSJt7jHf5NJBo9Jq/t/9FjcYAideif0mg+qe2jCUs= connectrpc.com/connect v1.21.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/codefly-dev/core v0.14.0 h1:rpLFxeHSf2PrbxsRSp/NcZt8htMRM38JV2Oc8RKievU= -github.com/codefly-dev/core v0.14.0/go.mod h1:rp4YZkCJEOufliWQAgK3sv8YlO7Xcva7+cN7hIlAR0I= +github.com/codefly-dev/core v0.3.41 h1:Etae1jOIWEl524U7tnCT3TaDPUtU5WGsf1XhlFALvt8= +github.com/codefly-dev/core v0.3.41/go.mod h1:uG0HQy5iWnkuWUPa1LbbiPfEgzuxi1SzEKwcFPC6eoA= github.com/codefly-dev/sdk-go/workcontext v0.0.0-20260927024316-b3fb40d43099 h1:MEzUboHy6HzBex1VoAOo3JqBgjTjSYs3jQGyqIwHOhQ= github.com/codefly-dev/sdk-go/workcontext v0.0.0-20260927024316-b3fb40d43099/go.mod h1:BbqUHTJEnmH3oxYblfjS3Oh9AUw14YaEFVV4Y19yQPY= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= From 3c27aef5bf93e3180ba89aba375914e22d1f8c61 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:56:26 -0400 Subject: [PATCH 05/12] fix(datasource): wrap unknown outcome diagnostics --- CHANGELOG.md | 6 +++--- README.md | 18 +++++++++------- datasource/errors.go | 7 +++--- datasource/errors_test.go | 40 ++++++++++++++++++++++------------- datasource/operations_test.go | 9 +++++--- 5 files changed, 48 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43a5a68..1f281db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ - Add `ErrNotPermitted`, `ErrUnknownOperation`, `InputError`, `RateLimited`, `ProviderRefused`, `OperationRefused`, `ErrEffectReused`, `ErrEffectNotFound` and `ErrOutcomeUnknown`. The host's `SOURCE_OPERATION_OUTCOME_UNKNOWN` - reason retains the effect ID and never exposes a retryable transport error. - `*OutcomeUnknown.Cause()` provides the original diagnostic explicitly for - logging and metrics without enabling generic transport retries. + reason retains the effect ID. `*OutcomeUnknown` wraps the original error for + `errors.Is`/`errors.As` and Connect diagnostics; callers must check + `ErrOutcomeUnknown` before applying transport retry rules. The SDK never retries. Only `SOURCE_PROVIDER_REFUSED` maps to a provider refusal; its status remains unavailable at this host ref. Host messages remain unchanged. - Lookup misses return the original ID with `NOT_ATTEMPTED`; callers may diff --git a/README.md b/README.md index 900b13a..df9dab8 100644 --- a/README.md +++ b/README.md @@ -90,16 +90,18 @@ The SDK surfaces are: `ErrOutcomeUnknown`; check it with `errors.Is`. **Unknown is an outcome, not evidence of failure or permission to repeat an effect.** Invoke also reports transport loss or deadline expiry after dispatch conservatively as - unknown, since it cannot prove the operation was read-only. That outcome does not unwrap - to a retryable Connect transport error. An unavailable lookup also returns - an unknown receipt: it cannot establish what happened to the effect. Use a + unknown, since it cannot prove the operation was read-only. Check + `errors.Is(err, datasource.ErrOutcomeUnknown)` **before** transport-code + retry logic: the wrapped Connect error remains available for diagnostics, + but its code does not establish whether the effect happened. An unavailable + lookup also returns an unknown receipt: it cannot establish what happened to the effect. Use a fresh bounded context to look up an effect after the original context has expired. - For logging and metrics, `errors.As(err, &outcome)` with - `var outcome *datasource.OutcomeUnknown` exposes `outcome.Cause()` and its - original Connect code. This explicit diagnostic access does not put a - retryable transport error in the outcome's unwrap chain. + For logging and metrics, `errors.As` can inspect both + `*datasource.OutcomeUnknown` (including its receipt) and the wrapped + `*connect.Error`; `errors.Is` also preserves an underlying context error. + The SDK never retries either error. `DeclareOperations(ctx, orgID, sourceID, []datasource.Operation)` replaces the source's declaration set atomically under the host's administrator @@ -129,7 +131,7 @@ The SDK surfaces are: | PermissionDenied | `ErrNotPermitted` (`errors.Is`) | | Invoke NotFound | `ErrUnknownOperation` (`errors.Is`) | | Lookup NotFound | receipt plus `ErrEffectNotFound` (`errors.Is`); keep the ID for an explicit reinvoke | - | InvalidArgument | `*InputError`; `Pointer` is populated only from a structured BadRequest field violation | + | InvalidArgument | `*InputError`; `Pointer` is empty at the recorded host ref | | ResourceExhausted + DATASOURCE_RATE_LIMITED | `*RateLimited`, with `ResetAt` from `reset_at` or `RetryInfo` | | FailedPrecondition + SOURCE_OPERATION_OUTCOME_UNKNOWN | receipt plus `ErrOutcomeUnknown` (`errors.Is`); the mutation may have happened | | FailedPrecondition + SOURCE_PROVIDER_REFUSED | `*ProviderRefused` | diff --git a/datasource/errors.go b/datasource/errors.go index c8f9bfa..7925330 100644 --- a/datasource/errors.go +++ b/datasource/errors.go @@ -92,9 +92,9 @@ func (e *hostError) Unwrap() error { return e.cause } func (e *hostError) Is(target error) bool { return target == e.kind } // OutcomeUnknown means the provider may have acted. Receipt retains the effect -// ID. It deliberately does not unwrap to a retryable Connect transport error; -// Cause exposes that diagnostic explicitly for logging and metrics only. -// Test the outcome with errors.Is(err, ErrOutcomeUnknown). +// ID and Unwrap preserves the original diagnostic for errors.Is/errors.As. +// Check errors.Is(err, ErrOutcomeUnknown) before classifying transport errors: +// the wrapped transport code is not permission to retry the effect. type OutcomeUnknown struct { Receipt Receipt cause error @@ -107,6 +107,7 @@ func (e *OutcomeUnknown) Error() string { return e.cause.Error() } func (e *OutcomeUnknown) Is(target error) bool { return target == ErrOutcomeUnknown } +func (e *OutcomeUnknown) Unwrap() error { return e.cause } // Cause returns the original diagnostic, if any. Its transport code does not // establish whether the effect happened and must not drive an effect retry. diff --git a/datasource/errors_test.go b/datasource/errors_test.go index 09a99b3..2f1973c 100644 --- a/datasource/errors_test.go +++ b/datasource/errors_test.go @@ -1,7 +1,9 @@ package datasource import ( + "context" "errors" + "fmt" "testing" "time" @@ -48,26 +50,13 @@ func TestHostErrorMapping(t *testing.T) { if got.Error() != tt.err.Error() { t.Fatalf("host diagnostic was changed: %q", got.Error()) } - if tt.want == ErrOutcomeUnknown { - var retryable *connect.Error - if errors.As(got, &retryable) { - t.Fatal("unknown outcome exposes a retryable transport error") - } - } else if !errors.Is(got, tt.err) { + var transport *connect.Error + if !errors.Is(got, tt.err) || !errors.As(got, &transport) || transport != tt.err { t.Fatal("host error was lost") } }) } - t.Run("input pointer", func(t *testing.T) { - err := detailedError(t, connect.CodeInvalidArgument, &errdetails.BadRequest{ - FieldViolations: []*errdetails.BadRequest_FieldViolation{{Field: "/invoice/items/0/id"}}, - }) - var input *InputError - if !errors.As(mapHostError(err, true, now), &input) || input.Pointer != "/invoice/items/0/id" || input.Error() != err.Error() { - t.Fatalf("input = %#v", input) - } - }) t.Run("provider refusal without status", func(t *testing.T) { err := detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{ Reason: "SOURCE_PROVIDER_REFUSED", @@ -147,3 +136,24 @@ func TestRateLimitReset(t *testing.T) { }) } } + +func TestUnknownOutcomeWrapsContextAndConnectErrors(t *testing.T) { + for _, tt := range []struct { + code connect.Code + cause error + }{ + {connect.CodeDeadlineExceeded, context.DeadlineExceeded}, + {connect.CodeCanceled, context.Canceled}, + } { + host := connect.NewError(tt.code, tt.cause) + mapped := mapHostError(host, true, time.Now()) + wrapped := fmt.Errorf("invoke: %w", mapped) + var outcome *OutcomeUnknown + var transport *connect.Error + if !errors.Is(wrapped, ErrOutcomeUnknown) || !errors.Is(wrapped, tt.cause) || + !errors.As(wrapped, &outcome) || !errors.As(wrapped, &transport) || transport != host || + connect.CodeOf(wrapped) != tt.code || outcome.Unwrap() != host { + t.Fatalf("unknown outcome lost its identity or cause: %v", wrapped) + } + } +} diff --git a/datasource/operations_test.go b/datasource/operations_test.go index 82489d6..5dfc2a3 100644 --- a/datasource/operations_test.go +++ b/datasource/operations_test.go @@ -197,10 +197,10 @@ func TestInvokeHostErrorsOverConnect(t *testing.T) { {"unknown", connectError(t, connect.CodeUnavailable), checkSentinel(datasource.ErrOutcomeUnknown)}, {"lost mutation", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_OPERATION_OUTCOME_UNKNOWN"}), checkSentinel(datasource.ErrOutcomeUnknown)}, {"reused effect", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_EFFECT_REUSED"}), checkSentinel(datasource.ErrEffectReused)}, - {"input", connectError(t, connect.CodeInvalidArgument, &errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{{Field: "/invoice/id"}}}), + {"input", connectError(t, connect.CodeInvalidArgument, &errdetails.ErrorInfo{Reason: "SOURCE_INPUT_REFUSED"}), func(t *testing.T, err error, _, _ time.Time) { var value *datasource.InputError - if !errors.As(err, &value) || value.Pointer != "/invoice/id" { + if !errors.As(err, &value) || value.Pointer != "" { t.Errorf("input error = %v", err) } }}, @@ -235,9 +235,12 @@ func TestInvokeHostErrorsOverConnect(t *testing.T) { var refused *datasource.ProviderRefused var transport *connect.Error var outcome *datasource.OutcomeUnknown - if errors.As(err, &refused) || errors.As(err, &transport) || result.Receipt.Status != datasource.ReceiptStatusUnknown { + if errors.As(err, &refused) || result.Receipt.Status != datasource.ReceiptStatusUnknown { t.Fatalf("unknown outcome misclassified: %+v, %v", result, err) } + if !errors.As(err, &transport) || connect.CodeOf(err) != connect.CodeOf(tt.err) { + t.Fatal("unknown outcome lost its wrapped Connect error") + } if !errors.As(err, &outcome) || outcome.Receipt.EffectID != result.Receipt.EffectID || connect.CodeOf(outcome.Cause()) != connect.CodeOf(tt.err) || outcome.Cause().Error() != tt.err.Error() { t.Fatal("explicit outcome diagnostics lost the Connect code, message, or effect ID") From 122e33348fe6b5bd3a4d6b58695d2c189aebc7d4 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:22:19 -0400 Subject: [PATCH 06/12] fix(datasource): align HTTP outcomes and deadline preflight --- CHANGELOG.md | 8 +- README.md | 13 ++- datasource/doc.go | 3 +- datasource/errors.go | 6 ++ datasource/errors_test.go | 22 +++-- datasource/invoke_options.go | 6 +- datasource/operations.go | 24 ++++-- datasource/operations_test.go | 155 +++++++++++++++++++++++++++++++++- datasource/types.go | 2 +- 9 files changed, 218 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f281db..d770576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,12 @@ explicitly reinvoke with the same ID. Committed receipts include `Output`, so recovery can retrieve output without another invoke. Receipt statuses are `COMMITTED`, `UNKNOWN` and `NOT_ATTEMPTED`. -- Validate effect IDs and reject an already-canceled context or expired - deadline before dispatch. Add operation descriptions and host-computed +- Every non-Connect HTTP failure after Invoke/Lookup dispatch, including 4xx, + 429 and 500, returns `ErrOutcomeUnknown` with the original effect ID and + diagnostic cause. Decoded RPC errors retain their existing typed mappings. +- Validate effect IDs and reject an expired deadline before dispatch with + `InputError`, no result and zero RPC calls. Explicit cancellation returns + `context.Canceled` with no result. Add operation descriptions and host-computed digests to declaration/list round-tripping. - Document the call and retry contracts in `README.md` and package examples. - Invoke payloads use `input_json` / `output_json` strings containing exactly diff --git a/README.md b/README.md index df9dab8..d2dc6b6 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,10 @@ The SDK surfaces are: supplies an effect ID of 1–128 UTF-8 bytes without control bytes or surrounding spaces (which HTTP would trim); interior spaces and Unicode are supported; when omitted, the SDK mints a UUIDv7. Invalid IDs return `*InputError` before - dispatch. An already-canceled context or expired deadline returns the plain - context error before dispatch. Once a call is attempted, even an error + dispatch. A deadline already expired before dispatch (including an earlier + parent deadline) returns `*InputError` with no `Result` and zero RPC calls. + An explicitly canceled context returns `context.Canceled` with no result. + Once a call is attempted, even an error returns a non-nil `Result` containing that ID in `Receipt.EffectID`. Persist a caller-owned ID **before** invoking a mutation if it must survive a process crash. @@ -90,7 +92,11 @@ The SDK surfaces are: `ErrOutcomeUnknown`; check it with `errors.Is`. **Unknown is an outcome, not evidence of failure or permission to repeat an effect.** Invoke also reports transport loss or deadline expiry after dispatch conservatively as - unknown, since it cannot prove the operation was read-only. Check + unknown, since it cannot prove the operation was read-only. Every non-Connect + HTTP error after Invoke or Lookup dispatch, including 4xx, 429 and 500, + likewise returns `ErrOutcomeUnknown` with the original ID. The Connect + decoder identifies RPC errors; decoded errors retain their code/reason + mappings. Check `errors.Is(err, datasource.ErrOutcomeUnknown)` **before** transport-code retry logic: the wrapped Connect error remains available for diagnostics, but its code does not establish whether the effect happened. An unavailable @@ -137,6 +143,7 @@ The SDK surfaces are: | FailedPrecondition + SOURCE_PROVIDER_REFUSED | `*ProviderRefused` | | FailedPrecondition + SOURCE_EFFECT_REUSED | `ErrEffectReused` (`errors.Is`) | | Other FailedPrecondition | `*OperationRefused`, with the structured `Reason` or an empty string | + | Non-Connect HTTP error after Invoke/Lookup dispatch (4xx or 5xx) | receipt plus `ErrOutcomeUnknown`; retain the original ID | | Ambiguous Invoke / unknown receipt | receipt plus `ErrOutcomeUnknown` (`errors.Is`) | The recorded host ref does not emit `BadRequest.FieldViolation` or provider diff --git a/datasource/doc.go b/datasource/doc.go index 6e71731..a9f4f86 100644 --- a/datasource/doc.go +++ b/datasource/doc.go @@ -20,7 +20,8 @@ // retries or deduplicates: the host alone replays byte-identical input_json under // the same ID and refuses different input. Persist the encoded input with the // ID and reuse json.RawMessage for replay. WithDeadline bounds the call, not -// the effect. +// the effect. An already-expired deadline returns InputError with no result +// before dispatch; a deadline expiring after dispatch leaves the outcome unknown. // // ErrOutcomeUnknown is a typed outcome: the provider may have acted. Lookup // returns the receipt alongside that sentinel, tested with errors.Is. Retain the diff --git a/datasource/errors.go b/datasource/errors.go index 7925330..91b8019 100644 --- a/datasource/errors.go +++ b/datasource/errors.go @@ -120,6 +120,12 @@ func mapHostError(err error, effectOutcome bool, now time.Time) error { if err == nil { return nil } + // HTTP status alone cannot establish whether the host attempted an effect. + // Use Connect's decoded-wire marker, not its inferred HTTP-to-code mapping: + // for example, a proxy's plain 429 and 500 otherwise produce different codes. + if effectOutcome && !connect.IsWireError(err) { + return &OutcomeUnknown{cause: err} + } var ce *connect.Error if !errors.As(err, &ce) { return err diff --git a/datasource/errors_test.go b/datasource/errors_test.go index 2f1973c..178d44e 100644 --- a/datasource/errors_test.go +++ b/datasource/errors_test.go @@ -15,7 +15,7 @@ import ( func detailedError(t *testing.T, code connect.Code, details ...proto.Message) *connect.Error { t.Helper() - err := connect.NewError(code, errors.New("provider said credential-looking-example-secret")) + err := connect.NewWireError(code, errors.New("provider said credential-looking-example-secret")) for _, message := range details { detail, detailErr := connect.NewErrorDetail(message) if detailErr != nil { @@ -66,6 +66,15 @@ func TestHostErrorMapping(t *testing.T) { t.Fatalf("refusal = %#v", refused) } }) + t.Run("forged provider status is ignored", func(t *testing.T) { + err := detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{ + Reason: "SOURCE_PROVIDER_REFUSED", Metadata: map[string]string{"provider_status": "401"}, + }) + var refused *ProviderRefused + if !errors.As(mapHostError(err, true, now), &refused) || refused.Status != 0 { + t.Fatalf("unadmitted metadata became a provider status: %#v", refused) + } + }) for _, reason := range []string{"", "SOURCE_DECLARATION_CHANGED", "SOURCE_REAUTH_REQUIRED", "SOURCE_OUTPUT_REFUSED", "SOURCE_EFFECT_REQUIRED", "SOURCE_OPERATIONS_UNAVAILABLE", "SOURCE_DECLARATION_INVALID", "SOURCE_EFFECT_BINDING_CHANGED", "FUTURE_REASON"} { t.Run("precondition/"+reason, func(t *testing.T) { @@ -82,7 +91,7 @@ func TestHostErrorMapping(t *testing.T) { }) } t.Run("input without structured pointer", func(t *testing.T) { - err := connect.NewError(connect.CodeInvalidArgument, errors.New("source operation: invalid at /invoice/id")) + err := connect.NewWireError(connect.CodeInvalidArgument, errors.New("source operation: invalid at /invoice/id")) var invalid *InputError if !errors.As(mapHostError(err, true, now), &invalid) || invalid.Pointer != "" || invalid.Error() != err.Error() { t.Fatalf("input = %#v", invalid) @@ -102,9 +111,12 @@ func TestHostErrorMapping(t *testing.T) { t.Errorf("read transport failure %v was presented as an effect outcome: %v", err, got) } } - plain := errors.New("local error") - if mapHostError(plain, true, now) != plain || mapHostError(nil, true, now) != nil { - t.Fatal("non-Connect error changed") + plain := errors.New("transport error") + if mapHostError(plain, false, now) != plain || mapHostError(nil, true, now) != nil { + t.Fatal("unrelated error changed") + } + if mapped := mapHostError(plain, true, now); !errors.Is(mapped, ErrOutcomeUnknown) || !errors.Is(mapped, plain) { + t.Fatal("post-dispatch transport error lost its unknown outcome or cause") } } diff --git a/datasource/invoke_options.go b/datasource/invoke_options.go index e0c81d9..1bc713b 100644 --- a/datasource/invoke_options.go +++ b/datasource/invoke_options.go @@ -9,6 +9,7 @@ type invokeOptions struct { effectID string effectIDSet bool deadline time.Time + deadlineSet bool } // WithEffectID reuses a caller-owned effect ID. The host replays an identical @@ -20,7 +21,8 @@ func WithEffectID(id string) InvokeOption { } // WithDeadline bounds the call by an absolute deadline. An earlier deadline on -// ctx still wins. Expiry does not prove that a provider mutation did not happen. +// ctx still wins. A deadline expired before dispatch returns InputError with no +// result. Expiry after dispatch does not prove that a mutation did not happen. func WithDeadline(deadline time.Time) InvokeOption { - return func(o *invokeOptions) { o.deadline = deadline } + return func(o *invokeOptions) { o.deadline, o.deadlineSet = deadline, true } } diff --git a/datasource/operations.go b/datasource/operations.go index 893eb2c..371d9ab 100644 --- a/datasource/operations.go +++ b/datasource/operations.go @@ -37,20 +37,19 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, } options.effectID = id.String() } - if !options.deadline.IsZero() { + if options.deadlineSet { var cancel context.CancelFunc ctx, cancel = context.WithDeadline(ctx, options.deadline) defer cancel() } - result := &Result{Receipt: Receipt{EffectID: options.effectID, Status: ReceiptStatusNotAttempted}} - if err := ctx.Err(); err != nil { - return result, err + if err := operationContextError(ctx); err != nil { + return nil, err } + result := &Result{Receipt: Receipt{EffectID: options.effectID, Status: ReceiptStatusUnknown}} request := connect.NewRequest(&v1.InvokeSourceOperationRequest{ OrgId: orgID, SourceId: sourceID, Operation: operation, InputJson: value, EffectId: options.effectID, }) request.Header().Set("x-codefly-effect-id", options.effectID) - result.Receipt.Status = ReceiptStatusUnknown response, err := c.inner.InvokeSourceOperation(ctx, request) if err != nil { mapped := mapHostError(err, true, time.Now()) @@ -86,10 +85,17 @@ func (c *Client) Lookup(ctx context.Context, orgID, sourceID, effectID string) ( if err := validateEffectID(effectID); err != nil { return nil, err } + if err := operationContextError(ctx); err != nil { + return nil, err + } response, err := c.inner.LookupInvokeSourceOperation(ctx, connect.NewRequest(&v1.LookupInvokeSourceOperationRequest{ EffectId: effectID, })) if err != nil { + if !connect.IsWireError(err) { + receipt := &Receipt{EffectID: effectID, Status: ReceiptStatusUnknown} + return receipt, &OutcomeUnknown{Receipt: *receipt, cause: err} + } if connect.CodeOf(err) == connect.CodeNotFound { return &Receipt{EffectID: effectID, Status: ReceiptStatusNotAttempted}, &hostError{kind: ErrEffectNotFound, cause: err} } @@ -220,3 +226,11 @@ func validateEffectID(id string) error { } return nil } + +func operationContextError(ctx context.Context) error { + err := ctx.Err() + if errors.Is(err, context.DeadlineExceeded) { + return &InputError{cause: fmt.Errorf("datasource: deadline expired before dispatch: %w", err)} + } + return err +} diff --git a/datasource/operations_test.go b/datasource/operations_test.go index 5dfc2a3..0f5b6f4 100644 --- a/datasource/operations_test.go +++ b/datasource/operations_test.go @@ -1,9 +1,12 @@ package datasource_test import ( + "compress/gzip" "context" "encoding/json" "errors" + "fmt" + "io" "net/http" "net/http/httptest" "reflect" @@ -637,6 +640,8 @@ func TestCanceledBeforeDispatchIsNotUnknown(t *testing.T) { c := newOperationsClient(t, &operationsHandler{}, connect.WithInterceptors(interceptor)) canceled, cancel := context.WithCancel(context.Background()) cancel() + expired, cancelExpired := context.WithDeadline(context.Background(), time.Now().Add(-time.Second)) + defer cancelExpired() for _, tt := range []struct { ctx context.Context opts []datasource.InvokeOption @@ -644,14 +649,160 @@ func TestCanceledBeforeDispatchIsNotUnknown(t *testing.T) { }{ {canceled, nil, context.Canceled}, {context.Background(), []datasource.InvokeOption{datasource.WithDeadline(time.Now().Add(-time.Second))}, context.DeadlineExceeded}, + {context.Background(), []datasource.InvokeOption{datasource.WithDeadline(time.Time{})}, context.DeadlineExceeded}, + {expired, nil, context.DeadlineExceeded}, + {expired, []datasource.InvokeOption{datasource.WithDeadline(time.Now().Add(time.Minute))}, context.DeadlineExceeded}, } { result, err := c.Invoke(tt.ctx, "org", "source", "create_invoice", json.RawMessage(`{}`), tt.opts...) - if !errors.Is(err, tt.want) || errors.Is(err, datasource.ErrOutcomeUnknown) || result == nil || - result.Receipt.EffectID == "" || result.Receipt.Status != datasource.ReceiptStatusNotAttempted { + if !errors.Is(err, tt.want) || errors.Is(err, datasource.ErrOutcomeUnknown) || result != nil { t.Fatalf("preflight = %+v, %v", result, err) } + var input *datasource.InputError + if errors.As(err, &input) != (tt.want == context.DeadlineExceeded) { + t.Fatalf("expired deadline must be InputError; cancellation remains context.Canceled: %v", err) + } + if tt.ctx.Err() != nil { + receipt, err := c.Lookup(tt.ctx, "org", "source", "persisted-effect") + if receipt != nil || !errors.Is(err, tt.want) || errors.Is(err, datasource.ErrOutcomeUnknown) || + errors.As(err, &input) != (tt.want == context.DeadlineExceeded) { + t.Fatalf("lookup preflight = %+v, %v", receipt, err) + } + } } if calls.Load() != 0 { t.Fatalf("canceled call dispatched %d RPCs", calls.Load()) } } + +func TestNonConnectHTTPFailuresAreUnknownAfterDispatch(t *testing.T) { + for _, status := range []int{400, 401, 403, 404, 408, 409, 422, 429, 499, 500, 501, 502, 503, 504, 599} { + for _, body := range []struct { + name, contentType, text string + }{ + {"text", "text/plain", "upstream request failed"}, + {"html", "text/html", "upstream request failed"}, + {"empty", "application/json", ""}, + {"invalid JSON", "application/json", "{"}, + } { + for _, effectID := range []string{"", "persisted-effect"} { + t.Run(fmt.Sprintf("%d/%s/%s", status, body.name, effectID), func(t *testing.T) { + var calls atomic.Int32 + receivedIDs := make(chan string, 2) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls.Add(1) + data, err := io.ReadAll(r.Body) + if err != nil { + t.Error(err) + } + if r.URL.Path == accountsv1connect.DatasourceServiceInvokeSourceOperationProcedure { + var request v1.InvokeSourceOperationRequest + if err := proto.Unmarshal(data, &request); err != nil { + t.Error(err) + } + if request.GetEffectId() != r.Header.Get("x-codefly-effect-id") { + t.Error("effect header changed") + } + receivedIDs <- request.GetEffectId() + } else if r.URL.Path == accountsv1connect.DatasourceServiceLookupInvokeSourceOperationProcedure { + var request v1.LookupInvokeSourceOperationRequest + if err := proto.Unmarshal(data, &request); err != nil { + t.Error(err) + } + receivedIDs <- request.GetEffectId() + } else { + t.Errorf("unexpected procedure %s", r.URL.Path) + } + w.Header().Set("Content-Type", body.contentType) + w.WriteHeader(status) + _, _ = io.WriteString(w, body.text) + })) + t.Cleanup(server.Close) + c := datasource.New(gw{base: server.URL, client: server.Client()}) + var opts []datasource.InvokeOption + if effectID != "" { + opts = append(opts, datasource.WithEffectID(effectID)) + } + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", json.RawMessage(`{}`), opts...) + var outcome *datasource.OutcomeUnknown + var transport *connect.Error + if result == nil || !errors.Is(err, datasource.ErrOutcomeUnknown) || !errors.As(err, &outcome) || + !errors.As(err, &transport) || result.Receipt.Status != datasource.ReceiptStatusUnknown { + t.Fatalf("HTTP %d invoke = %+v, %v", status, result, err) + } + id := result.Receipt.EffectID + if id == "" || (effectID != "" && id != effectID) || outcome.Receipt.EffectID != id || calls.Load() != 1 { + t.Fatalf("effect ID lost or SDK retried: %+v, calls %d", result, calls.Load()) + } + if received := <-receivedIDs; received != id { + t.Fatalf("invoke sent %q, returned %q", received, id) + } + receipt, err := c.Lookup(context.Background(), "org", "source", id) + if receipt == nil || !errors.Is(err, datasource.ErrOutcomeUnknown) || !errors.As(err, &outcome) || + receipt.EffectID != id || outcome.Receipt.EffectID != id || receipt.Status != datasource.ReceiptStatusUnknown || + errors.Is(err, datasource.ErrEffectNotFound) || calls.Load() != 2 { + t.Fatalf("HTTP %d lookup = %+v, %v, calls %d", status, receipt, err, calls.Load()) + } + if received := <-receivedIDs; received != id { + t.Fatalf("lookup sent %q, want %q", received, id) + } + }) + } + } + } +} + +func TestDecodedConnectHTTPFailuresKeepTheirMappings(t *testing.T) { + for _, compressed := range []bool{false, true} { + for _, tt := range []struct { + code connect.Code + status int + want error + body string + }{ + {connect.CodeInternal, 500, nil, `{"code":"internal","message":"host diagnostic"}`}, + {connect.CodeUnauthenticated, 401, nil, `{"code":"unauthenticated","message":"host diagnostic"}`}, + {connect.CodeResourceExhausted, 429, nil, `{"code":"resource_exhausted","message":"host diagnostic"}`}, + {connect.CodePermissionDenied, 403, datasource.ErrNotPermitted, `{"code":"permission_denied","message":"host diagnostic"}`}, + // Connect owns decoding: missing/unrecognized string codes use its + // HTTP fallback, and a recognized code wins over HTTP status. + {connect.CodeUnavailable, 429, datasource.ErrOutcomeUnknown, `{}`}, + {connect.CodeUnknown, 500, nil, `{}`}, + {connect.CodeUnavailable, 429, datasource.ErrOutcomeUnknown, `{"code":"future_code"}`}, + {connect.CodeUnknown, 500, nil, `{"code":"future_code"}`}, + {connect.CodePermissionDenied, 500, datasource.ErrNotPermitted, `{"code":"permission_denied"}`}, + } { + t.Run(fmt.Sprintf("%s/gzip=%t", tt.code, compressed), func(t *testing.T) { + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.Header().Set("Content-Type", "application/json") + if compressed { + w.Header().Set("Content-Encoding", "gzip") + } + w.WriteHeader(tt.status) + var writer io.Writer = w + if compressed { + zip := gzip.NewWriter(w) + defer zip.Close() + writer = zip + } + _, _ = io.WriteString(writer, tt.body) + })) + t.Cleanup(server.Close) + c := datasource.New(gw{base: server.URL, client: server.Client()}) + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", json.RawMessage(`{}`)) + var transport *connect.Error + if result == nil || result.Receipt.EffectID == "" || errors.Is(err, datasource.ErrOutcomeUnknown) != (tt.want == datasource.ErrOutcomeUnknown) || + !errors.As(err, &transport) || !connect.IsWireError(err) || transport.Code() != tt.code || + (tt.want != nil && !errors.Is(err, tt.want)) { + t.Fatalf("valid Connect invoke = %+v, %v", result, err) + } + _, err = c.Lookup(context.Background(), "org", "source", result.Receipt.EffectID) + if errors.Is(err, datasource.ErrOutcomeUnknown) != (tt.want == datasource.ErrOutcomeUnknown) || !connect.IsWireError(err) || connect.CodeOf(err) != tt.code || + (tt.want != nil && !errors.Is(err, tt.want)) || calls.Load() != 2 { + t.Fatalf("valid Connect lookup = %v, calls %d", err, calls.Load()) + } + }) + } + } +} diff --git a/datasource/types.go b/datasource/types.go index 6e78ead..621d846 100644 --- a/datasource/types.go +++ b/datasource/types.go @@ -38,7 +38,7 @@ type ReceiptStatus string const ( ReceiptStatusCommitted ReceiptStatus = "COMMITTED" ReceiptStatusUnknown ReceiptStatus = "UNKNOWN" - // ReceiptStatusNotAttempted labels a Lookup miss or local preflight rejection. + // ReceiptStatusNotAttempted labels a Lookup miss. // The host can also return NotFound for a removed declaration, so a lookup // miss is not historical proof that the provider never acted. Keep the ID. ReceiptStatusNotAttempted ReceiptStatus = "NOT_ATTEMPTED" From 193f9bdc8c0288bbbbd7d9ac9e1a5f777ff4b71f Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:52:20 -0400 Subject: [PATCH 07/12] fix(datasource): consume published host error details --- CHANGELOG.md | 11 +- README.md | 27 +- SOURCE.txt | 2 +- datasource/errors.go | 13 +- datasource/errors_test.go | 48 +- datasource/operations_test.go | 16 +- .../accountsv1connect/datasource.connect.go | 136 ++++- gen/saas/accounts/v1/datasource.pb.go | 554 ++++++++++++------ 8 files changed, 571 insertions(+), 236 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d770576..7d8018b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,10 @@ reason retains the effect ID. `*OutcomeUnknown` wraps the original error for `errors.Is`/`errors.As` and Connect diagnostics; callers must check `ErrOutcomeUnknown` before applying transport retry rules. The SDK never retries. - Only `SOURCE_PROVIDER_REFUSED` maps to a provider refusal; its status remains - unavailable at this host ref. Host messages remain unchanged. + Only `SOURCE_PROVIDER_REFUSED` maps to a provider refusal; its status comes + from the host's `provider_status` metadata (canonical HTTP 100–599, zero + when absent or invalid). `InputError.Pointer` preserves the host's first + `BadRequest.FieldViolation.field`. Host messages remain unchanged. - Lookup misses return the original ID with `NOT_ATTEMPTED`; callers may explicitly reinvoke with the same ID. Committed receipts include `Output`, so recovery can retrieve output without another invoke. Receipt statuses are @@ -33,9 +35,12 @@ - Update `golang.org/x/tools` so the API boundary gate can read Go 1.27 export data; the gate and its fixtures are unchanged. - Regenerate the full host API from PR #1052 head - `f4ce896d954101b164c3b511cbeea555f17302f7`, recorded in `SOURCE.txt` with module + `86ee51fd8b4d66aad9da2509483e14d3c7534764`, recorded in `SOURCE.txt` with module version `0.1.0`. This is a development PR head, not a release tag. Release waits for the host tag and regeneration from its commit. +- Include the runtime-facing `PruneSourceOperationReceipts` and + `LookupPruneSourceOperationReceipts` RPCs in generated bindings only; + no receipt retention methods are exposed by the caller facade. - Re-export the OAuth2 grant type/constants reachable from source reads. The generation template emits all host-owned `saas/` schemas and imports Core's Runnable descriptors from Core v0.3.41, which supplies the required package diff --git a/README.md b/README.md index d2dc6b6..439ae7f 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,11 @@ The SDK surfaces are: stored effect binding. These two arguments cannot select or override lookup authority. A lookup miss returns `ErrEffectNotFound` and a receipt carrying the same ID with status `NOT_ATTEMPTED`; an explicit reinvoke with that ID is - safe, and the SDK never invokes automatically. At the recorded host ref, - NotFound can also mean the declaration disappeared after an attempt; it is - not proof the provider never acted. Keep the original ID in either case. + safe, and the SDK never invokes automatically. The host documents + `source effect not found` as never attempted. Its implementation at the + recorded ref can also return NotFound when the declaration disappeared + after an attempt; a lookup miss is not proof the provider never acted. + Keep the original ID in either case. An unknown receipt is returned alongside `ErrOutcomeUnknown`; check it with `errors.Is`. **Unknown is an outcome, not evidence of failure or permission to repeat an effect.** Invoke also @@ -137,20 +139,22 @@ The SDK surfaces are: | PermissionDenied | `ErrNotPermitted` (`errors.Is`) | | Invoke NotFound | `ErrUnknownOperation` (`errors.Is`) | | Lookup NotFound | receipt plus `ErrEffectNotFound` (`errors.Is`); keep the ID for an explicit reinvoke | - | InvalidArgument | `*InputError`; `Pointer` is empty at the recorded host ref | + | InvalidArgument | `*InputError`, with `Pointer` from the first `BadRequest.FieldViolation.field` | | ResourceExhausted + DATASOURCE_RATE_LIMITED | `*RateLimited`, with `ResetAt` from `reset_at` or `RetryInfo` | | FailedPrecondition + SOURCE_OPERATION_OUTCOME_UNKNOWN | receipt plus `ErrOutcomeUnknown` (`errors.Is`); the mutation may have happened | - | FailedPrecondition + SOURCE_PROVIDER_REFUSED | `*ProviderRefused` | + | FailedPrecondition + SOURCE_PROVIDER_REFUSED | `*ProviderRefused`, with `Status` from `ErrorInfo.metadata["provider_status"]` when known | | FailedPrecondition + SOURCE_EFFECT_REUSED | `ErrEffectReused` (`errors.Is`) | | Other FailedPrecondition | `*OperationRefused`, with the structured `Reason` or an empty string | | Non-Connect HTTP error after Invoke/Lookup dispatch (4xx or 5xx) | receipt plus `ErrOutcomeUnknown`; retain the original ID | | Ambiguous Invoke / unknown receipt | receipt plus `ErrOutcomeUnknown` (`errors.Is`) | - The recorded host ref does not emit `BadRequest.FieldViolation` or provider - status metadata on errors: `InputError.Pointer` is empty and - `ProviderRefused.Status` stays zero. Those details belong to the host's - [#1049 contract](https://github.com/codefly-dev/module-saas-starter/issues/1049). - Its receipt guard also emits detail-free precondition refusals, represented + The recorded host ref emits structured input pointers and provider HTTP + statuses. `InputError.Pointer` is empty when no field violation is supplied; + otherwise the JSON Pointer is preserved verbatim. `ProviderRefused.Status` + is zero when metadata is missing or is not a three-digit ASCII decimal HTTP + status from 100–599. Status metadata is read only for `SOURCE_PROVIDER_REFUSED`; + it cannot turn an unknown outcome into a provider refusal. + The host's receipt guard also emits detail-free precondition refusals, represented as `OperationRefused` with an empty reason. A precondition refusal does not establish the outcome of an earlier attempt under that ID. @@ -167,6 +171,9 @@ The SDK surfaces are: [module-saas-starter#1052](https://github.com/codefly-dev/module-saas-starter/pull/1052). `SOURCE.txt` records its exact development PR head. It is not a release tag: release waits for the host tag and regeneration from the tagged commit. + The host's runtime-scheduled `PruneSourceOperationReceipts` and + `LookupPruneSourceOperationReceipts` are included in the generated bindings + only; this caller facade does not expose receipt retention operations. - **`moduleauthority/`** — the module-principal side of installed operation authority. A long-running module supplies its Codefly-projected registration credential once; the client mints and refreshes the module's short-lived Work diff --git a/SOURCE.txt b/SOURCE.txt index 87bb703..e6212e1 100644 --- a/SOURCE.txt +++ b/SOURCE.txt @@ -1,4 +1,4 @@ -gen/ generated from codefly-dev/module-saas-starter@f4ce896d954101b164c3b511cbeea555f17302f7 +gen/ generated from codefly-dev/module-saas-starter@86ee51fd8b4d66aad9da2509483e14d3c7534764 (accounts public proto: module/services/accounts/proto) module version: 0.1.0 (module/module.package.codefly.yaml) diff --git a/datasource/errors.go b/datasource/errors.go index 91b8019..42289f9 100644 --- a/datasource/errors.go +++ b/datasource/errors.go @@ -2,6 +2,7 @@ package datasource import ( "errors" + "strconv" "time" "connectrpc.com/connect" @@ -51,8 +52,8 @@ func (e *RateLimited) Error() string { } func (e *RateLimited) Unwrap() error { return e.cause } -// ProviderRefused reports SOURCE_PROVIDER_REFUSED. Status is reserved and stays -// zero: the recorded host contract does not emit a provider status on errors. +// ProviderRefused reports SOURCE_PROVIDER_REFUSED. Status is the HTTP status +// from the host's provider_status metadata, or zero when absent or invalid. type ProviderRefused struct { Status int cause error @@ -175,7 +176,13 @@ func mapHostError(err error, effectOutcome bool, now time.Time) error { case "SOURCE_OPERATION_OUTCOME_UNKNOWN": return &OutcomeUnknown{cause: err} case "SOURCE_PROVIDER_REFUSED": - return &ProviderRefused{cause: err} + encoded := info.GetMetadata()["provider_status"] + status, parseErr := strconv.Atoi(encoded) + // The host emits canonical decimal HTTP statuses from 100 through 599. + if parseErr != nil || status < 100 || status > 599 || strconv.Itoa(status) != encoded { + status = 0 + } + return &ProviderRefused{Status: status, cause: err} case "SOURCE_EFFECT_REUSED": return &hostError{kind: ErrEffectReused, cause: err} default: diff --git a/datasource/errors_test.go b/datasource/errors_test.go index 178d44e..d9f8060 100644 --- a/datasource/errors_test.go +++ b/datasource/errors_test.go @@ -66,15 +66,35 @@ func TestHostErrorMapping(t *testing.T) { t.Fatalf("refusal = %#v", refused) } }) - t.Run("forged provider status is ignored", func(t *testing.T) { - err := detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{ - Reason: "SOURCE_PROVIDER_REFUSED", Metadata: map[string]string{"provider_status": "401"}, + for _, tt := range []struct { + metadata string + status int + }{ + {"100", 100}, {"403", 403}, {"429", 429}, {"500", 500}, {"599", 599}, + {"", 0}, {"0", 0}, {"99", 0}, {"600", 0}, {"-403", 0}, {"+403", 0}, + {"0403", 0}, {" 403", 0}, {"403 ", 0}, {"403.0", 0}, {"403", 0}, + {"invalid", 0}, {"999999999999999999999999", 0}, + } { + t.Run("provider status/"+tt.metadata, func(t *testing.T) { + err := detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{ + Reason: "SOURCE_PROVIDER_REFUSED", Domain: "saas.accounts.v1", + Metadata: map[string]string{"provider_status": tt.metadata}, + }) + var refused *ProviderRefused + if !errors.As(mapHostError(err, true, now), &refused) || refused.Status != tt.status || + refused.Error() != err.Error() || !errors.Is(refused, err) { + t.Fatalf("provider refusal = %#v, want status %d with unchanged cause", refused, tt.status) + } }) - var refused *ProviderRefused - if !errors.As(mapHostError(err, true, now), &refused) || refused.Status != 0 { - t.Fatalf("unadmitted metadata became a provider status: %#v", refused) - } + } + // Metadata never changes the meaning of the structured reason. + unknown := detailedError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{ + Reason: "SOURCE_OPERATION_OUTCOME_UNKNOWN", Domain: "saas.accounts.v1", + Metadata: map[string]string{"provider_status": "403"}, }) + if mapped := mapHostError(unknown, true, now); !errors.Is(mapped, ErrOutcomeUnknown) { + t.Fatalf("provider metadata changed an unknown outcome: %v", mapped) + } for _, reason := range []string{"", "SOURCE_DECLARATION_CHANGED", "SOURCE_REAUTH_REQUIRED", "SOURCE_OUTPUT_REFUSED", "SOURCE_EFFECT_REQUIRED", "SOURCE_OPERATIONS_UNAVAILABLE", "SOURCE_DECLARATION_INVALID", "SOURCE_EFFECT_BINDING_CHANGED", "FUTURE_REASON"} { t.Run("precondition/"+reason, func(t *testing.T) { @@ -97,6 +117,20 @@ func TestHostErrorMapping(t *testing.T) { t.Fatalf("input = %#v", invalid) } }) + for _, pointer := range []string{"/id", "/", "/items/0/a~1b~0c"} { + t.Run("input pointer"+pointer, func(t *testing.T) { + err := detailedError(t, connect.CodeInvalidArgument, + &errdetails.ErrorInfo{Reason: "SOURCE_INPUT_REFUSED", Domain: "saas.accounts.v1"}, + &errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{{ + Field: pointer, Description: "input does not satisfy the declaration", + }}}) + var invalid *InputError + if !errors.As(mapHostError(err, true, now), &invalid) || invalid.Pointer != pointer || + invalid.Error() != err.Error() || !errors.Is(invalid, err) { + t.Fatalf("input = %#v, want pointer %q with unchanged cause", invalid, pointer) + } + }) + } for _, code := range []connect.Code{connect.CodeUnknown, connect.CodeUnauthenticated, connect.CodeAlreadyExists, connect.CodeAborted, connect.CodeOutOfRange, connect.CodeUnimplemented, connect.CodeInternal, connect.CodeDataLoss, connect.CodeResourceExhausted} { diff --git a/datasource/operations_test.go b/datasource/operations_test.go index 0f5b6f4..e19335d 100644 --- a/datasource/operations_test.go +++ b/datasource/operations_test.go @@ -200,10 +200,16 @@ func TestInvokeHostErrorsOverConnect(t *testing.T) { {"unknown", connectError(t, connect.CodeUnavailable), checkSentinel(datasource.ErrOutcomeUnknown)}, {"lost mutation", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_OPERATION_OUTCOME_UNKNOWN"}), checkSentinel(datasource.ErrOutcomeUnknown)}, {"reused effect", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_EFFECT_REUSED"}), checkSentinel(datasource.ErrEffectReused)}, - {"input", connectError(t, connect.CodeInvalidArgument, &errdetails.ErrorInfo{Reason: "SOURCE_INPUT_REFUSED"}), + // These details match the host's TestSourceOperationSDKErrorDetails at + // the ref in SOURCE.txt: a schema refusal at /id and a provider HTTP 403. + {"input", connectError(t, connect.CodeInvalidArgument, + &errdetails.ErrorInfo{Reason: "SOURCE_INPUT_REFUSED", Domain: "saas.accounts.v1"}, + &errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{{ + Field: "/id", Description: "input does not satisfy the declaration", + }}}), func(t *testing.T, err error, _, _ time.Time) { var value *datasource.InputError - if !errors.As(err, &value) || value.Pointer != "" { + if !errors.As(err, &value) || value.Pointer != "/id" { t.Errorf("input error = %v", err) } }}, @@ -214,10 +220,12 @@ func TestInvokeHostErrorsOverConnect(t *testing.T) { t.Errorf("rate limit = %#v", value) } }}, - {"provider", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{Reason: "SOURCE_PROVIDER_REFUSED"}), + {"provider", connectError(t, connect.CodeFailedPrecondition, &errdetails.ErrorInfo{ + Reason: "SOURCE_PROVIDER_REFUSED", Domain: "saas.accounts.v1", Metadata: map[string]string{"provider_status": "403"}, + }), func(t *testing.T, err error, _, _ time.Time) { var value *datasource.ProviderRefused - if !errors.As(err, &value) || value.Status != 0 { + if !errors.As(err, &value) || value.Status != 403 { t.Errorf("provider refusal = %v", err) } }}, diff --git a/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go b/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go index afb5bc4..20eb8ec 100644 --- a/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go +++ b/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go @@ -33,6 +33,12 @@ const ( // reflection-formatted method names, remove the leading slash and convert the remaining slash to a // period. const ( + // DatasourceServicePruneSourceOperationReceiptsProcedure is the fully-qualified name of the + // DatasourceService's PruneSourceOperationReceipts RPC. + DatasourceServicePruneSourceOperationReceiptsProcedure = "/saas.accounts.v1.DatasourceService/PruneSourceOperationReceipts" + // DatasourceServiceLookupPruneSourceOperationReceiptsProcedure is the fully-qualified name of the + // DatasourceService's LookupPruneSourceOperationReceipts RPC. + DatasourceServiceLookupPruneSourceOperationReceiptsProcedure = "/saas.accounts.v1.DatasourceService/LookupPruneSourceOperationReceipts" // DatasourceServiceInvokeSourceOperationProcedure is the fully-qualified name of the // DatasourceService's InvokeSourceOperation RPC. DatasourceServiceInvokeSourceOperationProcedure = "/saas.accounts.v1.DatasourceService/InvokeSourceOperation" @@ -118,9 +124,22 @@ const ( // DatasourceServiceClient is a client for the saas.accounts.v1.DatasourceService service. type DatasourceServiceClient interface { + // PruneSourceOperationReceipts expires response bodies after Invoke's declared + // total timeout plus a 24-hour lookup grace. It retains effect tombstones. + // The composition schedules this source-scoped host operation in the runtime. + // Shared sources require an org admin; personal sources require their owner. + PruneSourceOperationReceipts(context.Context, *connect.Request[v1.PruneSourceOperationReceiptsRequest]) (*connect.Response[v1.PruneSourceOperationReceiptsResponse], error) + LookupPruneSourceOperationReceipts(context.Context, *connect.Request[v1.LookupPruneSourceOperationReceiptsRequest]) (*connect.Response[v1.PruneSourceOperationReceiptsResponse], error) // InvokeSourceOperation calls an admitted operation under current authority. + // FailedPrecondition with ErrorInfo.reason SOURCE_OPERATION_OUTCOME_UNKNOWN + // means this effect will not be redispatched and its receipt stays unresolved. + // Other refusal reasons do not mean unknown. Provider refusals include the + // decimal HTTP status in ErrorInfo.metadata["provider_status"] when known. + // InvalidArgument carries BadRequest.FieldViolation.field as a JSON pointer. InvokeSourceOperation(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) // LookupInvokeSourceOperation recovers a receipt without contacting the provider. + // NotFound "source effect not found" means there is no attempt marker; retrying + // the same effect is safe. An unknown receipt never authorizes redispatch. LookupInvokeSourceOperation(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) // ListSourceOperations reads the currently admitted declarations under source authority. ListSourceOperations(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) @@ -219,6 +238,18 @@ func NewDatasourceServiceClient(httpClient connect.HTTPClient, baseURL string, o baseURL = strings.TrimRight(baseURL, "/") datasourceServiceMethods := v1.File_saas_accounts_v1_datasource_proto.Services().ByName("DatasourceService").Methods() return &datasourceServiceClient{ + pruneSourceOperationReceipts: connect.NewClient[v1.PruneSourceOperationReceiptsRequest, v1.PruneSourceOperationReceiptsResponse]( + httpClient, + baseURL+DatasourceServicePruneSourceOperationReceiptsProcedure, + connect.WithSchema(datasourceServiceMethods.ByName("PruneSourceOperationReceipts")), + connect.WithClientOptions(opts...), + ), + lookupPruneSourceOperationReceipts: connect.NewClient[v1.LookupPruneSourceOperationReceiptsRequest, v1.PruneSourceOperationReceiptsResponse]( + httpClient, + baseURL+DatasourceServiceLookupPruneSourceOperationReceiptsProcedure, + connect.WithSchema(datasourceServiceMethods.ByName("LookupPruneSourceOperationReceipts")), + connect.WithClientOptions(opts...), + ), invokeSourceOperation: connect.NewClient[v1.InvokeSourceOperationRequest, v1.InvokeSourceOperationResponse]( httpClient, baseURL+DatasourceServiceInvokeSourceOperationProcedure, @@ -386,33 +417,47 @@ func NewDatasourceServiceClient(httpClient connect.HTTPClient, baseURL string, o // datasourceServiceClient implements DatasourceServiceClient. type datasourceServiceClient struct { - invokeSourceOperation *connect.Client[v1.InvokeSourceOperationRequest, v1.InvokeSourceOperationResponse] - lookupInvokeSourceOperation *connect.Client[v1.LookupInvokeSourceOperationRequest, v1.InvokeSourceOperationResponse] - listSourceOperations *connect.Client[v1.ListSourceOperationsRequest, v1.ListSourceOperationsResponse] - declareSourceOperations *connect.Client[v1.DeclareSourceOperationsRequest, v1.DeclareSourceOperationsResponse] - addGitHubSource *connect.Client[v1.AddGitHubSourceRequest, v1.AddGitHubSourceResponse] - addSource *connect.Client[v1.AddSourceRequest, v1.AddSourceResponse] - getDatasourceCatalog *connect.Client[v1.GetDatasourceCatalogRequest, v1.GetDatasourceCatalogResponse] - listSources *connect.Client[v1.ListSourcesRequest, v1.ListSourcesResponse] - getSource *connect.Client[v1.GetSourceRequest, v1.GetSourceResponse] - syncSource *connect.Client[v1.SyncSourceRequest, v1.SyncSourceResponse] - getSourceSync *connect.Client[v1.GetSourceSyncRequest, v1.GetSourceSyncResponse] - deleteSource *connect.Client[v1.DeleteSourceRequest, v1.DeleteSourceResponse] - beginGitHubAppSetup *connect.Client[v1.BeginGitHubAppSetupRequest, v1.BeginGitHubAppSetupResponse] - completeGitHubAppSetup *connect.Client[v1.CompleteGitHubAppSetupRequest, v1.CompleteGitHubAppSetupResponse] - migrateGitHubSourceToApp *connect.Client[v1.MigrateGitHubSourceToAppRequest, v1.MigrateGitHubSourceToAppResponse] - beginDatasourceAccountLink *connect.Client[v1.BeginDatasourceAccountLinkRequest, v1.BeginDatasourceAccountLinkResponse] - completeDatasourceAccountLink *connect.Client[v1.CompleteDatasourceAccountLinkRequest, v1.CompleteDatasourceAccountLinkResponse] - listMyDatasourceAccountLinks *connect.Client[v1.ListMyDatasourceAccountLinksRequest, v1.ListMyDatasourceAccountLinksResponse] - deleteDatasourceAccountLink *connect.Client[v1.DeleteDatasourceAccountLinkRequest, v1.DeleteDatasourceAccountLinkResponse] - getDatasourceDirectory *connect.Client[v1.GetDatasourceDirectoryRequest, v1.GetDatasourceDirectoryResponse] - bindDatasourceGroup *connect.Client[v1.BindDatasourceGroupRequest, v1.BindDatasourceGroupResponse] - unbindDatasourceGroup *connect.Client[v1.UnbindDatasourceGroupRequest, v1.UnbindDatasourceGroupResponse] - claimDatasourceDomain *connect.Client[v1.ClaimDatasourceDomainRequest, v1.ClaimDatasourceDomainResponse] - verifyDatasourceDomain *connect.Client[v1.VerifyDatasourceDomainRequest, v1.VerifyDatasourceDomainResponse] - deleteDatasourceDomain *connect.Client[v1.DeleteDatasourceDomainRequest, v1.DeleteDatasourceDomainResponse] - listSourceDelegations *connect.Client[v1.ListSourceDelegationsRequest, v1.ListSourceDelegationsResponse] - revokeSourceDelegation *connect.Client[v1.RevokeSourceDelegationRequest, v1.RevokeSourceDelegationResponse] + pruneSourceOperationReceipts *connect.Client[v1.PruneSourceOperationReceiptsRequest, v1.PruneSourceOperationReceiptsResponse] + lookupPruneSourceOperationReceipts *connect.Client[v1.LookupPruneSourceOperationReceiptsRequest, v1.PruneSourceOperationReceiptsResponse] + invokeSourceOperation *connect.Client[v1.InvokeSourceOperationRequest, v1.InvokeSourceOperationResponse] + lookupInvokeSourceOperation *connect.Client[v1.LookupInvokeSourceOperationRequest, v1.InvokeSourceOperationResponse] + listSourceOperations *connect.Client[v1.ListSourceOperationsRequest, v1.ListSourceOperationsResponse] + declareSourceOperations *connect.Client[v1.DeclareSourceOperationsRequest, v1.DeclareSourceOperationsResponse] + addGitHubSource *connect.Client[v1.AddGitHubSourceRequest, v1.AddGitHubSourceResponse] + addSource *connect.Client[v1.AddSourceRequest, v1.AddSourceResponse] + getDatasourceCatalog *connect.Client[v1.GetDatasourceCatalogRequest, v1.GetDatasourceCatalogResponse] + listSources *connect.Client[v1.ListSourcesRequest, v1.ListSourcesResponse] + getSource *connect.Client[v1.GetSourceRequest, v1.GetSourceResponse] + syncSource *connect.Client[v1.SyncSourceRequest, v1.SyncSourceResponse] + getSourceSync *connect.Client[v1.GetSourceSyncRequest, v1.GetSourceSyncResponse] + deleteSource *connect.Client[v1.DeleteSourceRequest, v1.DeleteSourceResponse] + beginGitHubAppSetup *connect.Client[v1.BeginGitHubAppSetupRequest, v1.BeginGitHubAppSetupResponse] + completeGitHubAppSetup *connect.Client[v1.CompleteGitHubAppSetupRequest, v1.CompleteGitHubAppSetupResponse] + migrateGitHubSourceToApp *connect.Client[v1.MigrateGitHubSourceToAppRequest, v1.MigrateGitHubSourceToAppResponse] + beginDatasourceAccountLink *connect.Client[v1.BeginDatasourceAccountLinkRequest, v1.BeginDatasourceAccountLinkResponse] + completeDatasourceAccountLink *connect.Client[v1.CompleteDatasourceAccountLinkRequest, v1.CompleteDatasourceAccountLinkResponse] + listMyDatasourceAccountLinks *connect.Client[v1.ListMyDatasourceAccountLinksRequest, v1.ListMyDatasourceAccountLinksResponse] + deleteDatasourceAccountLink *connect.Client[v1.DeleteDatasourceAccountLinkRequest, v1.DeleteDatasourceAccountLinkResponse] + getDatasourceDirectory *connect.Client[v1.GetDatasourceDirectoryRequest, v1.GetDatasourceDirectoryResponse] + bindDatasourceGroup *connect.Client[v1.BindDatasourceGroupRequest, v1.BindDatasourceGroupResponse] + unbindDatasourceGroup *connect.Client[v1.UnbindDatasourceGroupRequest, v1.UnbindDatasourceGroupResponse] + claimDatasourceDomain *connect.Client[v1.ClaimDatasourceDomainRequest, v1.ClaimDatasourceDomainResponse] + verifyDatasourceDomain *connect.Client[v1.VerifyDatasourceDomainRequest, v1.VerifyDatasourceDomainResponse] + deleteDatasourceDomain *connect.Client[v1.DeleteDatasourceDomainRequest, v1.DeleteDatasourceDomainResponse] + listSourceDelegations *connect.Client[v1.ListSourceDelegationsRequest, v1.ListSourceDelegationsResponse] + revokeSourceDelegation *connect.Client[v1.RevokeSourceDelegationRequest, v1.RevokeSourceDelegationResponse] +} + +// PruneSourceOperationReceipts calls +// saas.accounts.v1.DatasourceService.PruneSourceOperationReceipts. +func (c *datasourceServiceClient) PruneSourceOperationReceipts(ctx context.Context, req *connect.Request[v1.PruneSourceOperationReceiptsRequest]) (*connect.Response[v1.PruneSourceOperationReceiptsResponse], error) { + return c.pruneSourceOperationReceipts.CallUnary(ctx, req) +} + +// LookupPruneSourceOperationReceipts calls +// saas.accounts.v1.DatasourceService.LookupPruneSourceOperationReceipts. +func (c *datasourceServiceClient) LookupPruneSourceOperationReceipts(ctx context.Context, req *connect.Request[v1.LookupPruneSourceOperationReceiptsRequest]) (*connect.Response[v1.PruneSourceOperationReceiptsResponse], error) { + return c.lookupPruneSourceOperationReceipts.CallUnary(ctx, req) } // InvokeSourceOperation calls saas.accounts.v1.DatasourceService.InvokeSourceOperation. @@ -554,9 +599,22 @@ func (c *datasourceServiceClient) RevokeSourceDelegation(ctx context.Context, re // DatasourceServiceHandler is an implementation of the saas.accounts.v1.DatasourceService service. type DatasourceServiceHandler interface { + // PruneSourceOperationReceipts expires response bodies after Invoke's declared + // total timeout plus a 24-hour lookup grace. It retains effect tombstones. + // The composition schedules this source-scoped host operation in the runtime. + // Shared sources require an org admin; personal sources require their owner. + PruneSourceOperationReceipts(context.Context, *connect.Request[v1.PruneSourceOperationReceiptsRequest]) (*connect.Response[v1.PruneSourceOperationReceiptsResponse], error) + LookupPruneSourceOperationReceipts(context.Context, *connect.Request[v1.LookupPruneSourceOperationReceiptsRequest]) (*connect.Response[v1.PruneSourceOperationReceiptsResponse], error) // InvokeSourceOperation calls an admitted operation under current authority. + // FailedPrecondition with ErrorInfo.reason SOURCE_OPERATION_OUTCOME_UNKNOWN + // means this effect will not be redispatched and its receipt stays unresolved. + // Other refusal reasons do not mean unknown. Provider refusals include the + // decimal HTTP status in ErrorInfo.metadata["provider_status"] when known. + // InvalidArgument carries BadRequest.FieldViolation.field as a JSON pointer. InvokeSourceOperation(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) // LookupInvokeSourceOperation recovers a receipt without contacting the provider. + // NotFound "source effect not found" means there is no attempt marker; retrying + // the same effect is safe. An unknown receipt never authorizes redispatch. LookupInvokeSourceOperation(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) // ListSourceOperations reads the currently admitted declarations under source authority. ListSourceOperations(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) @@ -651,6 +709,18 @@ type DatasourceServiceHandler interface { // and JSON codecs. They also support gzip compression. func NewDatasourceServiceHandler(svc DatasourceServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { datasourceServiceMethods := v1.File_saas_accounts_v1_datasource_proto.Services().ByName("DatasourceService").Methods() + datasourceServicePruneSourceOperationReceiptsHandler := connect.NewUnaryHandler( + DatasourceServicePruneSourceOperationReceiptsProcedure, + svc.PruneSourceOperationReceipts, + connect.WithSchema(datasourceServiceMethods.ByName("PruneSourceOperationReceipts")), + connect.WithHandlerOptions(opts...), + ) + datasourceServiceLookupPruneSourceOperationReceiptsHandler := connect.NewUnaryHandler( + DatasourceServiceLookupPruneSourceOperationReceiptsProcedure, + svc.LookupPruneSourceOperationReceipts, + connect.WithSchema(datasourceServiceMethods.ByName("LookupPruneSourceOperationReceipts")), + connect.WithHandlerOptions(opts...), + ) datasourceServiceInvokeSourceOperationHandler := connect.NewUnaryHandler( DatasourceServiceInvokeSourceOperationProcedure, svc.InvokeSourceOperation, @@ -815,6 +885,10 @@ func NewDatasourceServiceHandler(svc DatasourceServiceHandler, opts ...connect.H ) return "/saas.accounts.v1.DatasourceService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { + case DatasourceServicePruneSourceOperationReceiptsProcedure: + datasourceServicePruneSourceOperationReceiptsHandler.ServeHTTP(w, r) + case DatasourceServiceLookupPruneSourceOperationReceiptsProcedure: + datasourceServiceLookupPruneSourceOperationReceiptsHandler.ServeHTTP(w, r) case DatasourceServiceInvokeSourceOperationProcedure: datasourceServiceInvokeSourceOperationHandler.ServeHTTP(w, r) case DatasourceServiceLookupInvokeSourceOperationProcedure: @@ -878,6 +952,14 @@ func NewDatasourceServiceHandler(svc DatasourceServiceHandler, opts ...connect.H // UnimplementedDatasourceServiceHandler returns CodeUnimplemented from all methods. type UnimplementedDatasourceServiceHandler struct{} +func (UnimplementedDatasourceServiceHandler) PruneSourceOperationReceipts(context.Context, *connect.Request[v1.PruneSourceOperationReceiptsRequest]) (*connect.Response[v1.PruneSourceOperationReceiptsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.DatasourceService.PruneSourceOperationReceipts is not implemented")) +} + +func (UnimplementedDatasourceServiceHandler) LookupPruneSourceOperationReceipts(context.Context, *connect.Request[v1.LookupPruneSourceOperationReceiptsRequest]) (*connect.Response[v1.PruneSourceOperationReceiptsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.DatasourceService.LookupPruneSourceOperationReceipts is not implemented")) +} + func (UnimplementedDatasourceServiceHandler) InvokeSourceOperation(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.DatasourceService.InvokeSourceOperation is not implemented")) } diff --git a/gen/saas/accounts/v1/datasource.pb.go b/gen/saas/accounts/v1/datasource.pb.go index 02cf713..09ce5e3 100644 --- a/gen/saas/accounts/v1/datasource.pb.go +++ b/gen/saas/accounts/v1/datasource.pb.go @@ -998,7 +998,9 @@ func (x *GitHubDatasourceConfig) GetFileExtensions() []string { // envelope in the credential store and are rotated in place on refresh. type ApiOAuth2Config struct { state protoimpl.MessageState `protogen:"open.v1"` - Grant ApiOAuth2Config_Grant `protobuf:"varint,4,opt,name=grant,proto3,enum=saas.accounts.v1.ApiOAuth2Config_Grant" json:"grant,omitempty"` + // AUTHORIZATION_CODE stores only this grant selector on a source; endpoint, + // client ID and scopes below come from the current deployment registration. + Grant ApiOAuth2Config_Grant `protobuf:"varint,4,opt,name=grant,proto3,enum=saas.accounts.v1.ApiOAuth2Config_Grant" json:"grant,omitempty"` // Token endpoint the refresh_token grant is exchanged at, e.g. // "https://oauth2.example.com/token". Absolute http(s) URL. TokenUrl string `protobuf:"bytes,1,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` @@ -6055,11 +6057,176 @@ func (x *LookupInvokeSourceOperationRequest) GetEffectId() string { return "" } +// Runtime-scheduled receipt retention; no host timer or queue is created. +type PruneSourceOperationReceiptsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + SourceId string `protobuf:"bytes,2,opt,name=source_id,json=sourceId,proto3" json:"source_id,omitempty"` + EffectId string `protobuf:"bytes,3,opt,name=effect_id,json=effectId,proto3" json:"effect_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PruneSourceOperationReceiptsRequest) Reset() { + *x = PruneSourceOperationReceiptsRequest{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PruneSourceOperationReceiptsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PruneSourceOperationReceiptsRequest) ProtoMessage() {} + +func (x *PruneSourceOperationReceiptsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[74] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PruneSourceOperationReceiptsRequest.ProtoReflect.Descriptor instead. +func (*PruneSourceOperationReceiptsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{74} +} + +func (x *PruneSourceOperationReceiptsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *PruneSourceOperationReceiptsRequest) GetSourceId() string { + if x != nil { + return x.SourceId + } + return "" +} + +func (x *PruneSourceOperationReceiptsRequest) GetEffectId() string { + if x != nil { + return x.EffectId + } + return "" +} + +type PruneSourceOperationReceiptsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + EffectId string `protobuf:"bytes,1,opt,name=effect_id,json=effectId,proto3" json:"effect_id,omitempty"` + Receipts int64 `protobuf:"varint,2,opt,name=receipts,proto3" json:"receipts,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` // committed or unknown after the cleanup receipt expires. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PruneSourceOperationReceiptsResponse) Reset() { + *x = PruneSourceOperationReceiptsResponse{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PruneSourceOperationReceiptsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PruneSourceOperationReceiptsResponse) ProtoMessage() {} + +func (x *PruneSourceOperationReceiptsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[75] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PruneSourceOperationReceiptsResponse.ProtoReflect.Descriptor instead. +func (*PruneSourceOperationReceiptsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{75} +} + +func (x *PruneSourceOperationReceiptsResponse) GetEffectId() string { + if x != nil { + return x.EffectId + } + return "" +} + +func (x *PruneSourceOperationReceiptsResponse) GetReceipts() int64 { + if x != nil { + return x.Receipts + } + return 0 +} + +func (x *PruneSourceOperationReceiptsResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +type LookupPruneSourceOperationReceiptsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EffectId string `protobuf:"bytes,1,opt,name=effect_id,json=effectId,proto3" json:"effect_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LookupPruneSourceOperationReceiptsRequest) Reset() { + *x = LookupPruneSourceOperationReceiptsRequest{} + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LookupPruneSourceOperationReceiptsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LookupPruneSourceOperationReceiptsRequest) ProtoMessage() {} + +func (x *LookupPruneSourceOperationReceiptsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_datasource_proto_msgTypes[76] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LookupPruneSourceOperationReceiptsRequest.ProtoReflect.Descriptor instead. +func (*LookupPruneSourceOperationReceiptsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_datasource_proto_rawDescGZIP(), []int{76} +} + +func (x *LookupPruneSourceOperationReceiptsRequest) GetEffectId() string { + if x != nil { + return x.EffectId + } + return "" +} + var File_saas_accounts_v1_datasource_proto protoreflect.FileDescriptor const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "\n" + - "!saas/accounts/v1/datasource.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a!codefly/runnable/v0/options.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"\x83\x01\n" + + "!saas/accounts/v1/datasource.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a!codefly/runnable/v0/options.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"\x83\x01\n" + "\x16GitHubDatasourceConfig\x12\x12\n" + "\x04repo\x18\x01 \x01(\tR\x04repo\x12\x14\n" + "\x05paths\x18\x02 \x03(\tR\x05paths\x12\x16\n" + @@ -6474,6 +6641,17 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "\areceipt\x18\x02 \x01(\v2(.saas.accounts.v1.SourceOperationReceiptR\areceipt\"M\n" + "\"LookupInvokeSourceOperationRequest\x12'\n" + "\teffect_id\x18\x01 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01(\x80\x01R\beffectId\"\x94\x01\n" + + "#PruneSourceOperationReceiptsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12%\n" + + "\tsource_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\bsourceId\x12%\n" + + "\teffect_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03(\x80\x01R\beffectId\"w\n" + + "$PruneSourceOperationReceiptsResponse\x12\x1b\n" + + "\teffect_id\x18\x01 \x01(\tR\beffectId\x12\x1a\n" + + "\breceipts\x18\x02 \x01(\x03R\breceipts\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\"T\n" + + ")LookupPruneSourceOperationReceiptsRequest\x12'\n" + + "\teffect_id\x18\x01 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01(\x80\x01R\beffectId*\xb7\x01\n" + "\x12DatasourceProvider\x12#\n" + "\x1fDATASOURCE_PROVIDER_UNSPECIFIED\x10\x00\x12\x1e\n" + @@ -6550,16 +6728,23 @@ const file_saas_accounts_v1_datasource_proto_rawDesc = "" + "+SOURCE_DELEGATION_REVOCATION_MEMBER_REMOVED\x10\x04\x120\n" + ",SOURCE_DELEGATION_REVOCATION_PERMISSION_LOST\x10\x05\x12.\n" + "*SOURCE_DELEGATION_REVOCATION_USER_INACTIVE\x10\x06\x120\n" + - ",SOURCE_DELEGATION_REVOCATION_BINDING_CHANGED\x10\a2\xd6*\n" + - "\x11DatasourceService\x12\xfc\x03\n" + + ",SOURCE_DELEGATION_REVOCATION_BINDING_CHANGED\x10\a2\x9e0\n" + + "\x11DatasourceService\x12\xb9\x03\n" + + "\x1cPruneSourceOperationReceipts\x125.saas.accounts.v1.PruneSourceOperationReceiptsRequest\x1a6.saas.accounts.v1.PruneSourceOperationReceiptsResponse\"\xa9\x02\xc2\xf3\x18l\b\x02\x10\x03\x1a\x11datasource:invoke\"\x11datasource:invoke*\x0f\n" + + "\tsource_id\x10\x04\x18\x030\x01:#\n" + + "\x1fsaas.datasource.receipts.pruned\x10\x02@\x03H\x04P\x03X\x03`\x01\xda\xf3\x18\xb4\x01\n" + + "\x02\b<\x12\x03\b\xd8\x04\x18\x05\"\x02\b\x02*\vUNAVAILABLE*\x12RESOURCE_EXHAUSTED*\aABORTED2\x0fsaas-datasourceJF/saas.accounts.v1.DatasourceService/LookupPruneSourceOperationReceiptsP\x80 X\x80 `\x01r\x18\n" + + "\x06source\x12\x06invoke\x12\x04read\x18\x01\x12\xe6\x01\n" + + "\"LookupPruneSourceOperationReceipts\x12;.saas.accounts.v1.LookupPruneSourceOperationReceiptsRequest\x1a6.saas.accounts.v1.PruneSourceOperationReceiptsResponse\"K\xc2\xf3\x18G\b\x02\x10\x03\x1a\x0fdatasource:read\"\x0fdatasource:read*\x0f\n" + + "\teffect_id\x10\x04\x18\x030\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x12\xfc\x03\n" + "\x15InvokeSourceOperation\x12..saas.accounts.v1.InvokeSourceOperationRequest\x1a/.saas.accounts.v1.InvokeSourceOperationResponse\"\x81\x03\xc2\xf3\x18n\b\x02\x10\x03\x1a\x11datasource:invoke\"\x11datasource:invoke*\x0f\n" + "\tsource_id\x10\x04\x18\x030\x01:%\n" + "!saas.datasource.operation.invoked\x10\x04@\x03H\x04P\x04X\x04`\x01\xda\xf3\x18\xaf\x01\n" + "\x02\b<\x12\x03\b\xd8\x04\x18\x05\"\x02\b\x02*\vUNAVAILABLE*\x12RESOURCE_EXHAUSTED*\aABORTED2\x0fsaas-datasourceJ?/saas.accounts.v1.DatasourceService/LookupInvokeSourceOperationP\x80\x80\x04X\x80\x80@`\x01r\x18\n" + "\x06source\x12\x06invoke\x12\x04read\x18\x01\x82\xd3\xe4\x93\x02U:\x01*\"P/v1/organizations/{org_id}/datasources/{source_id}/operations/{operation}:invoke\x12\x87\x02\n" + "\x1bLookupInvokeSourceOperation\x124.saas.accounts.v1.LookupInvokeSourceOperationRequest\x1a/.saas.accounts.v1.InvokeSourceOperationResponse\"\x80\x01\xc2\xf3\x18G\b\x02\x10\x03\x1a\x0fdatasource:read\"\x0fdatasource:read*\x0f\n" + - "\teffect_id\x10\x04\x18\x030\x01:\x02\x10\x01@\x01H\x03P\x03X\x04`\x01\x82\xd3\xe4\x93\x02/\x12-/v1/datasource-operation-receipts/{effect_id}\x12\xe2\x01\n" + - "\x14ListSourceOperations\x12-.saas.accounts.v1.ListSourceOperationsRequest\x1a..saas.accounts.v1.ListSourceOperationsResponse\"k\xc2\xf3\x18\"\b\x02\x10\x03*\f\n" + + "\teffect_id\x10\x04\x18\x030\x01:\x02\x10\x01@\x01H\x03P\x03X\x04`\x01\x82\xd3\xe4\x93\x02/\x12-/v1/datasource-operation-receipts/{effect_id}\x12\x85\x02\n" + + "\x14ListSourceOperations\x12-.saas.accounts.v1.ListSourceOperationsRequest\x1a..saas.accounts.v1.ListSourceOperationsResponse\"\x8d\x01\xc2\xf3\x18D\b\x02\x10\x03\x1a\x0fdatasource:read\"\x0fdatasource:read*\f\n" + "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02?\x12=/v1/organizations/{org_id}/datasources/{source_id}/operations\x12\x93\x02\n" + "\x17DeclareSourceOperations\x120.saas.accounts.v1.DeclareSourceOperationsRequest\x1a1.saas.accounts.v1.DeclareSourceOperationsResponse\"\x92\x01\xc2\xf3\x18F\b\x02\x10\x04*\f\n" + "\x06org_id\x10\x02\x18\x010\x01:&\n" + @@ -6641,101 +6826,104 @@ func file_saas_accounts_v1_datasource_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_datasource_proto_enumTypes = make([]protoimpl.EnumInfo, 14) -var file_saas_accounts_v1_datasource_proto_msgTypes = make([]protoimpl.MessageInfo, 74) +var file_saas_accounts_v1_datasource_proto_msgTypes = make([]protoimpl.MessageInfo, 77) var file_saas_accounts_v1_datasource_proto_goTypes = []any{ - (DatasourceProvider)(0), // 0: saas.accounts.v1.DatasourceProvider - (DatasourceStatus)(0), // 1: saas.accounts.v1.DatasourceStatus - (DatasourceLiveDelivery)(0), // 2: saas.accounts.v1.DatasourceLiveDelivery - (ApiCredentialKind)(0), // 3: saas.accounts.v1.ApiCredentialKind - (DatasourceConnectorInterface)(0), // 4: saas.accounts.v1.DatasourceConnectorInterface - (DatasourceCredentialMode)(0), // 5: saas.accounts.v1.DatasourceCredentialMode - (DatasourceReadersModel)(0), // 6: saas.accounts.v1.DatasourceReadersModel - (SourceSyncPhase)(0), // 7: saas.accounts.v1.SourceSyncPhase - (SourceSyncFailureReason)(0), // 8: saas.accounts.v1.SourceSyncFailureReason - (SourceSyncTrigger)(0), // 9: saas.accounts.v1.SourceSyncTrigger - (DatasourceDomainStatus)(0), // 10: saas.accounts.v1.DatasourceDomainStatus - (SourceDelegationRevocation)(0), // 11: saas.accounts.v1.SourceDelegationRevocation - (ApiOAuth2Config_Grant)(0), // 12: saas.accounts.v1.ApiOAuth2Config.Grant - (SourceOperation_Effect)(0), // 13: saas.accounts.v1.SourceOperation.Effect - (*GitHubDatasourceConfig)(nil), // 14: saas.accounts.v1.GitHubDatasourceConfig - (*ApiOAuth2Config)(nil), // 15: saas.accounts.v1.ApiOAuth2Config - (*ApiDatasourceConfig)(nil), // 16: saas.accounts.v1.ApiDatasourceConfig - (*CrawlerDatasourceConfig)(nil), // 17: saas.accounts.v1.CrawlerDatasourceConfig - (*UploadDatasourceConfig)(nil), // 18: saas.accounts.v1.UploadDatasourceConfig - (*Datasource)(nil), // 19: saas.accounts.v1.Datasource - (*AddGitHubSourceRequest)(nil), // 20: saas.accounts.v1.AddGitHubSourceRequest - (*AddGitHubSourceResponse)(nil), // 21: saas.accounts.v1.AddGitHubSourceResponse - (*AddSourceRequest)(nil), // 22: saas.accounts.v1.AddSourceRequest - (*AddSourceResponse)(nil), // 23: saas.accounts.v1.AddSourceResponse - (*DatasourceConfigField)(nil), // 24: saas.accounts.v1.DatasourceConfigField - (*DatasourceConnectorBudget)(nil), // 25: saas.accounts.v1.DatasourceConnectorBudget - (*DatasourceProviderDescriptor)(nil), // 26: saas.accounts.v1.DatasourceProviderDescriptor - (*GetDatasourceCatalogRequest)(nil), // 27: saas.accounts.v1.GetDatasourceCatalogRequest - (*GetDatasourceCatalogResponse)(nil), // 28: saas.accounts.v1.GetDatasourceCatalogResponse - (*ListSourcesRequest)(nil), // 29: saas.accounts.v1.ListSourcesRequest - (*ListSourcesResponse)(nil), // 30: saas.accounts.v1.ListSourcesResponse - (*GetSourceRequest)(nil), // 31: saas.accounts.v1.GetSourceRequest - (*GetSourceResponse)(nil), // 32: saas.accounts.v1.GetSourceResponse - (*SyncSourceRequest)(nil), // 33: saas.accounts.v1.SyncSourceRequest - (*SyncSourceResponse)(nil), // 34: saas.accounts.v1.SyncSourceResponse - (*GetSourceSyncRequest)(nil), // 35: saas.accounts.v1.GetSourceSyncRequest - (*SourceSyncDelivery)(nil), // 36: saas.accounts.v1.SourceSyncDelivery - (*SourceSyncChanges)(nil), // 37: saas.accounts.v1.SourceSyncChanges - (*SourceSyncFailure)(nil), // 38: saas.accounts.v1.SourceSyncFailure - (*SourceSyncProgress)(nil), // 39: saas.accounts.v1.SourceSyncProgress - (*GetSourceSyncResponse)(nil), // 40: saas.accounts.v1.GetSourceSyncResponse - (*DeleteSourceRequest)(nil), // 41: saas.accounts.v1.DeleteSourceRequest - (*DeleteSourceResponse)(nil), // 42: saas.accounts.v1.DeleteSourceResponse - (*GitHubAppRepository)(nil), // 43: saas.accounts.v1.GitHubAppRepository - (*BeginGitHubAppSetupRequest)(nil), // 44: saas.accounts.v1.BeginGitHubAppSetupRequest - (*BeginGitHubAppSetupResponse)(nil), // 45: saas.accounts.v1.BeginGitHubAppSetupResponse - (*CompleteGitHubAppSetupRequest)(nil), // 46: saas.accounts.v1.CompleteGitHubAppSetupRequest - (*CompleteGitHubAppSetupResponse)(nil), // 47: saas.accounts.v1.CompleteGitHubAppSetupResponse - (*MigrateGitHubSourceToAppRequest)(nil), // 48: saas.accounts.v1.MigrateGitHubSourceToAppRequest - (*MigrateGitHubSourceToAppResponse)(nil), // 49: saas.accounts.v1.MigrateGitHubSourceToAppResponse - (*DatasourceAccountLink)(nil), // 50: saas.accounts.v1.DatasourceAccountLink - (*DatasourceGroupBinding)(nil), // 51: saas.accounts.v1.DatasourceGroupBinding - (*DatasourceVerifiedDomain)(nil), // 52: saas.accounts.v1.DatasourceVerifiedDomain - (*DatasourceDirectoryTeam)(nil), // 53: saas.accounts.v1.DatasourceDirectoryTeam - (*BeginDatasourceAccountLinkRequest)(nil), // 54: saas.accounts.v1.BeginDatasourceAccountLinkRequest - (*BeginDatasourceAccountLinkResponse)(nil), // 55: saas.accounts.v1.BeginDatasourceAccountLinkResponse - (*CompleteDatasourceAccountLinkRequest)(nil), // 56: saas.accounts.v1.CompleteDatasourceAccountLinkRequest - (*CompleteDatasourceAccountLinkResponse)(nil), // 57: saas.accounts.v1.CompleteDatasourceAccountLinkResponse - (*ListMyDatasourceAccountLinksRequest)(nil), // 58: saas.accounts.v1.ListMyDatasourceAccountLinksRequest - (*ListMyDatasourceAccountLinksResponse)(nil), // 59: saas.accounts.v1.ListMyDatasourceAccountLinksResponse - (*DeleteDatasourceAccountLinkRequest)(nil), // 60: saas.accounts.v1.DeleteDatasourceAccountLinkRequest - (*DeleteDatasourceAccountLinkResponse)(nil), // 61: saas.accounts.v1.DeleteDatasourceAccountLinkResponse - (*GetDatasourceDirectoryRequest)(nil), // 62: saas.accounts.v1.GetDatasourceDirectoryRequest - (*GetDatasourceDirectoryResponse)(nil), // 63: saas.accounts.v1.GetDatasourceDirectoryResponse - (*BindDatasourceGroupRequest)(nil), // 64: saas.accounts.v1.BindDatasourceGroupRequest - (*BindDatasourceGroupResponse)(nil), // 65: saas.accounts.v1.BindDatasourceGroupResponse - (*UnbindDatasourceGroupRequest)(nil), // 66: saas.accounts.v1.UnbindDatasourceGroupRequest - (*UnbindDatasourceGroupResponse)(nil), // 67: saas.accounts.v1.UnbindDatasourceGroupResponse - (*ClaimDatasourceDomainRequest)(nil), // 68: saas.accounts.v1.ClaimDatasourceDomainRequest - (*ClaimDatasourceDomainResponse)(nil), // 69: saas.accounts.v1.ClaimDatasourceDomainResponse - (*VerifyDatasourceDomainRequest)(nil), // 70: saas.accounts.v1.VerifyDatasourceDomainRequest - (*VerifyDatasourceDomainResponse)(nil), // 71: saas.accounts.v1.VerifyDatasourceDomainResponse - (*DeleteDatasourceDomainRequest)(nil), // 72: saas.accounts.v1.DeleteDatasourceDomainRequest - (*DeleteDatasourceDomainResponse)(nil), // 73: saas.accounts.v1.DeleteDatasourceDomainResponse - (*SourceDelegation)(nil), // 74: saas.accounts.v1.SourceDelegation - (*ListSourceDelegationsRequest)(nil), // 75: saas.accounts.v1.ListSourceDelegationsRequest - (*ListSourceDelegationsResponse)(nil), // 76: saas.accounts.v1.ListSourceDelegationsResponse - (*RevokeSourceDelegationRequest)(nil), // 77: saas.accounts.v1.RevokeSourceDelegationRequest - (*RevokeSourceDelegationResponse)(nil), // 78: saas.accounts.v1.RevokeSourceDelegationResponse - (*SourceOperation)(nil), // 79: saas.accounts.v1.SourceOperation - (*DeclareSourceOperationsRequest)(nil), // 80: saas.accounts.v1.DeclareSourceOperationsRequest - (*DeclareSourceOperationsResponse)(nil), // 81: saas.accounts.v1.DeclareSourceOperationsResponse - (*ListSourceOperationsRequest)(nil), // 82: saas.accounts.v1.ListSourceOperationsRequest - (*ListSourceOperationsResponse)(nil), // 83: saas.accounts.v1.ListSourceOperationsResponse - (*InvokeSourceOperationRequest)(nil), // 84: saas.accounts.v1.InvokeSourceOperationRequest - (*SourceOperationReceipt)(nil), // 85: saas.accounts.v1.SourceOperationReceipt - (*InvokeSourceOperationResponse)(nil), // 86: saas.accounts.v1.InvokeSourceOperationResponse - (*LookupInvokeSourceOperationRequest)(nil), // 87: saas.accounts.v1.LookupInvokeSourceOperationRequest - (*timestamppb.Timestamp)(nil), // 88: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 89: google.protobuf.Duration - (v1.JobState)(0), // 90: saas.jobs.v1.JobState - (*v1.JobExecutionReference)(nil), // 91: saas.jobs.v1.JobExecutionReference - (*structpb.Struct)(nil), // 92: google.protobuf.Struct + (DatasourceProvider)(0), // 0: saas.accounts.v1.DatasourceProvider + (DatasourceStatus)(0), // 1: saas.accounts.v1.DatasourceStatus + (DatasourceLiveDelivery)(0), // 2: saas.accounts.v1.DatasourceLiveDelivery + (ApiCredentialKind)(0), // 3: saas.accounts.v1.ApiCredentialKind + (DatasourceConnectorInterface)(0), // 4: saas.accounts.v1.DatasourceConnectorInterface + (DatasourceCredentialMode)(0), // 5: saas.accounts.v1.DatasourceCredentialMode + (DatasourceReadersModel)(0), // 6: saas.accounts.v1.DatasourceReadersModel + (SourceSyncPhase)(0), // 7: saas.accounts.v1.SourceSyncPhase + (SourceSyncFailureReason)(0), // 8: saas.accounts.v1.SourceSyncFailureReason + (SourceSyncTrigger)(0), // 9: saas.accounts.v1.SourceSyncTrigger + (DatasourceDomainStatus)(0), // 10: saas.accounts.v1.DatasourceDomainStatus + (SourceDelegationRevocation)(0), // 11: saas.accounts.v1.SourceDelegationRevocation + (ApiOAuth2Config_Grant)(0), // 12: saas.accounts.v1.ApiOAuth2Config.Grant + (SourceOperation_Effect)(0), // 13: saas.accounts.v1.SourceOperation.Effect + (*GitHubDatasourceConfig)(nil), // 14: saas.accounts.v1.GitHubDatasourceConfig + (*ApiOAuth2Config)(nil), // 15: saas.accounts.v1.ApiOAuth2Config + (*ApiDatasourceConfig)(nil), // 16: saas.accounts.v1.ApiDatasourceConfig + (*CrawlerDatasourceConfig)(nil), // 17: saas.accounts.v1.CrawlerDatasourceConfig + (*UploadDatasourceConfig)(nil), // 18: saas.accounts.v1.UploadDatasourceConfig + (*Datasource)(nil), // 19: saas.accounts.v1.Datasource + (*AddGitHubSourceRequest)(nil), // 20: saas.accounts.v1.AddGitHubSourceRequest + (*AddGitHubSourceResponse)(nil), // 21: saas.accounts.v1.AddGitHubSourceResponse + (*AddSourceRequest)(nil), // 22: saas.accounts.v1.AddSourceRequest + (*AddSourceResponse)(nil), // 23: saas.accounts.v1.AddSourceResponse + (*DatasourceConfigField)(nil), // 24: saas.accounts.v1.DatasourceConfigField + (*DatasourceConnectorBudget)(nil), // 25: saas.accounts.v1.DatasourceConnectorBudget + (*DatasourceProviderDescriptor)(nil), // 26: saas.accounts.v1.DatasourceProviderDescriptor + (*GetDatasourceCatalogRequest)(nil), // 27: saas.accounts.v1.GetDatasourceCatalogRequest + (*GetDatasourceCatalogResponse)(nil), // 28: saas.accounts.v1.GetDatasourceCatalogResponse + (*ListSourcesRequest)(nil), // 29: saas.accounts.v1.ListSourcesRequest + (*ListSourcesResponse)(nil), // 30: saas.accounts.v1.ListSourcesResponse + (*GetSourceRequest)(nil), // 31: saas.accounts.v1.GetSourceRequest + (*GetSourceResponse)(nil), // 32: saas.accounts.v1.GetSourceResponse + (*SyncSourceRequest)(nil), // 33: saas.accounts.v1.SyncSourceRequest + (*SyncSourceResponse)(nil), // 34: saas.accounts.v1.SyncSourceResponse + (*GetSourceSyncRequest)(nil), // 35: saas.accounts.v1.GetSourceSyncRequest + (*SourceSyncDelivery)(nil), // 36: saas.accounts.v1.SourceSyncDelivery + (*SourceSyncChanges)(nil), // 37: saas.accounts.v1.SourceSyncChanges + (*SourceSyncFailure)(nil), // 38: saas.accounts.v1.SourceSyncFailure + (*SourceSyncProgress)(nil), // 39: saas.accounts.v1.SourceSyncProgress + (*GetSourceSyncResponse)(nil), // 40: saas.accounts.v1.GetSourceSyncResponse + (*DeleteSourceRequest)(nil), // 41: saas.accounts.v1.DeleteSourceRequest + (*DeleteSourceResponse)(nil), // 42: saas.accounts.v1.DeleteSourceResponse + (*GitHubAppRepository)(nil), // 43: saas.accounts.v1.GitHubAppRepository + (*BeginGitHubAppSetupRequest)(nil), // 44: saas.accounts.v1.BeginGitHubAppSetupRequest + (*BeginGitHubAppSetupResponse)(nil), // 45: saas.accounts.v1.BeginGitHubAppSetupResponse + (*CompleteGitHubAppSetupRequest)(nil), // 46: saas.accounts.v1.CompleteGitHubAppSetupRequest + (*CompleteGitHubAppSetupResponse)(nil), // 47: saas.accounts.v1.CompleteGitHubAppSetupResponse + (*MigrateGitHubSourceToAppRequest)(nil), // 48: saas.accounts.v1.MigrateGitHubSourceToAppRequest + (*MigrateGitHubSourceToAppResponse)(nil), // 49: saas.accounts.v1.MigrateGitHubSourceToAppResponse + (*DatasourceAccountLink)(nil), // 50: saas.accounts.v1.DatasourceAccountLink + (*DatasourceGroupBinding)(nil), // 51: saas.accounts.v1.DatasourceGroupBinding + (*DatasourceVerifiedDomain)(nil), // 52: saas.accounts.v1.DatasourceVerifiedDomain + (*DatasourceDirectoryTeam)(nil), // 53: saas.accounts.v1.DatasourceDirectoryTeam + (*BeginDatasourceAccountLinkRequest)(nil), // 54: saas.accounts.v1.BeginDatasourceAccountLinkRequest + (*BeginDatasourceAccountLinkResponse)(nil), // 55: saas.accounts.v1.BeginDatasourceAccountLinkResponse + (*CompleteDatasourceAccountLinkRequest)(nil), // 56: saas.accounts.v1.CompleteDatasourceAccountLinkRequest + (*CompleteDatasourceAccountLinkResponse)(nil), // 57: saas.accounts.v1.CompleteDatasourceAccountLinkResponse + (*ListMyDatasourceAccountLinksRequest)(nil), // 58: saas.accounts.v1.ListMyDatasourceAccountLinksRequest + (*ListMyDatasourceAccountLinksResponse)(nil), // 59: saas.accounts.v1.ListMyDatasourceAccountLinksResponse + (*DeleteDatasourceAccountLinkRequest)(nil), // 60: saas.accounts.v1.DeleteDatasourceAccountLinkRequest + (*DeleteDatasourceAccountLinkResponse)(nil), // 61: saas.accounts.v1.DeleteDatasourceAccountLinkResponse + (*GetDatasourceDirectoryRequest)(nil), // 62: saas.accounts.v1.GetDatasourceDirectoryRequest + (*GetDatasourceDirectoryResponse)(nil), // 63: saas.accounts.v1.GetDatasourceDirectoryResponse + (*BindDatasourceGroupRequest)(nil), // 64: saas.accounts.v1.BindDatasourceGroupRequest + (*BindDatasourceGroupResponse)(nil), // 65: saas.accounts.v1.BindDatasourceGroupResponse + (*UnbindDatasourceGroupRequest)(nil), // 66: saas.accounts.v1.UnbindDatasourceGroupRequest + (*UnbindDatasourceGroupResponse)(nil), // 67: saas.accounts.v1.UnbindDatasourceGroupResponse + (*ClaimDatasourceDomainRequest)(nil), // 68: saas.accounts.v1.ClaimDatasourceDomainRequest + (*ClaimDatasourceDomainResponse)(nil), // 69: saas.accounts.v1.ClaimDatasourceDomainResponse + (*VerifyDatasourceDomainRequest)(nil), // 70: saas.accounts.v1.VerifyDatasourceDomainRequest + (*VerifyDatasourceDomainResponse)(nil), // 71: saas.accounts.v1.VerifyDatasourceDomainResponse + (*DeleteDatasourceDomainRequest)(nil), // 72: saas.accounts.v1.DeleteDatasourceDomainRequest + (*DeleteDatasourceDomainResponse)(nil), // 73: saas.accounts.v1.DeleteDatasourceDomainResponse + (*SourceDelegation)(nil), // 74: saas.accounts.v1.SourceDelegation + (*ListSourceDelegationsRequest)(nil), // 75: saas.accounts.v1.ListSourceDelegationsRequest + (*ListSourceDelegationsResponse)(nil), // 76: saas.accounts.v1.ListSourceDelegationsResponse + (*RevokeSourceDelegationRequest)(nil), // 77: saas.accounts.v1.RevokeSourceDelegationRequest + (*RevokeSourceDelegationResponse)(nil), // 78: saas.accounts.v1.RevokeSourceDelegationResponse + (*SourceOperation)(nil), // 79: saas.accounts.v1.SourceOperation + (*DeclareSourceOperationsRequest)(nil), // 80: saas.accounts.v1.DeclareSourceOperationsRequest + (*DeclareSourceOperationsResponse)(nil), // 81: saas.accounts.v1.DeclareSourceOperationsResponse + (*ListSourceOperationsRequest)(nil), // 82: saas.accounts.v1.ListSourceOperationsRequest + (*ListSourceOperationsResponse)(nil), // 83: saas.accounts.v1.ListSourceOperationsResponse + (*InvokeSourceOperationRequest)(nil), // 84: saas.accounts.v1.InvokeSourceOperationRequest + (*SourceOperationReceipt)(nil), // 85: saas.accounts.v1.SourceOperationReceipt + (*InvokeSourceOperationResponse)(nil), // 86: saas.accounts.v1.InvokeSourceOperationResponse + (*LookupInvokeSourceOperationRequest)(nil), // 87: saas.accounts.v1.LookupInvokeSourceOperationRequest + (*PruneSourceOperationReceiptsRequest)(nil), // 88: saas.accounts.v1.PruneSourceOperationReceiptsRequest + (*PruneSourceOperationReceiptsResponse)(nil), // 89: saas.accounts.v1.PruneSourceOperationReceiptsResponse + (*LookupPruneSourceOperationReceiptsRequest)(nil), // 90: saas.accounts.v1.LookupPruneSourceOperationReceiptsRequest + (*timestamppb.Timestamp)(nil), // 91: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 92: google.protobuf.Duration + (v1.JobState)(0), // 93: saas.jobs.v1.JobState + (*v1.JobExecutionReference)(nil), // 94: saas.jobs.v1.JobExecutionReference + (*structpb.Struct)(nil), // 95: google.protobuf.Struct } var file_saas_accounts_v1_datasource_proto_depIdxs = []int32{ 12, // 0: saas.accounts.v1.ApiOAuth2Config.grant:type_name -> saas.accounts.v1.ApiOAuth2Config.Grant @@ -6744,15 +6932,15 @@ var file_saas_accounts_v1_datasource_proto_depIdxs = []int32{ 0, // 3: saas.accounts.v1.Datasource.provider:type_name -> saas.accounts.v1.DatasourceProvider 14, // 4: saas.accounts.v1.Datasource.github:type_name -> saas.accounts.v1.GitHubDatasourceConfig 1, // 5: saas.accounts.v1.Datasource.status:type_name -> saas.accounts.v1.DatasourceStatus - 88, // 6: saas.accounts.v1.Datasource.created_at:type_name -> google.protobuf.Timestamp - 88, // 7: saas.accounts.v1.Datasource.updated_at:type_name -> google.protobuf.Timestamp - 88, // 8: saas.accounts.v1.Datasource.last_synced_at:type_name -> google.protobuf.Timestamp + 91, // 6: saas.accounts.v1.Datasource.created_at:type_name -> google.protobuf.Timestamp + 91, // 7: saas.accounts.v1.Datasource.updated_at:type_name -> google.protobuf.Timestamp + 91, // 8: saas.accounts.v1.Datasource.last_synced_at:type_name -> google.protobuf.Timestamp 16, // 9: saas.accounts.v1.Datasource.api:type_name -> saas.accounts.v1.ApiDatasourceConfig 17, // 10: saas.accounts.v1.Datasource.crawler:type_name -> saas.accounts.v1.CrawlerDatasourceConfig 18, // 11: saas.accounts.v1.Datasource.upload:type_name -> saas.accounts.v1.UploadDatasourceConfig - 88, // 12: saas.accounts.v1.Datasource.last_ingested_at:type_name -> google.protobuf.Timestamp + 91, // 12: saas.accounts.v1.Datasource.last_ingested_at:type_name -> google.protobuf.Timestamp 2, // 13: saas.accounts.v1.Datasource.live_delivery:type_name -> saas.accounts.v1.DatasourceLiveDelivery - 89, // 14: saas.accounts.v1.Datasource.reconcile_interval:type_name -> google.protobuf.Duration + 92, // 14: saas.accounts.v1.Datasource.reconcile_interval:type_name -> google.protobuf.Duration 19, // 15: saas.accounts.v1.AddGitHubSourceResponse.datasource:type_name -> saas.accounts.v1.Datasource 0, // 16: saas.accounts.v1.AddSourceRequest.provider:type_name -> saas.accounts.v1.DatasourceProvider 14, // 17: saas.accounts.v1.AddSourceRequest.github:type_name -> saas.accounts.v1.GitHubDatasourceConfig @@ -6770,31 +6958,31 @@ var file_saas_accounts_v1_datasource_proto_depIdxs = []int32{ 26, // 29: saas.accounts.v1.GetDatasourceCatalogResponse.providers:type_name -> saas.accounts.v1.DatasourceProviderDescriptor 19, // 30: saas.accounts.v1.ListSourcesResponse.datasources:type_name -> saas.accounts.v1.Datasource 19, // 31: saas.accounts.v1.GetSourceResponse.datasource:type_name -> saas.accounts.v1.Datasource - 90, // 32: saas.accounts.v1.SourceSyncDelivery.state:type_name -> saas.jobs.v1.JobState - 91, // 33: saas.accounts.v1.SourceSyncDelivery.execution:type_name -> saas.jobs.v1.JobExecutionReference + 93, // 32: saas.accounts.v1.SourceSyncDelivery.state:type_name -> saas.jobs.v1.JobState + 94, // 33: saas.accounts.v1.SourceSyncDelivery.execution:type_name -> saas.jobs.v1.JobExecutionReference 8, // 34: saas.accounts.v1.SourceSyncFailure.reason:type_name -> saas.accounts.v1.SourceSyncFailureReason - 88, // 35: saas.accounts.v1.SourceSyncFailure.retry_at:type_name -> google.protobuf.Timestamp + 91, // 35: saas.accounts.v1.SourceSyncFailure.retry_at:type_name -> google.protobuf.Timestamp 7, // 36: saas.accounts.v1.SourceSyncProgress.phase:type_name -> saas.accounts.v1.SourceSyncPhase 9, // 37: saas.accounts.v1.SourceSyncProgress.trigger:type_name -> saas.accounts.v1.SourceSyncTrigger - 88, // 38: saas.accounts.v1.SourceSyncProgress.queued_at:type_name -> google.protobuf.Timestamp - 88, // 39: saas.accounts.v1.SourceSyncProgress.fetching_at:type_name -> google.protobuf.Timestamp - 88, // 40: saas.accounts.v1.SourceSyncProgress.compiled_at:type_name -> google.protobuf.Timestamp - 88, // 41: saas.accounts.v1.SourceSyncProgress.handed_off_at:type_name -> google.protobuf.Timestamp - 88, // 42: saas.accounts.v1.SourceSyncProgress.finished_at:type_name -> google.protobuf.Timestamp + 91, // 38: saas.accounts.v1.SourceSyncProgress.queued_at:type_name -> google.protobuf.Timestamp + 91, // 39: saas.accounts.v1.SourceSyncProgress.fetching_at:type_name -> google.protobuf.Timestamp + 91, // 40: saas.accounts.v1.SourceSyncProgress.compiled_at:type_name -> google.protobuf.Timestamp + 91, // 41: saas.accounts.v1.SourceSyncProgress.handed_off_at:type_name -> google.protobuf.Timestamp + 91, // 42: saas.accounts.v1.SourceSyncProgress.finished_at:type_name -> google.protobuf.Timestamp 37, // 43: saas.accounts.v1.SourceSyncProgress.changes:type_name -> saas.accounts.v1.SourceSyncChanges 38, // 44: saas.accounts.v1.SourceSyncProgress.failure:type_name -> saas.accounts.v1.SourceSyncFailure - 90, // 45: saas.accounts.v1.GetSourceSyncResponse.state:type_name -> saas.jobs.v1.JobState + 93, // 45: saas.accounts.v1.GetSourceSyncResponse.state:type_name -> saas.jobs.v1.JobState 36, // 46: saas.accounts.v1.GetSourceSyncResponse.deliveries:type_name -> saas.accounts.v1.SourceSyncDelivery 39, // 47: saas.accounts.v1.GetSourceSyncResponse.progress:type_name -> saas.accounts.v1.SourceSyncProgress - 88, // 48: saas.accounts.v1.BeginGitHubAppSetupResponse.expires_at:type_name -> google.protobuf.Timestamp + 91, // 48: saas.accounts.v1.BeginGitHubAppSetupResponse.expires_at:type_name -> google.protobuf.Timestamp 43, // 49: saas.accounts.v1.CompleteGitHubAppSetupResponse.repositories:type_name -> saas.accounts.v1.GitHubAppRepository 19, // 50: saas.accounts.v1.MigrateGitHubSourceToAppResponse.datasource:type_name -> saas.accounts.v1.Datasource - 88, // 51: saas.accounts.v1.DatasourceAccountLink.created_at:type_name -> google.protobuf.Timestamp - 88, // 52: saas.accounts.v1.DatasourceGroupBinding.created_at:type_name -> google.protobuf.Timestamp + 91, // 51: saas.accounts.v1.DatasourceAccountLink.created_at:type_name -> google.protobuf.Timestamp + 91, // 52: saas.accounts.v1.DatasourceGroupBinding.created_at:type_name -> google.protobuf.Timestamp 10, // 53: saas.accounts.v1.DatasourceVerifiedDomain.status:type_name -> saas.accounts.v1.DatasourceDomainStatus - 88, // 54: saas.accounts.v1.DatasourceVerifiedDomain.verified_at:type_name -> google.protobuf.Timestamp - 88, // 55: saas.accounts.v1.DatasourceVerifiedDomain.created_at:type_name -> google.protobuf.Timestamp - 88, // 56: saas.accounts.v1.BeginDatasourceAccountLinkResponse.expires_at:type_name -> google.protobuf.Timestamp + 91, // 54: saas.accounts.v1.DatasourceVerifiedDomain.verified_at:type_name -> google.protobuf.Timestamp + 91, // 55: saas.accounts.v1.DatasourceVerifiedDomain.created_at:type_name -> google.protobuf.Timestamp + 91, // 56: saas.accounts.v1.BeginDatasourceAccountLinkResponse.expires_at:type_name -> google.protobuf.Timestamp 50, // 57: saas.accounts.v1.CompleteDatasourceAccountLinkResponse.link:type_name -> saas.accounts.v1.DatasourceAccountLink 50, // 58: saas.accounts.v1.ListMyDatasourceAccountLinksResponse.links:type_name -> saas.accounts.v1.DatasourceAccountLink 50, // 59: saas.accounts.v1.GetDatasourceDirectoryResponse.links:type_name -> saas.accounts.v1.DatasourceAccountLink @@ -6804,74 +6992,78 @@ var file_saas_accounts_v1_datasource_proto_depIdxs = []int32{ 51, // 63: saas.accounts.v1.BindDatasourceGroupResponse.binding:type_name -> saas.accounts.v1.DatasourceGroupBinding 52, // 64: saas.accounts.v1.ClaimDatasourceDomainResponse.domain:type_name -> saas.accounts.v1.DatasourceVerifiedDomain 52, // 65: saas.accounts.v1.VerifyDatasourceDomainResponse.domain:type_name -> saas.accounts.v1.DatasourceVerifiedDomain - 88, // 66: saas.accounts.v1.SourceDelegation.created_at:type_name -> google.protobuf.Timestamp - 88, // 67: saas.accounts.v1.SourceDelegation.revoked_at:type_name -> google.protobuf.Timestamp + 91, // 66: saas.accounts.v1.SourceDelegation.created_at:type_name -> google.protobuf.Timestamp + 91, // 67: saas.accounts.v1.SourceDelegation.revoked_at:type_name -> google.protobuf.Timestamp 11, // 68: saas.accounts.v1.SourceDelegation.revocation:type_name -> saas.accounts.v1.SourceDelegationRevocation 74, // 69: saas.accounts.v1.ListSourceDelegationsResponse.delegations:type_name -> saas.accounts.v1.SourceDelegation 74, // 70: saas.accounts.v1.RevokeSourceDelegationResponse.delegation:type_name -> saas.accounts.v1.SourceDelegation - 92, // 71: saas.accounts.v1.SourceOperation.input_schema:type_name -> google.protobuf.Struct - 92, // 72: saas.accounts.v1.SourceOperation.output_schema:type_name -> google.protobuf.Struct + 95, // 71: saas.accounts.v1.SourceOperation.input_schema:type_name -> google.protobuf.Struct + 95, // 72: saas.accounts.v1.SourceOperation.output_schema:type_name -> google.protobuf.Struct 13, // 73: saas.accounts.v1.SourceOperation.effect:type_name -> saas.accounts.v1.SourceOperation.Effect 79, // 74: saas.accounts.v1.DeclareSourceOperationsRequest.operations:type_name -> saas.accounts.v1.SourceOperation 79, // 75: saas.accounts.v1.DeclareSourceOperationsResponse.operations:type_name -> saas.accounts.v1.SourceOperation 79, // 76: saas.accounts.v1.ListSourceOperationsResponse.operations:type_name -> saas.accounts.v1.SourceOperation 85, // 77: saas.accounts.v1.InvokeSourceOperationResponse.receipt:type_name -> saas.accounts.v1.SourceOperationReceipt - 84, // 78: saas.accounts.v1.DatasourceService.InvokeSourceOperation:input_type -> saas.accounts.v1.InvokeSourceOperationRequest - 87, // 79: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:input_type -> saas.accounts.v1.LookupInvokeSourceOperationRequest - 82, // 80: saas.accounts.v1.DatasourceService.ListSourceOperations:input_type -> saas.accounts.v1.ListSourceOperationsRequest - 80, // 81: saas.accounts.v1.DatasourceService.DeclareSourceOperations:input_type -> saas.accounts.v1.DeclareSourceOperationsRequest - 20, // 82: saas.accounts.v1.DatasourceService.AddGitHubSource:input_type -> saas.accounts.v1.AddGitHubSourceRequest - 22, // 83: saas.accounts.v1.DatasourceService.AddSource:input_type -> saas.accounts.v1.AddSourceRequest - 27, // 84: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:input_type -> saas.accounts.v1.GetDatasourceCatalogRequest - 29, // 85: saas.accounts.v1.DatasourceService.ListSources:input_type -> saas.accounts.v1.ListSourcesRequest - 31, // 86: saas.accounts.v1.DatasourceService.GetSource:input_type -> saas.accounts.v1.GetSourceRequest - 33, // 87: saas.accounts.v1.DatasourceService.SyncSource:input_type -> saas.accounts.v1.SyncSourceRequest - 35, // 88: saas.accounts.v1.DatasourceService.GetSourceSync:input_type -> saas.accounts.v1.GetSourceSyncRequest - 41, // 89: saas.accounts.v1.DatasourceService.DeleteSource:input_type -> saas.accounts.v1.DeleteSourceRequest - 44, // 90: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:input_type -> saas.accounts.v1.BeginGitHubAppSetupRequest - 46, // 91: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:input_type -> saas.accounts.v1.CompleteGitHubAppSetupRequest - 48, // 92: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:input_type -> saas.accounts.v1.MigrateGitHubSourceToAppRequest - 54, // 93: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:input_type -> saas.accounts.v1.BeginDatasourceAccountLinkRequest - 56, // 94: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:input_type -> saas.accounts.v1.CompleteDatasourceAccountLinkRequest - 58, // 95: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:input_type -> saas.accounts.v1.ListMyDatasourceAccountLinksRequest - 60, // 96: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:input_type -> saas.accounts.v1.DeleteDatasourceAccountLinkRequest - 62, // 97: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:input_type -> saas.accounts.v1.GetDatasourceDirectoryRequest - 64, // 98: saas.accounts.v1.DatasourceService.BindDatasourceGroup:input_type -> saas.accounts.v1.BindDatasourceGroupRequest - 66, // 99: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:input_type -> saas.accounts.v1.UnbindDatasourceGroupRequest - 68, // 100: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:input_type -> saas.accounts.v1.ClaimDatasourceDomainRequest - 70, // 101: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:input_type -> saas.accounts.v1.VerifyDatasourceDomainRequest - 72, // 102: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:input_type -> saas.accounts.v1.DeleteDatasourceDomainRequest - 75, // 103: saas.accounts.v1.DatasourceService.ListSourceDelegations:input_type -> saas.accounts.v1.ListSourceDelegationsRequest - 77, // 104: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:input_type -> saas.accounts.v1.RevokeSourceDelegationRequest - 86, // 105: saas.accounts.v1.DatasourceService.InvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse - 86, // 106: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse - 83, // 107: saas.accounts.v1.DatasourceService.ListSourceOperations:output_type -> saas.accounts.v1.ListSourceOperationsResponse - 81, // 108: saas.accounts.v1.DatasourceService.DeclareSourceOperations:output_type -> saas.accounts.v1.DeclareSourceOperationsResponse - 21, // 109: saas.accounts.v1.DatasourceService.AddGitHubSource:output_type -> saas.accounts.v1.AddGitHubSourceResponse - 23, // 110: saas.accounts.v1.DatasourceService.AddSource:output_type -> saas.accounts.v1.AddSourceResponse - 28, // 111: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:output_type -> saas.accounts.v1.GetDatasourceCatalogResponse - 30, // 112: saas.accounts.v1.DatasourceService.ListSources:output_type -> saas.accounts.v1.ListSourcesResponse - 32, // 113: saas.accounts.v1.DatasourceService.GetSource:output_type -> saas.accounts.v1.GetSourceResponse - 34, // 114: saas.accounts.v1.DatasourceService.SyncSource:output_type -> saas.accounts.v1.SyncSourceResponse - 40, // 115: saas.accounts.v1.DatasourceService.GetSourceSync:output_type -> saas.accounts.v1.GetSourceSyncResponse - 42, // 116: saas.accounts.v1.DatasourceService.DeleteSource:output_type -> saas.accounts.v1.DeleteSourceResponse - 45, // 117: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:output_type -> saas.accounts.v1.BeginGitHubAppSetupResponse - 47, // 118: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:output_type -> saas.accounts.v1.CompleteGitHubAppSetupResponse - 49, // 119: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:output_type -> saas.accounts.v1.MigrateGitHubSourceToAppResponse - 55, // 120: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:output_type -> saas.accounts.v1.BeginDatasourceAccountLinkResponse - 57, // 121: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:output_type -> saas.accounts.v1.CompleteDatasourceAccountLinkResponse - 59, // 122: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:output_type -> saas.accounts.v1.ListMyDatasourceAccountLinksResponse - 61, // 123: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:output_type -> saas.accounts.v1.DeleteDatasourceAccountLinkResponse - 63, // 124: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:output_type -> saas.accounts.v1.GetDatasourceDirectoryResponse - 65, // 125: saas.accounts.v1.DatasourceService.BindDatasourceGroup:output_type -> saas.accounts.v1.BindDatasourceGroupResponse - 67, // 126: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:output_type -> saas.accounts.v1.UnbindDatasourceGroupResponse - 69, // 127: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:output_type -> saas.accounts.v1.ClaimDatasourceDomainResponse - 71, // 128: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:output_type -> saas.accounts.v1.VerifyDatasourceDomainResponse - 73, // 129: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:output_type -> saas.accounts.v1.DeleteDatasourceDomainResponse - 76, // 130: saas.accounts.v1.DatasourceService.ListSourceDelegations:output_type -> saas.accounts.v1.ListSourceDelegationsResponse - 78, // 131: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:output_type -> saas.accounts.v1.RevokeSourceDelegationResponse - 105, // [105:132] is the sub-list for method output_type - 78, // [78:105] is the sub-list for method input_type + 88, // 78: saas.accounts.v1.DatasourceService.PruneSourceOperationReceipts:input_type -> saas.accounts.v1.PruneSourceOperationReceiptsRequest + 90, // 79: saas.accounts.v1.DatasourceService.LookupPruneSourceOperationReceipts:input_type -> saas.accounts.v1.LookupPruneSourceOperationReceiptsRequest + 84, // 80: saas.accounts.v1.DatasourceService.InvokeSourceOperation:input_type -> saas.accounts.v1.InvokeSourceOperationRequest + 87, // 81: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:input_type -> saas.accounts.v1.LookupInvokeSourceOperationRequest + 82, // 82: saas.accounts.v1.DatasourceService.ListSourceOperations:input_type -> saas.accounts.v1.ListSourceOperationsRequest + 80, // 83: saas.accounts.v1.DatasourceService.DeclareSourceOperations:input_type -> saas.accounts.v1.DeclareSourceOperationsRequest + 20, // 84: saas.accounts.v1.DatasourceService.AddGitHubSource:input_type -> saas.accounts.v1.AddGitHubSourceRequest + 22, // 85: saas.accounts.v1.DatasourceService.AddSource:input_type -> saas.accounts.v1.AddSourceRequest + 27, // 86: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:input_type -> saas.accounts.v1.GetDatasourceCatalogRequest + 29, // 87: saas.accounts.v1.DatasourceService.ListSources:input_type -> saas.accounts.v1.ListSourcesRequest + 31, // 88: saas.accounts.v1.DatasourceService.GetSource:input_type -> saas.accounts.v1.GetSourceRequest + 33, // 89: saas.accounts.v1.DatasourceService.SyncSource:input_type -> saas.accounts.v1.SyncSourceRequest + 35, // 90: saas.accounts.v1.DatasourceService.GetSourceSync:input_type -> saas.accounts.v1.GetSourceSyncRequest + 41, // 91: saas.accounts.v1.DatasourceService.DeleteSource:input_type -> saas.accounts.v1.DeleteSourceRequest + 44, // 92: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:input_type -> saas.accounts.v1.BeginGitHubAppSetupRequest + 46, // 93: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:input_type -> saas.accounts.v1.CompleteGitHubAppSetupRequest + 48, // 94: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:input_type -> saas.accounts.v1.MigrateGitHubSourceToAppRequest + 54, // 95: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:input_type -> saas.accounts.v1.BeginDatasourceAccountLinkRequest + 56, // 96: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:input_type -> saas.accounts.v1.CompleteDatasourceAccountLinkRequest + 58, // 97: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:input_type -> saas.accounts.v1.ListMyDatasourceAccountLinksRequest + 60, // 98: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:input_type -> saas.accounts.v1.DeleteDatasourceAccountLinkRequest + 62, // 99: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:input_type -> saas.accounts.v1.GetDatasourceDirectoryRequest + 64, // 100: saas.accounts.v1.DatasourceService.BindDatasourceGroup:input_type -> saas.accounts.v1.BindDatasourceGroupRequest + 66, // 101: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:input_type -> saas.accounts.v1.UnbindDatasourceGroupRequest + 68, // 102: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:input_type -> saas.accounts.v1.ClaimDatasourceDomainRequest + 70, // 103: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:input_type -> saas.accounts.v1.VerifyDatasourceDomainRequest + 72, // 104: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:input_type -> saas.accounts.v1.DeleteDatasourceDomainRequest + 75, // 105: saas.accounts.v1.DatasourceService.ListSourceDelegations:input_type -> saas.accounts.v1.ListSourceDelegationsRequest + 77, // 106: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:input_type -> saas.accounts.v1.RevokeSourceDelegationRequest + 89, // 107: saas.accounts.v1.DatasourceService.PruneSourceOperationReceipts:output_type -> saas.accounts.v1.PruneSourceOperationReceiptsResponse + 89, // 108: saas.accounts.v1.DatasourceService.LookupPruneSourceOperationReceipts:output_type -> saas.accounts.v1.PruneSourceOperationReceiptsResponse + 86, // 109: saas.accounts.v1.DatasourceService.InvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse + 86, // 110: saas.accounts.v1.DatasourceService.LookupInvokeSourceOperation:output_type -> saas.accounts.v1.InvokeSourceOperationResponse + 83, // 111: saas.accounts.v1.DatasourceService.ListSourceOperations:output_type -> saas.accounts.v1.ListSourceOperationsResponse + 81, // 112: saas.accounts.v1.DatasourceService.DeclareSourceOperations:output_type -> saas.accounts.v1.DeclareSourceOperationsResponse + 21, // 113: saas.accounts.v1.DatasourceService.AddGitHubSource:output_type -> saas.accounts.v1.AddGitHubSourceResponse + 23, // 114: saas.accounts.v1.DatasourceService.AddSource:output_type -> saas.accounts.v1.AddSourceResponse + 28, // 115: saas.accounts.v1.DatasourceService.GetDatasourceCatalog:output_type -> saas.accounts.v1.GetDatasourceCatalogResponse + 30, // 116: saas.accounts.v1.DatasourceService.ListSources:output_type -> saas.accounts.v1.ListSourcesResponse + 32, // 117: saas.accounts.v1.DatasourceService.GetSource:output_type -> saas.accounts.v1.GetSourceResponse + 34, // 118: saas.accounts.v1.DatasourceService.SyncSource:output_type -> saas.accounts.v1.SyncSourceResponse + 40, // 119: saas.accounts.v1.DatasourceService.GetSourceSync:output_type -> saas.accounts.v1.GetSourceSyncResponse + 42, // 120: saas.accounts.v1.DatasourceService.DeleteSource:output_type -> saas.accounts.v1.DeleteSourceResponse + 45, // 121: saas.accounts.v1.DatasourceService.BeginGitHubAppSetup:output_type -> saas.accounts.v1.BeginGitHubAppSetupResponse + 47, // 122: saas.accounts.v1.DatasourceService.CompleteGitHubAppSetup:output_type -> saas.accounts.v1.CompleteGitHubAppSetupResponse + 49, // 123: saas.accounts.v1.DatasourceService.MigrateGitHubSourceToApp:output_type -> saas.accounts.v1.MigrateGitHubSourceToAppResponse + 55, // 124: saas.accounts.v1.DatasourceService.BeginDatasourceAccountLink:output_type -> saas.accounts.v1.BeginDatasourceAccountLinkResponse + 57, // 125: saas.accounts.v1.DatasourceService.CompleteDatasourceAccountLink:output_type -> saas.accounts.v1.CompleteDatasourceAccountLinkResponse + 59, // 126: saas.accounts.v1.DatasourceService.ListMyDatasourceAccountLinks:output_type -> saas.accounts.v1.ListMyDatasourceAccountLinksResponse + 61, // 127: saas.accounts.v1.DatasourceService.DeleteDatasourceAccountLink:output_type -> saas.accounts.v1.DeleteDatasourceAccountLinkResponse + 63, // 128: saas.accounts.v1.DatasourceService.GetDatasourceDirectory:output_type -> saas.accounts.v1.GetDatasourceDirectoryResponse + 65, // 129: saas.accounts.v1.DatasourceService.BindDatasourceGroup:output_type -> saas.accounts.v1.BindDatasourceGroupResponse + 67, // 130: saas.accounts.v1.DatasourceService.UnbindDatasourceGroup:output_type -> saas.accounts.v1.UnbindDatasourceGroupResponse + 69, // 131: saas.accounts.v1.DatasourceService.ClaimDatasourceDomain:output_type -> saas.accounts.v1.ClaimDatasourceDomainResponse + 71, // 132: saas.accounts.v1.DatasourceService.VerifyDatasourceDomain:output_type -> saas.accounts.v1.VerifyDatasourceDomainResponse + 73, // 133: saas.accounts.v1.DatasourceService.DeleteDatasourceDomain:output_type -> saas.accounts.v1.DeleteDatasourceDomainResponse + 76, // 134: saas.accounts.v1.DatasourceService.ListSourceDelegations:output_type -> saas.accounts.v1.ListSourceDelegationsResponse + 78, // 135: saas.accounts.v1.DatasourceService.RevokeSourceDelegation:output_type -> saas.accounts.v1.RevokeSourceDelegationResponse + 107, // [107:136] is the sub-list for method output_type + 78, // [78:107] is the sub-list for method input_type 78, // [78:78] is the sub-list for extension type_name 78, // [78:78] is the sub-list for extension extendee 0, // [0:78] is the sub-list for field type_name @@ -6901,7 +7093,7 @@ func file_saas_accounts_v1_datasource_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_datasource_proto_rawDesc), len(file_saas_accounts_v1_datasource_proto_rawDesc)), NumEnums: 14, - NumMessages: 74, + NumMessages: 77, NumExtensions: 0, NumServices: 1, }, From 00aa1cf8fd590711238df517d6e3336d933e1cbf Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:46:13 -0400 Subject: [PATCH 08/12] fix(datasource): preserve removed declaration outcomes --- CHANGELOG.md | 11 +- README.md | 27 ++- SOURCE.txt | 2 +- datasource/doc.go | 6 + datasource/doc_test.go | 2 +- datasource/errors.go | 31 ++++ datasource/operations.go | 18 +- datasource/removed_operation_test.go | 172 ++++++++++++++++++ datasource/types.go | 5 +- .../accountsv1connect/datasource.connect.go | 10 + 10 files changed, 268 insertions(+), 16 deletions(-) create mode 100644 datasource/removed_operation_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d8018b..ac7b856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ from the host's `provider_status` metadata (canonical HTTP 100–599, zero when absent or invalid). `InputError.Pointer` preserves the host's first `BadRequest.FieldViolation.field`. Host messages remain unchanged. +- Add `OperationDeclarationRemoved` / `ErrOperationDeclarationRemoved` for + `SOURCE_OPERATION_DECLARATION_REMOVED`. It preserves the original effect ID + and `receipt_status` as `ReceiptStatus` and `Receipt.Status` (COMMITTED or + UNKNOWN; absent/unrecognized metadata becomes UNKNOWN). Recover with Lookup, + never with a fresh ID; the SDK never retries or invents receipt output. +- Remove the old Lookup caveat: the host now recovers saved receipts after + declaration removal/replacement, under current source read authority. + Committed receipts survive; unresolved/expired stays unknown. Lookup NotFound + means no attempt marker exists. - Lookup misses return the original ID with `NOT_ATTEMPTED`; callers may explicitly reinvoke with the same ID. Committed receipts include `Output`, so recovery can retrieve output without another invoke. Receipt statuses are @@ -35,7 +44,7 @@ - Update `golang.org/x/tools` so the API boundary gate can read Go 1.27 export data; the gate and its fixtures are unchanged. - Regenerate the full host API from PR #1052 head - `86ee51fd8b4d66aad9da2509483e14d3c7534764`, recorded in `SOURCE.txt` with module + `2a16c688c61d66645e1063c495fa38eaf2fa52de`, recorded in `SOURCE.txt` with module version `0.1.0`. This is a development PR head, not a release tag. Release waits for the host tag and regeneration from its commit. - Include the runtime-facing `PruneSourceOperationReceipts` and diff --git a/README.md b/README.md index 439ae7f..66ccb23 100644 --- a/README.md +++ b/README.md @@ -85,11 +85,22 @@ The SDK surfaces are: stored effect binding. These two arguments cannot select or override lookup authority. A lookup miss returns `ErrEffectNotFound` and a receipt carrying the same ID with status `NOT_ATTEMPTED`; an explicit reinvoke with that ID is - safe, and the SDK never invokes automatically. The host documents - `source effect not found` as never attempted. Its implementation at the - recorded ref can also return NotFound when the declaration disappeared - after an attempt; a lookup miss is not proof the provider never acted. - Keep the original ID in either case. + safe, and the SDK never invokes automatically. NotFound means the host has + no attempt marker. Once an attempt exists, Lookup recovers the saved receipt + independently of the current declaration: committed receipts survive its + removal or replacement, and unresolved or expired receipts remain unknown. + The caller's current source read authority still applies; revoked access refuses. + + Re-invoking a retained effect after its declaration is removed returns + `*OperationDeclarationRemoved` (`errors.Is(err, ErrOperationDeclarationRemoved)`). + Its `ReceiptStatus` and `Receipt.Status` carry the host's `receipt_status`: + exact `committed` becomes `COMMITTED`; `unknown`, missing or unrecognized + metadata becomes `UNKNOWN`. The outcome and `Result.Receipt` retain the + original effect ID. This is distinct from a refusal and from `OutcomeUnknown`, + even when its receipt status is unknown. No output, commit time or provider + status is inferred from the error. **Recover with Lookup, never with a fresh ID.** + A committed status here means the saved output must be retrieved through Lookup. + An unknown receipt is returned alongside `ErrOutcomeUnknown`; check it with `errors.Is`. **Unknown is an outcome, not evidence of failure or permission to repeat an effect.** Invoke also @@ -99,7 +110,8 @@ The SDK surfaces are: likewise returns `ErrOutcomeUnknown` with the original ID. The Connect decoder identifies RPC errors; decoded errors retain their code/reason mappings. Check - `errors.Is(err, datasource.ErrOutcomeUnknown)` **before** transport-code + `errors.Is(err, datasource.ErrOutcomeUnknown)` and + `errors.Is(err, datasource.ErrOperationDeclarationRemoved)` **before** transport-code retry logic: the wrapped Connect error remains available for diagnostics, but its code does not establish whether the effect happened. An unavailable lookup also returns an unknown receipt: it cannot establish what happened to the effect. Use a @@ -142,6 +154,7 @@ The SDK surfaces are: | InvalidArgument | `*InputError`, with `Pointer` from the first `BadRequest.FieldViolation.field` | | ResourceExhausted + DATASOURCE_RATE_LIMITED | `*RateLimited`, with `ResetAt` from `reset_at` or `RetryInfo` | | FailedPrecondition + SOURCE_OPERATION_OUTCOME_UNKNOWN | receipt plus `ErrOutcomeUnknown` (`errors.Is`); the mutation may have happened | + | FailedPrecondition + SOURCE_OPERATION_DECLARATION_REMOVED | receipt plus `*OperationDeclarationRemoved` / `ErrOperationDeclarationRemoved`; recover the original ID with Lookup | | FailedPrecondition + SOURCE_PROVIDER_REFUSED | `*ProviderRefused`, with `Status` from `ErrorInfo.metadata["provider_status"]` when known | | FailedPrecondition + SOURCE_EFFECT_REUSED | `ErrEffectReused` (`errors.Is`) | | Other FailedPrecondition | `*OperationRefused`, with the structured `Reason` or an empty string | @@ -164,7 +177,7 @@ The SDK surfaces are: metadata. These names mirror the Python facade's `invoke`, `lookup`, `declare_operations`, `list_operations`, `Operation`, `Result`, `Receipt`, `NotPermitted`, `UnknownOperation`, `EffectNotFound`, `EffectReused`, - `InputError`, `RateLimited`, `ProviderRefused`, `OperationRefused` and + `InputError`, `RateLimited`, `ProviderRefused`, `OperationRefused`, `OperationDeclarationRemoved` and `OutcomeUnknown`. This call surface uses the four host RPCs from diff --git a/SOURCE.txt b/SOURCE.txt index e6212e1..4345fa7 100644 --- a/SOURCE.txt +++ b/SOURCE.txt @@ -1,4 +1,4 @@ -gen/ generated from codefly-dev/module-saas-starter@86ee51fd8b4d66aad9da2509483e14d3c7534764 +gen/ generated from codefly-dev/module-saas-starter@2a16c688c61d66645e1063c495fa38eaf2fa52de (accounts public proto: module/services/accounts/proto) module version: 0.1.0 (module/module.package.codefly.yaml) diff --git a/datasource/doc.go b/datasource/doc.go index a9f4f86..82483dd 100644 --- a/datasource/doc.go +++ b/datasource/doc.go @@ -29,4 +29,10 @@ // new ID to retry. See Example_invoke for the complete compiled example. // Lookup's ErrEffectNotFound retains the ID for a safe explicit same-ID invoke; // committed receipts include Output without another invocation. +// +// ErrOperationDeclarationRemoved is a distinct recovery outcome carrying the +// retained effect's ReceiptStatus (COMMITTED or UNKNOWN). Recover with Lookup, +// never with a fresh ID: a committed status here does not include the saved +// output. Lookup survives declaration removal/replacement under current source +// read authority; unresolved or expired receipts remain unknown. package datasource diff --git a/datasource/doc_test.go b/datasource/doc_test.go index f1b2143..551092f 100644 --- a/datasource/doc_test.go +++ b/datasource/doc_test.go @@ -16,7 +16,7 @@ func Example_invoke() { ds := datasource.New(gw) result, err := ds.Invoke(ctx, "org-id", "source-id", "list_invoices", map[string]any{"limit": 20}, datasource.WithDeadline(time.Now().Add(5*time.Second))) - if errors.Is(err, datasource.ErrOutcomeUnknown) { + if errors.Is(err, datasource.ErrOutcomeUnknown) || errors.Is(err, datasource.ErrOperationDeclarationRemoved) { // Keep the original request values (including the person's Work Context) // while replacing its expired cancellation/deadline for this lookup. lookupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) diff --git a/datasource/errors.go b/datasource/errors.go index 42289f9..8d80257 100644 --- a/datasource/errors.go +++ b/datasource/errors.go @@ -17,6 +17,9 @@ var ( ErrEffectNotFound = errors.New("datasource: effect not found") // ErrEffectReused means the host rejected changed input under an existing ID. ErrEffectReused = errors.New("datasource: effect ID reused") + // ErrOperationDeclarationRemoved means a retained effect's declaration was + // removed. Recover with Lookup under the original ID; never mint a fresh ID. + ErrOperationDeclarationRemoved = errors.New("datasource: operation declaration removed; look up the original effect") // ErrOutcomeUnknown means the effect may have happened. Inspect the receipt // with Lookup; never interpret this outcome as permission to repeat it. ErrOutcomeUnknown = errors.New("datasource: outcome unknown; look up the effect") @@ -83,6 +86,28 @@ func (e *OperationRefused) Error() string { } func (e *OperationRefused) Unwrap() error { return e.cause } +// OperationDeclarationRemoved reports retained effect evidence after its +// declaration was removed. ReceiptStatus and Receipt.Status carry the host's +// observation: COMMITTED or UNKNOWN (also used for missing/invalid metadata). +// This outcome has no output, commit time or provider status; recover those +// with Lookup using Receipt.EffectID. Never recover with a fresh ID. +type OperationDeclarationRemoved struct { + ReceiptStatus ReceiptStatus + Receipt Receipt + cause error +} + +func (e *OperationDeclarationRemoved) Error() string { + if e.cause == nil { + return ErrOperationDeclarationRemoved.Error() + } + return e.cause.Error() +} +func (e *OperationDeclarationRemoved) Is(target error) bool { + return target == ErrOperationDeclarationRemoved +} +func (e *OperationDeclarationRemoved) Unwrap() error { return e.cause } + type hostError struct { kind error cause error @@ -175,6 +200,12 @@ func mapHostError(err error, effectOutcome bool, now time.Time) error { switch info.GetReason() { case "SOURCE_OPERATION_OUTCOME_UNKNOWN": return &OutcomeUnknown{cause: err} + case "SOURCE_OPERATION_DECLARATION_REMOVED": + status := ReceiptStatusUnknown + if info.GetMetadata()["receipt_status"] == "committed" { + status = ReceiptStatusCommitted + } + return &OperationDeclarationRemoved{ReceiptStatus: status, Receipt: Receipt{Status: status}, cause: err} case "SOURCE_PROVIDER_REFUSED": encoded := info.GetMetadata()["provider_status"] status, parseErr := strconv.Atoi(encoded) diff --git a/datasource/operations.go b/datasource/operations.go index 371d9ab..93e1006 100644 --- a/datasource/operations.go +++ b/datasource/operations.go @@ -17,6 +17,8 @@ import ( // person. The host owns provider credentials, routing, replay and the receipt. // On an RPC error the returned Result still carries the effect ID. An unknown // outcome must be looked up, not interpreted as permission to repeat an effect. +// OperationDeclarationRemoved also requires Lookup with the original ID; +// its receipt status never authorizes recovery with a fresh ID. func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, input any, opts ...InvokeOption) (*Result, error) { value, err := inputJSON(input) if err != nil { @@ -57,6 +59,11 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, if errors.As(mapped, &unknown) { unknown.Receipt = result.Receipt } + var removed *OperationDeclarationRemoved + if errors.As(mapped, &removed) { + result.Receipt.Status = removed.ReceiptStatus + removed.Receipt = result.Receipt + } return result, mapped } receipt, err := operationReceipt(response.Msg.GetReceipt(), options.effectID) @@ -76,9 +83,9 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, // unknown outcome is returned alongside ErrOutcomeUnknown (errors.Is works). // An unavailable lookup also leaves the outcome unknown; it is never a reason // to dispatch the effect again. -// A NotFound response returns ErrEffectNotFound and the original ID for an -// explicit same-ID invoke. At the recorded host ref, a removed declaration can -// also produce NotFound; it is not proof that the provider never acted. +// A NotFound response means no attempt exists and returns ErrEffectNotFound +// with the original ID for an explicit same-ID invoke. Saved receipts survive +// declaration removal or replacement; current source read authority still applies. // orgID and sourceID preserve the shared SDK signature; the host derives their // authority from the gateway's Work Context and the stored effect binding. func (c *Client) Lookup(ctx context.Context, orgID, sourceID, effectID string) (*Receipt, error) { @@ -105,6 +112,11 @@ func (c *Client) Lookup(ctx context.Context, orgID, sourceID, effectID string) ( unknown.Receipt = Receipt{EffectID: effectID, Status: ReceiptStatusUnknown} return &unknown.Receipt, mapped } + var removed *OperationDeclarationRemoved + if errors.As(mapped, &removed) { + removed.Receipt.EffectID = effectID + return &removed.Receipt, mapped + } return nil, mapped } receipt, err := operationReceipt(response.Msg.GetReceipt(), effectID) diff --git a/datasource/removed_operation_test.go b/datasource/removed_operation_test.go new file mode 100644 index 0000000..b90b52f --- /dev/null +++ b/datasource/removed_operation_test.go @@ -0,0 +1,172 @@ +package datasource_test + +import ( + "context" + "errors" + "fmt" + "reflect" + "sync/atomic" + "testing" + + "connectrpc.com/connect" + "google.golang.org/genproto/googleapis/rpc/errdetails" + + "github.com/codefly-dev/saas-sdk-go/datasource" + v1 "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1" +) + +func declarationRemovedError(t *testing.T, metadata map[string]string) *connect.Error { + t.Helper() + err := connect.NewError(connect.CodeFailedPrecondition, errors.New("source operation declaration was removed; recover the original effect with Lookup")) + detail, detailErr := connect.NewErrorDetail(&errdetails.ErrorInfo{ + Reason: "SOURCE_OPERATION_DECLARATION_REMOVED", Domain: "saas.accounts.v1", Metadata: metadata, + }) + if detailErr != nil { + t.Fatal(detailErr) + } + err.AddDetail(detail) + return err +} + +func TestOperationDeclarationRemovedOverConnect(t *testing.T) { + for _, tt := range []struct { + name string + metadata map[string]string + status datasource.ReceiptStatus + }{ + {"committed", map[string]string{"receipt_status": "committed"}, datasource.ReceiptStatusCommitted}, + {"unknown", map[string]string{"receipt_status": "unknown"}, datasource.ReceiptStatusUnknown}, + {"missing", nil, datasource.ReceiptStatusUnknown}, + {"empty", map[string]string{"receipt_status": ""}, datasource.ReceiptStatusUnknown}, + {"uppercase", map[string]string{"receipt_status": "COMMITTED"}, datasource.ReceiptStatusUnknown}, + {"whitespace", map[string]string{"receipt_status": " committed "}, datasource.ReceiptStatusUnknown}, + {"not attempted", map[string]string{"receipt_status": "not_attempted"}, datasource.ReceiptStatusUnknown}, + {"unrecognized", map[string]string{"receipt_status": "future"}, datasource.ReceiptStatusUnknown}, + } { + for _, suppliedID := range []bool{false, true} { + for _, jsonCodec := range []bool{false, true} { + t.Run(fmt.Sprintf("%s/provided=%t/json=%t", tt.name, suppliedID, jsonCodec), func(t *testing.T) { + var invokes, lookups atomic.Int32 + hostError := declarationRemovedError(t, tt.metadata) + var options []connect.ClientOption + if jsonCodec { + options = append(options, connect.WithProtoJSON()) + } + var sentID atomic.Value + c := newOperationsClient(t, &operationsHandler{ + invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + invokes.Add(1) + sentID.Store(req.Msg.GetEffectId()) + return nil, hostError + }, + lookup: func(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + lookups.Add(1) + // Defensive classification: this reason normally comes from Invoke. + return nil, hostError + }, + }, options...) + var invokeOptions []datasource.InvokeOption + if suppliedID { + invokeOptions = append(invokeOptions, datasource.WithEffectID("persisted-effect")) + } + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{}, invokeOptions...) + outcome := checkDeclarationRemoved(t, err, tt.status, hostError.Error()) + effectID, _ := sentID.Load().(string) + if result == nil || effectID == "" || (suppliedID && effectID != "persisted-effect") || + !reflect.DeepEqual(result.Receipt, datasource.Receipt{EffectID: effectID, Status: tt.status}) || + !reflect.DeepEqual(outcome.Receipt, result.Receipt) || result.Output != nil { + t.Fatalf("removed outcome lost the ID/status or invented evidence: %+v, %+v", result, outcome) + } + if invokes.Load() != 1 || lookups.Load() != 0 { + t.Fatal("removed outcome triggered an automatic retry or lookup") + } + receipt, err := c.Lookup(context.Background(), "org", "source", effectID) + outcome = checkDeclarationRemoved(t, err, tt.status, hostError.Error()) + if receipt == nil || !reflect.DeepEqual(*receipt, result.Receipt) || + !reflect.DeepEqual(*receipt, outcome.Receipt) || invokes.Load() != 1 || lookups.Load() != 1 { + t.Fatalf("lookup lost evidence or reinvoked: %+v, %v", receipt, err) + } + }) + } + } + } +} + +func checkDeclarationRemoved(t *testing.T, err error, status datasource.ReceiptStatus, message string) *datasource.OperationDeclarationRemoved { + t.Helper() + var outcome *datasource.OperationDeclarationRemoved + var provider *datasource.ProviderRefused + var refused *datasource.OperationRefused + var transport *connect.Error + if !errors.Is(err, datasource.ErrOperationDeclarationRemoved) || !errors.As(err, &outcome) || + outcome.ReceiptStatus != status || outcome.Receipt.Status != status || + errors.Is(err, datasource.ErrOutcomeUnknown) || errors.Is(err, datasource.ErrUnknownOperation) || + errors.Is(err, datasource.ErrEffectNotFound) || errors.As(err, &provider) || errors.As(err, &refused) { + t.Fatalf("removed outcome misclassified: %T %+v", err, err) + } + if !errors.As(err, &transport) || connect.CodeOf(err) != connect.CodeFailedPrecondition || + outcome.Unwrap() != transport || err.Error() != message { + t.Fatalf("removed outcome lost host diagnostics: %v", err) + } + if !errors.Is(fmt.Errorf("invoke: %w", err), datasource.ErrOperationDeclarationRemoved) { + t.Fatal("wrapped outcome lost its sentinel") + } + return outcome +} + +func TestRemovedDeclarationRecoveryUsesLookup(t *testing.T) { + for _, state := range []string{"committed", "unresolved", "expired", "revoked access"} { + t.Run(state, func(t *testing.T) { + var invokes, lookups atomic.Int32 + const effectID = "original-effect" + const output = `{"id":18446744073709551615}` + status := "unknown" + if state == "committed" || state == "revoked access" { + status = "committed" + } + c := newOperationsClient(t, &operationsHandler{ + invoke: func(context.Context, *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + invokes.Add(1) + return nil, declarationRemovedError(t, map[string]string{"receipt_status": status}) + }, + lookup: func(_ context.Context, req *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + lookups.Add(1) + if req.Msg.GetEffectId() != effectID { + t.Error("recovery changed the effect ID") + } + if state == "revoked access" { + return nil, connect.NewError(connect.CodePermissionDenied, errors.New("source read access revoked")) + } + if state != "committed" { + return connect.NewResponse(&v1.InvokeSourceOperationResponse{Receipt: &v1.SourceOperationReceipt{EffectId: effectID, Status: "unknown"}}), nil + } + receipt := committedReceipt(effectID) + receipt.OutputJson = output + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: output, Receipt: receipt}), nil + }, + }) + result, err := c.Invoke(context.Background(), "org", "source", "create_invoice", map[string]any{}, datasource.WithEffectID(effectID)) + if !errors.Is(err, datasource.ErrOperationDeclarationRemoved) || result == nil { + t.Fatalf("removed declaration = %+v, %v", result, err) + } + receipt, err := c.Lookup(context.Background(), "org", "source", result.Receipt.EffectID) + switch state { + case "committed": + if err != nil || receipt == nil || receipt.Status != datasource.ReceiptStatusCommitted || string(receipt.Output) != output { + t.Fatalf("saved commit = %+v, %v", receipt, err) + } + case "revoked access": + if !errors.Is(err, datasource.ErrNotPermitted) || receipt != nil { + t.Fatalf("revoked access = %+v, %v", receipt, err) + } + default: + if !errors.Is(err, datasource.ErrOutcomeUnknown) || receipt == nil || receipt.Status != datasource.ReceiptStatusUnknown || receipt.Output != nil { + t.Fatalf("unresolved/expired receipt = %+v, %v", receipt, err) + } + } + if invokes.Load() != 1 || lookups.Load() != 1 { + t.Fatal("receipt recovery retried or reinvoked") + } + }) + } +} diff --git a/datasource/types.go b/datasource/types.go index 621d846..b090ddc 100644 --- a/datasource/types.go +++ b/datasource/types.go @@ -38,9 +38,8 @@ type ReceiptStatus string const ( ReceiptStatusCommitted ReceiptStatus = "COMMITTED" ReceiptStatusUnknown ReceiptStatus = "UNKNOWN" - // ReceiptStatusNotAttempted labels a Lookup miss. - // The host can also return NotFound for a removed declaration, so a lookup - // miss is not historical proof that the provider never acted. Keep the ID. + // ReceiptStatusNotAttempted means Lookup found no attempt marker. + // A caller may explicitly invoke with the same ID; the SDK never retries. ReceiptStatusNotAttempted ReceiptStatus = "NOT_ATTEMPTED" ) diff --git a/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go b/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go index 20eb8ec..91d0a47 100644 --- a/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go +++ b/gen/saas/accounts/v1/accountsv1connect/datasource.connect.go @@ -133,6 +133,9 @@ type DatasourceServiceClient interface { // InvokeSourceOperation calls an admitted operation under current authority. // FailedPrecondition with ErrorInfo.reason SOURCE_OPERATION_OUTCOME_UNKNOWN // means this effect will not be redispatched and its receipt stays unresolved. + // SOURCE_OPERATION_DECLARATION_REMOVED refuses a retained effect whose + // declaration was removed; ErrorInfo.metadata["receipt_status"] is committed + // or unknown. Recover that same effect with Lookup; never choose a fresh ID. // Other refusal reasons do not mean unknown. Provider refusals include the // decimal HTTP status in ErrorInfo.metadata["provider_status"] when known. // InvalidArgument carries BadRequest.FieldViolation.field as a JSON pointer. @@ -140,6 +143,8 @@ type DatasourceServiceClient interface { // LookupInvokeSourceOperation recovers a receipt without contacting the provider. // NotFound "source effect not found" means there is no attempt marker; retrying // the same effect is safe. An unknown receipt never authorizes redispatch. + // Once an attempt exists, retained receipt evidence survives declaration + // removal or replacement. Current source read authority still applies. LookupInvokeSourceOperation(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) // ListSourceOperations reads the currently admitted declarations under source authority. ListSourceOperations(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) @@ -608,6 +613,9 @@ type DatasourceServiceHandler interface { // InvokeSourceOperation calls an admitted operation under current authority. // FailedPrecondition with ErrorInfo.reason SOURCE_OPERATION_OUTCOME_UNKNOWN // means this effect will not be redispatched and its receipt stays unresolved. + // SOURCE_OPERATION_DECLARATION_REMOVED refuses a retained effect whose + // declaration was removed; ErrorInfo.metadata["receipt_status"] is committed + // or unknown. Recover that same effect with Lookup; never choose a fresh ID. // Other refusal reasons do not mean unknown. Provider refusals include the // decimal HTTP status in ErrorInfo.metadata["provider_status"] when known. // InvalidArgument carries BadRequest.FieldViolation.field as a JSON pointer. @@ -615,6 +623,8 @@ type DatasourceServiceHandler interface { // LookupInvokeSourceOperation recovers a receipt without contacting the provider. // NotFound "source effect not found" means there is no attempt marker; retrying // the same effect is safe. An unknown receipt never authorizes redispatch. + // Once an attempt exists, retained receipt evidence survives declaration + // removal or replacement. Current source read authority still applies. LookupInvokeSourceOperation(context.Context, *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) // ListSourceOperations reads the currently admitted declarations under source authority. ListSourceOperations(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) From dc0124b217070224f03b61be7fafbdeae8221460 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:24:07 -0400 Subject: [PATCH 09/12] fix(datasource): align parity error and receipt boundaries --- CHANGELOG.md | 11 ++ README.md | 31 +++- datasource/client.go | 2 +- datasource/connect_response.go | 86 +++++++++ datasource/doc.go | 4 + datasource/errors.go | 24 ++- datasource/errors_test.go | 3 + datasource/json.go | 15 +- datasource/operations.go | 28 ++- datasource/operations_test.go | 4 + datasource/parity_test.go | 315 +++++++++++++++++++++++++++++++++ datasource/types.go | 6 +- 12 files changed, 516 insertions(+), 13 deletions(-) create mode 100644 datasource/connect_response.go create mode 100644 datasource/parity_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ac7b856..58ca830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased — callable sources (awaits host tag) +- Fix cross-language parity at the error/receipt boundary: skip null and unknown + details without losing effect IDs; scope `SOURCE_OPERATION_OUTCOME_UNKNOWN` + to Invoke/Lookup; normalize UTF-8 charset, gzip case and BOM on Connect JSON + errors while retaining the gateway client and Connect decoder. +- Add `DatasourceError{Receipt}` for invalid output on a valid COMMITTED receipt. + Receipt output comes from `receipt.output_json`; result output comes from the + top-level field. Both validate without discarding known commitment. Unknown + receipts ignore output; invalid receipt identity/status/time stays unknown. +- Normalize absent listed schemas to `{}`; declaration still requires both + schemas before dispatch. Use the protobuf Timestamp JSON profile for receipt + times and rate-limit resets, rejecting year zero and excess fractional digits. - Add `datasource.Client.Invoke`, `Lookup`, `DeclareOperations` and `ListOperations` on the existing gateway-bound Connect client, matching the Python facade. Add operations, JSON output, receipts, UUIDv7 effect IDs and diff --git a/README.md b/README.md index 66ccb23..5eebc06 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,15 @@ The SDK surfaces are: removal or replacement, and unresolved or expired receipts remain unknown. The caller's current source read authority still applies; revoked access refuses. + `Receipt.Output` always comes from the saved `receipt.output_json` evidence; + `Result.Output` comes from the top-level `output_json`. Both fields must contain + one JSON object on a committed response. If either is missing or malformed, + `*DatasourceError` retains the COMMITTED receipt, effect ID, commit time and + provider status (and any valid receipt output). Bad output does not erase a + commit or authorize another effect. Missing/mismatched receipt identity, + unsupported status or invalid commit time instead yields `ErrOutcomeUnknown`. + An UNKNOWN receipt does not require valid output. + Re-invoking a retained effect after its declaration is removed returns `*OperationDeclarationRemoved` (`errors.Is(err, ErrOperationDeclarationRemoved)`). Its `ReceiptStatus` and `Receipt.Status` carry the host's `receipt_status`: @@ -125,7 +134,10 @@ The SDK surfaces are: `DeclareOperations(ctx, orgID, sourceID, []datasource.Operation)` replaces the source's declaration set atomically under the host's administrator - check. `ListOperations(ctx, orgID, sourceID)` reads it. For example, an API + check. Both schemas must be supplied as JSON objects; omitted schemas return + `InputError` before dispatch. Explicit `{}` is accepted for host admission. + `ListOperations(ctx, orgID, sourceID)` reads the set and represents absent + schemas as `{}`, so its operations can be re-declared. For example, an API connected to `https://example.com` could declare: ```go @@ -160,6 +172,21 @@ The SDK surfaces are: | Other FailedPrecondition | `*OperationRefused`, with the structured `Reason` or an empty string | | Non-Connect HTTP error after Invoke/Lookup dispatch (4xx or 5xx) | receipt plus `ErrOutcomeUnknown`; retain the original ID | | Ambiguous Invoke / unknown receipt | receipt plus `ErrOutcomeUnknown` (`errors.Is`) | + | Valid COMMITTED receipt with invalid output JSON | receipt plus `*DatasourceError`; commit evidence is retained | + + `SOURCE_OPERATION_OUTCOME_UNKNOWN` is an effect outcome only on Invoke and + Lookup. On Declare/List it is `OperationRefused` carrying that reason. + Null, unknown or undecodable protobuf error details are skipped; valid + neighboring details still determine the outcome. For Declare/List, + non-Connect HTTP 403 maps to `ErrNotPermitted` through Connect's HTTP fallback; + on Invoke/Lookup the same non-Connect response remains unknown. + + Connect JSON errors accept a case-insensitive UTF-8 charset parameter, + case-insensitive gzip encoding and a leading UTF-8 BOM. A response adapter + normalizes those encodings before Connect decodes the envelope; it forwards + the request unchanged through the gateway's existing HTTP client. Other + charsets and media types remain non-Connect. Timestamp strings follow the + protobuf JSON profile (years 0001–9999, at most nine fractional digits). The recorded host ref emits structured input pointers and provider HTTP statuses. `InputError.Pointer` is empty when no field violation is supplied; @@ -177,7 +204,7 @@ The SDK surfaces are: metadata. These names mirror the Python facade's `invoke`, `lookup`, `declare_operations`, `list_operations`, `Operation`, `Result`, `Receipt`, `NotPermitted`, `UnknownOperation`, `EffectNotFound`, `EffectReused`, - `InputError`, `RateLimited`, `ProviderRefused`, `OperationRefused`, `OperationDeclarationRemoved` and + `DatasourceError`, `InputError`, `RateLimited`, `ProviderRefused`, `OperationRefused`, `OperationDeclarationRemoved` and `OutcomeUnknown`. This call surface uses the four host RPCs from diff --git a/datasource/client.go b/datasource/client.go index 88cfba0..178e6af 100644 --- a/datasource/client.go +++ b/datasource/client.go @@ -31,7 +31,7 @@ type Client struct { // connect.WithGRPC()) are forwarded to the underlying client. func New(gw Gateway, opts ...connect.ClientOption) *Client { return &Client{ - inner: accountsv1connect.NewDatasourceServiceClient(gw.HTTPClient(), gw.BaseURL(), opts...), + inner: accountsv1connect.NewDatasourceServiceClient(connectResponseClient{client: gw.HTTPClient()}, gw.BaseURL(), opts...), } } diff --git a/datasource/connect_response.go b/datasource/connect_response.go new file mode 100644 index 0000000..93d35a8 --- /dev/null +++ b/datasource/connect_response.go @@ -0,0 +1,86 @@ +package datasource + +import ( + "bufio" + "bytes" + "compress/gzip" + "errors" + "io" + "mime" + "net/http" + "strings" +) + +// connectResponseClient normalizes the shared SDK's accepted HTTP encodings +// before Connect decodes an error. It delegates the original request unchanged +// to the gateway client; Connect still owns envelope, detail and status decoding. +type connectResponseClient struct{ client *http.Client } + +func (c connectResponseClient) Do(request *http.Request) (*http.Response, error) { + response, err := c.client.Do(request) + if err != nil || response.StatusCode == http.StatusOK || + strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc") { + return response, err + } + mediaType, params, err := mime.ParseMediaType(response.Header.Get("Content-Type")) + if err != nil || mediaType != "application/json" || len(params) > 1 || + (len(params) == 1 && !strings.EqualFold(params["charset"], "utf-8")) { + return response, nil + } + encoding := strings.ToLower(response.Header.Get("Content-Encoding")) + if encoding != "" && encoding != "identity" && encoding != "gzip" { + return response, nil + } + // Clone the response metadata, never the gateway's client/transport/request. + normalized := *response + normalized.Header = response.Header.Clone() + normalized.Header.Set("Content-Type", "application/json") + normalized.Header.Del("Content-Encoding") + normalized.Header.Del("Content-Length") + normalized.ContentLength = -1 + normalized.Uncompressed = response.Uncompressed || encoding == "gzip" + normalized.Body = &connectJSONErrorBody{source: response.Body, compressed: encoding == "gzip"} + return &normalized, nil +} + +// Decode lazily so Connect retains responsibility for read failures and closing +// the original response. Peeking only the prefix avoids buffering another copy. +type connectJSONErrorBody struct { + source io.ReadCloser + compressed bool + initialized bool + reader *bufio.Reader + gzip *gzip.Reader + err error +} + +func (b *connectJSONErrorBody) Read(p []byte) (int, error) { + if !b.initialized { + b.initialized = true + var source io.Reader = b.source + if b.compressed { + b.gzip, b.err = gzip.NewReader(b.source) + if b.err != nil { + return 0, b.err + } + source = b.gzip + } + b.reader = bufio.NewReader(source) + prefix, _ := b.reader.Peek(3) + if bytes.Equal(prefix, []byte{0xef, 0xbb, 0xbf}) { + _, _ = b.reader.Discard(3) + } + } + if b.err != nil { + return 0, b.err + } + return b.reader.Read(p) +} + +func (b *connectJSONErrorBody) Close() error { + var err error + if b.gzip != nil { + err = b.gzip.Close() + } + return errors.Join(err, b.source.Close()) +} diff --git a/datasource/doc.go b/datasource/doc.go index 82483dd..7c0b26d 100644 --- a/datasource/doc.go +++ b/datasource/doc.go @@ -35,4 +35,8 @@ // never with a fresh ID: a committed status here does not include the saved // output. Lookup survives declaration removal/replacement under current source // read authority; unresolved or expired receipts remain unknown. +// +// DatasourceError retains a valid COMMITTED receipt when either output JSON +// field is malformed. Receipt.Output uses receipt.output_json; Result.Output +// uses top-level output_json. Bad output never authorizes repeating a commit. package datasource diff --git a/datasource/errors.go b/datasource/errors.go index 8d80257..80179d2 100644 --- a/datasource/errors.go +++ b/datasource/errors.go @@ -25,6 +25,22 @@ var ( ErrOutcomeUnknown = errors.New("datasource: outcome unknown; look up the effect") ) +// DatasourceError reports invalid output accompanying a valid committed receipt. +// Receipt preserves the commit evidence and any valid receipt output. This is +// not an unknown effect or permission to retry it, especially under a fresh ID. +type DatasourceError struct { + Receipt Receipt + cause error +} + +func (e *DatasourceError) Error() string { + if e.cause == nil { + return "datasource: invalid committed output" + } + return e.cause.Error() +} +func (e *DatasourceError) Unwrap() error { return e.cause } + // InputError reports a rejected input. Pointer is the host's JSON Pointer from // BadRequest.FieldViolation.Field, or empty when no structured pointer is supplied. type InputError struct { @@ -160,6 +176,9 @@ func mapHostError(err error, effectOutcome bool, now time.Time) error { var retry *errdetails.RetryInfo var bad *errdetails.BadRequest for _, detail := range ce.Details() { + if detail == nil { + continue + } value, detailErr := detail.Value() if detailErr != nil { continue @@ -188,7 +207,7 @@ func mapHostError(err error, effectOutcome bool, now time.Time) error { if info.GetReason() != "DATASOURCE_RATE_LIMITED" { return err } - reset, parseErr := time.Parse(time.RFC3339Nano, info.GetMetadata()["reset_at"]) + reset, parseErr := parseHostTime(info.GetMetadata()["reset_at"]) if parseErr != nil { reset = time.Time{} if delay := retry.GetRetryDelay(); delay != nil && delay.CheckValid() == nil && delay.AsDuration() >= 0 { @@ -199,6 +218,9 @@ func mapHostError(err error, effectOutcome bool, now time.Time) error { case connect.CodeFailedPrecondition: switch info.GetReason() { case "SOURCE_OPERATION_OUTCOME_UNKNOWN": + if !effectOutcome { + return &OperationRefused{Reason: info.GetReason(), cause: err} + } return &OutcomeUnknown{cause: err} case "SOURCE_OPERATION_DECLARATION_REMOVED": status := ReceiptStatusUnknown diff --git a/datasource/errors_test.go b/datasource/errors_test.go index d9f8060..8bc5474 100644 --- a/datasource/errors_test.go +++ b/datasource/errors_test.go @@ -165,6 +165,9 @@ func TestRateLimitReset(t *testing.T) { {"retry delay", nil, &errdetails.RetryInfo{RetryDelay: durationpb.New(3 * time.Second)}, now.Add(3 * time.Second)}, {"absolute wins", map[string]string{"reset_at": now.Add(time.Minute).Format(time.RFC3339)}, &errdetails.RetryInfo{RetryDelay: durationpb.New(time.Second)}, now.Add(time.Minute)}, {"invalid absolute falls back", map[string]string{"reset_at": "invalid"}, &errdetails.RetryInfo{RetryDelay: durationpb.New(time.Second)}, now.Add(time.Second)}, + {"year zero", map[string]string{"reset_at": "0000-01-01T00:00:00Z"}, nil, time.Time{}}, + {"excess precision", map[string]string{"reset_at": "2026-10-09T12:00:00.1234567890Z"}, nil, time.Time{}}, + {"invalid timestamp falls back", map[string]string{"reset_at": "0000-01-01T00:00:00Z"}, &errdetails.RetryInfo{RetryDelay: durationpb.New(time.Second)}, now.Add(time.Second)}, {"missing", nil, &errdetails.RetryInfo{}, time.Time{}}, {"negative", nil, &errdetails.RetryInfo{RetryDelay: durationpb.New(-time.Second)}, time.Time{}}, {"invalid duration", nil, &errdetails.RetryInfo{RetryDelay: &durationpb.Duration{Seconds: 1, Nanos: -1}}, time.Time{}}, diff --git a/datasource/json.go b/datasource/json.go index a6ba0ee..505885b 100644 --- a/datasource/json.go +++ b/datasource/json.go @@ -4,10 +4,13 @@ import ( "bytes" "encoding/json" "errors" + "strconv" + "time" "unicode/utf8" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" ) // Payloads remain JSON text, preserving integer precision and number spelling. @@ -48,8 +51,18 @@ func schemaStruct(data []byte) (*structpb.Struct, error) { func schemaJSON(value *structpb.Struct) (json.RawMessage, error) { if value == nil { - return nil, nil + return json.RawMessage(`{}`), nil } data, err := protojson.Marshal(value) return json.RawMessage(data), err } + +// Use protobuf's shared Timestamp JSON profile, including year/fraction bounds, +// rather than time.Parse's acceptance of year zero and truncated subseconds. +func parseHostTime(value string) (time.Time, error) { + var timestamp timestamppb.Timestamp + if err := protojson.Unmarshal([]byte(strconv.Quote(value)), ×tamp); err != nil { + return time.Time{}, err + } + return timestamp.AsTime(), nil +} diff --git a/datasource/operations.go b/datasource/operations.go index 93e1006..9a18b1f 100644 --- a/datasource/operations.go +++ b/datasource/operations.go @@ -2,6 +2,7 @@ package datasource import ( "context" + "encoding/json" "errors" "fmt" "time" @@ -74,8 +75,7 @@ func (c *Client) Invoke(ctx context.Context, orgID, sourceID, operation string, if receipt.Status == ReceiptStatusUnknown { return result, &OutcomeUnknown{Receipt: *receipt} } - result.Output, err = objectJSON(response.Msg.GetOutputJson()) - result.Receipt.Output = result.Output + result.Output, err = committedOutput(response.Msg, &result.Receipt) return result, err } @@ -106,7 +106,10 @@ func (c *Client) Lookup(ctx context.Context, orgID, sourceID, effectID string) ( if connect.CodeOf(err) == connect.CodeNotFound { return &Receipt{EffectID: effectID, Status: ReceiptStatusNotAttempted}, &hostError{kind: ErrEffectNotFound, cause: err} } - mapped := mapHostError(err, connect.CodeOf(err) == connect.CodeUnavailable, time.Now()) + if code := connect.CodeOf(err); code == connect.CodeDeadlineExceeded || code == connect.CodeCanceled { + return nil, err + } + mapped := mapHostError(err, true, time.Now()) var unknown *OutcomeUnknown if errors.As(mapped, &unknown) { unknown.Receipt = Receipt{EffectID: effectID, Status: ReceiptStatusUnknown} @@ -127,10 +130,25 @@ func (c *Client) Lookup(ctx context.Context, orgID, sourceID, effectID string) ( if receipt.Status == ReceiptStatusUnknown { return receipt, &OutcomeUnknown{Receipt: *receipt} } - receipt.Output, err = objectJSON(response.Msg.GetOutputJson()) + _, err = committedOutput(response.Msg, receipt) return receipt, err } +// Receipt output is the saved evidence. Top-level output supplies Result.Output +// and must also be a JSON object, but never overwrites the receipt's output. +func committedOutput(value *v1.InvokeSourceOperationResponse, receipt *Receipt) (json.RawMessage, error) { + var err error + receipt.Output, err = objectJSON(value.GetReceipt().GetOutputJson()) + if err != nil { + return nil, &DatasourceError{Receipt: *receipt, cause: err} + } + output, err := objectJSON(value.GetOutputJson()) + if err != nil { + return nil, &DatasourceError{Receipt: *receipt, cause: err} + } + return output, nil +} + // DeclareOperations asks the host to replace the source's declarations // atomically. The host validates the set and enforces administrator permission. func (c *Client) DeclareOperations(ctx context.Context, orgID, sourceID string, operations []Operation) error { @@ -215,7 +233,7 @@ func operationReceipt(value *v1.SourceOperationReceipt, effectID string) (*Recei } if committed := value.GetCommittedAt(); committed != "" { var err error - receipt.CommittedAt, err = time.Parse(time.RFC3339Nano, committed) + receipt.CommittedAt, err = parseHostTime(committed) if err != nil { return nil, fmt.Errorf("datasource: invalid receipt commit time: %w", err) } diff --git a/datasource/operations_test.go b/datasource/operations_test.go index e19335d..64b02aa 100644 --- a/datasource/operations_test.go +++ b/datasource/operations_test.go @@ -401,6 +401,10 @@ func TestUnknownAndMalformedReceiptsRetainOriginalEffectID(t *testing.T) { {"mismatch", committedReceipt("another-effect")}, {"unspecified", &v1.SourceOperationReceipt{EffectId: "effect"}}, {"invalid timestamp", &v1.SourceOperationReceipt{EffectId: "effect", Status: "committed", CommittedAt: "invalid-time"}}, + {"year zero", &v1.SourceOperationReceipt{EffectId: "effect", Status: "committed", CommittedAt: "0000-01-01T00:00:00Z"}}, + {"excess precision", &v1.SourceOperationReceipt{EffectId: "effect", Status: "committed", CommittedAt: "2026-10-09T12:00:00.1234567890Z"}}, + {"pending", &v1.SourceOperationReceipt{EffectId: "effect", Status: "pending"}}, + {"uppercase status", &v1.SourceOperationReceipt{EffectId: "effect", Status: "COMMITTED"}}, } { t.Run(tt.name, func(t *testing.T) { var calls atomic.Int32 diff --git a/datasource/parity_test.go b/datasource/parity_test.go new file mode 100644 index 0000000..3e101e6 --- /dev/null +++ b/datasource/parity_test.go @@ -0,0 +1,315 @@ +package datasource_test + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "sync/atomic" + "testing" + + "connectrpc.com/connect" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/protobuf/proto" + + "github.com/codefly-dev/saas-sdk-go/datasource" + v1 "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1" +) + +func parityCall(c *datasource.Client, method string) (*datasource.Receipt, error) { + ctx := context.Background() + switch method { + case "invoke": + result, err := c.Invoke(ctx, "org", "source", "read_item", map[string]any{}) + if result == nil { + return nil, err + } + return &result.Receipt, err + case "lookup": + return c.Lookup(ctx, "org", "source", "persisted-effect") + case "declare": + return nil, c.DeclareOperations(ctx, "org", "source", []datasource.Operation{{ + Name: "read_item", Method: "GET", Path: "/items", Effect: datasource.EffectReadOnly, + Input: json.RawMessage(`{}`), Output: json.RawMessage(`{}`), + }}) + default: + _, err := c.ListOperations(ctx, "org", "source") + return nil, err + } +} + +func wireDetail(t *testing.T, message proto.Message) string { + t.Helper() + data, err := proto.Marshal(message) + if err != nil { + t.Fatal(err) + } + encoded, err := json.Marshal(map[string]string{ + "type": string(message.ProtoReflect().Descriptor().FullName()), "value": base64.RawStdEncoding.EncodeToString(data), + }) + if err != nil { + t.Fatal(err) + } + return string(encoded) +} + +func TestNilAndUnknownWireDetailsPreserveOutcome(t *testing.T) { + unknownReason := wireDetail(t, &errdetails.ErrorInfo{Reason: "SOURCE_OPERATION_OUTCOME_UNKNOWN", Domain: "saas.accounts.v1"}) + for _, details := range []string{ + `[null]`, + `[null,{"type":"unregistered.Detail","value":"AA"},{"type":"google.rpc.ErrorInfo","value":"AA"}]`, + `[null,` + unknownReason + `,null,{"type":"unregistered.Detail","value":"AA"}]`, + } { + for _, method := range []string{"invoke", "lookup", "declare", "list"} { + t.Run(method+details, func(t *testing.T) { + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, `{"code":"failed_precondition","message":"diagnostic","details":%s}`, details) + })) + defer server.Close() + c := datasource.New(gw{base: server.URL, client: server.Client()}) + receipt, err := parityCall(c, method) + withReason := details == `[null,`+unknownReason+`,null,{"type":"unregistered.Detail","value":"AA"}]` + if withReason && (method == "invoke" || method == "lookup") { + var outcome *datasource.OutcomeUnknown + if !errors.Is(err, datasource.ErrOutcomeUnknown) || !errors.As(err, &outcome) || + receipt == nil || receipt.EffectID == "" || outcome.Receipt.EffectID != receipt.EffectID { + t.Fatalf("nil detail lost effect evidence: %+v, %v", receipt, err) + } + } else { + var refused *datasource.OperationRefused + wantReason := "" + if withReason { + wantReason = "SOURCE_OPERATION_OUTCOME_UNKNOWN" + } + if !errors.As(err, &refused) || refused.Reason != wantReason || errors.Is(err, datasource.ErrOutcomeUnknown) { + t.Fatalf("detail/scope mapping = %T %v", err, err) + } + } + if method == "invoke" && (receipt == nil || receipt.EffectID == "") { + t.Fatal("invoke lost its minted ID") + } + if !connect.IsWireError(err) || connect.CodeOf(err) != connect.CodeFailedPrecondition || calls.Load() != 1 { + t.Fatalf("wire identity lost or retried: %v, calls %d", err, calls.Load()) + } + }) + } + } +} + +func TestConnectErrorEncodingParity(t *testing.T) { + for _, contentType := range []string{"application/json", "application/json; charset=utf-8", "application/json; charset=UTF-8", `application/json; charset="UtF-8"`} { + for _, encoding := range []string{"", "gzip", "GZIP", "gZiP"} { + for _, bom := range []bool{false, true} { + for _, method := range []string{"invoke", "lookup", "declare", "list"} { + t.Run(fmt.Sprintf("%s/%s/%s/bom=%t", method, contentType, encoding, bom), func(t *testing.T) { + body := []byte(`{"code":"permission_denied","message":"host diagnostic"}`) + if bom { + body = append([]byte{0xef, 0xbb, 0xbf}, body...) + } + if encoding != "" { + var compressed bytes.Buffer + writer := gzip.NewWriter(&compressed) + _, _ = writer.Write(body) + _ = writer.Close() + body = compressed.Bytes() + } + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.Header().Set("Content-Type", contentType) + if encoding != "" { + w.Header().Set("Content-Encoding", encoding) + } + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write(body) + })) + defer server.Close() + receipt, err := parityCall(datasource.New(gw{base: server.URL, client: server.Client()}), method) + if !errors.Is(err, datasource.ErrNotPermitted) || !connect.IsWireError(err) || + err.Error() != "permission_denied: host diagnostic" || calls.Load() != 1 { + t.Fatalf("encoded error = %v, calls %d", err, calls.Load()) + } + if method == "invoke" && (receipt == nil || receipt.EffectID == "") { + t.Fatal("encoded error lost the effect ID") + } + }) + } + } + } + } +} + +func TestNonWireForbiddenHasMethodSpecificMapping(t *testing.T) { + for _, tt := range []struct{ contentType, encoding, body string }{ + {"text/plain", "", "forbidden"}, {"application/json", "", ""}, + {"application/json; charset=iso-8859-1", "", `{"code":"permission_denied"}`}, + {"application/problem+json", "", `{"code":"permission_denied"}`}, + {"application/json", "", `[]`}, {"application/json", "", `{"code":123}`}, + {"application/json", "", `{"message":123}`}, {"application/json", "", `{} trailing`}, + {"application/json", "gzip", "corrupt gzip"}, {"application/json", "GZIP", "corrupt gzip"}, + } { + for _, method := range []string{"invoke", "lookup", "declare", "list"} { + t.Run(method+tt.contentType+tt.encoding+tt.body, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", tt.contentType) + w.Header().Set("Content-Encoding", tt.encoding) + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte(tt.body)) + })) + defer server.Close() + receipt, err := parityCall(datasource.New(gw{base: server.URL, client: server.Client()}), method) + want := datasource.ErrNotPermitted + if method == "invoke" || method == "lookup" { + want = datasource.ErrOutcomeUnknown + if receipt == nil || receipt.EffectID == "" || receipt.Status != datasource.ReceiptStatusUnknown { + t.Fatalf("non-wire error lost the effect: %+v", receipt) + } + } + if !errors.Is(err, want) || connect.IsWireError(err) { + t.Fatalf("non-wire %s error = %v, want %v", method, err, want) + } + }) + } + } +} + +func TestCommittedOutputFieldsRetainTheirOwnEvidence(t *testing.T) { + for _, tt := range []struct { + name, top, nested string + invalid bool + }{ + {"different", `{"top":1}`, `{"receipt":9007199254740993}`, false}, + {"missing top", "", `{"receipt":1}`, true}, + {"bad top", `[]`, `{"receipt":1}`, true}, + {"trailing top", `{}{}`, `{"receipt":1}`, true}, + {"nan top", `{"n":NaN}`, `{"receipt":1}`, true}, + {"missing receipt output", `{}`, "", true}, + {"bad receipt output", `{}`, `[]`, true}, + {"both bad", "nope", "nope", true}, + } { + t.Run(tt.name, func(t *testing.T) { + var calls atomic.Int32 + response := func(id string) *connect.Response[v1.InvokeSourceOperationResponse] { + calls.Add(1) + receipt := committedReceipt(id) + receipt.OutputJson = tt.nested + return connect.NewResponse(&v1.InvokeSourceOperationResponse{OutputJson: tt.top, Receipt: receipt}) + } + c := newOperationsClient(t, &operationsHandler{ + invoke: func(_ context.Context, req *connect.Request[v1.InvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return response(req.Msg.EffectId), nil + }, + lookup: func(_ context.Context, req *connect.Request[v1.LookupInvokeSourceOperationRequest]) (*connect.Response[v1.InvokeSourceOperationResponse], error) { + return response(req.Msg.EffectId), nil + }, + }) + result, invokeErr := c.Invoke(context.Background(), "org", "source", "read_item", map[string]any{}) + if result == nil || result.Receipt.EffectID == "" { + t.Fatal("committed result lost its effect ID") + } + receipt, lookupErr := c.Lookup(context.Background(), "org", "source", result.Receipt.EffectID) + for _, value := range []struct { + receipt *datasource.Receipt + err error + }{{&result.Receipt, invokeErr}, {receipt, lookupErr}} { + if value.receipt == nil || value.receipt.Status != datasource.ReceiptStatusCommitted || value.receipt.CommittedAt.IsZero() || value.receipt.ProviderStatus != 200 { + t.Fatalf("output erased commit evidence: %+v, %v", value.receipt, value.err) + } + var outputErr *datasource.DatasourceError + if tt.invalid { + if !errors.As(value.err, &outputErr) || errors.Is(value.err, datasource.ErrOutcomeUnknown) || + !reflect.DeepEqual(outputErr.Receipt, *value.receipt) || outputErr.Unwrap() == nil { + t.Fatalf("invalid committed output = %T %v", value.err, value.err) + } + } else if value.err != nil || string(value.receipt.Output) != tt.nested { + t.Fatalf("canonical receipt output = %s, %v", value.receipt.Output, value.err) + } + if json.Valid([]byte(tt.nested)) && tt.nested != "[]" && string(value.receipt.Output) != tt.nested { + t.Fatal("valid canonical output was discarded") + } + } + if !tt.invalid && string(result.Output) != tt.top { + t.Fatal("result output must retain the top-level field") + } + if calls.Load() != 2 { + t.Fatal("output failure retried the effect") + } + }) + } +} + +func TestLostOrUndecodableReplyRetainsEffectID(t *testing.T) { + for _, lost := range []bool{false, true} { + for _, method := range []string{"invoke", "lookup"} { + t.Run(fmt.Sprintf("%s/lost=%t", method, lost), func(t *testing.T) { + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + if lost { + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + t.Error(err) + return + } + _ = conn.Close() + return + } + w.Header().Set("Content-Type", "application/proto") + _, _ = w.Write([]byte("not protobuf")) + })) + defer server.Close() + receipt, err := parityCall(datasource.New(gw{base: server.URL, client: server.Client()}), method) + var unknown *datasource.OutcomeUnknown + if !errors.As(err, &unknown) || receipt == nil || receipt.EffectID == "" || receipt.Status != datasource.ReceiptStatusUnknown || + unknown.Receipt.EffectID != receipt.EffectID || unknown.Unwrap() == nil || calls.Load() != 1 { + t.Fatalf("lost/undecodable reply lost evidence or retried: %+v, %v, calls %d", receipt, err, calls.Load()) + } + }) + } + } +} + +func TestMissingSchemasRefuseDeclareAndNormalizeList(t *testing.T) { + var declares atomic.Int32 + c := newOperationsClient(t, &operationsHandler{ + declare: func(_ context.Context, req *connect.Request[v1.DeclareSourceOperationsRequest]) (*connect.Response[v1.DeclareSourceOperationsResponse], error) { + declares.Add(1) + for _, operation := range req.Msg.Operations { + if operation.InputSchema == nil || operation.OutputSchema == nil { + t.Error("missing schema was dispatched") + } + } + return connect.NewResponse(&v1.DeclareSourceOperationsResponse{}), nil + }, + list: func(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) { + return connect.NewResponse(&v1.ListSourceOperationsResponse{Operations: []*v1.SourceOperation{{Name: "read_item", Effect: v1.SourceOperation_EFFECT_READ_ONLY}}}), nil + }, + }) + for _, operation := range []datasource.Operation{ + {Effect: datasource.EffectReadOnly}, + {Input: json.RawMessage(`{}`), Effect: datasource.EffectReadOnly}, + {Output: json.RawMessage(`{}`), Effect: datasource.EffectReadOnly}, + } { + var inputErr *datasource.InputError + if err := c.DeclareOperations(context.Background(), "org", "source", []datasource.Operation{operation}); !errors.As(err, &inputErr) || declares.Load() != 0 { + t.Fatalf("missing schemas dispatched or untyped: %v, calls %d", err, declares.Load()) + } + } + operations, err := c.ListOperations(context.Background(), "org", "source") + if err != nil || len(operations) != 1 || string(operations[0].Input) != "{}" || string(operations[0].Output) != "{}" { + t.Fatalf("unset listed schemas = %+v, %v", operations, err) + } + if err := c.DeclareOperations(context.Background(), "org", "source", operations); err != nil || declares.Load() != 1 { + t.Fatalf("explicit empty schemas did not redeclare: %v", err) + } +} diff --git a/datasource/types.go b/datasource/types.go index b090ddc..541fa1a 100644 --- a/datasource/types.go +++ b/datasource/types.go @@ -51,12 +51,12 @@ type Receipt struct { Status ReceiptStatus CommittedAt time.Time ProviderStatus int - // Output is the committed output, also available after Lookup. It is nil - // when there is no committed output. + // Output is the committed receipt.output_json evidence, also available after + // Lookup. It is nil when unavailable or malformed (see DatasourceError). Output json.RawMessage } -// Result carries the provider output and the host receipt. Invoke also returns +// Result carries top-level output_json and the host receipt. Invoke also returns // a non-nil Result on an RPC error so a minted effect ID is never lost. type Result struct { Output json.RawMessage From 6b9bf8a662ce6575c61e4d36fa1deb47a457211a Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:00:31 -0400 Subject: [PATCH 10/12] fix(datasource): type unsupported encoding and effect errors --- CHANGELOG.md | 5 ++ README.md | 18 +++-- datasource/connect_response.go | 25 +++++-- datasource/connect_response_test.go | 44 ++++++++++++ datasource/doc.go | 4 ++ datasource/errors.go | 14 ++-- datasource/operations.go | 6 +- datasource/parity_test.go | 102 ++++++++++++++++++++++++++++ 8 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 datasource/connect_response_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 58ca830..c3d8192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased — callable sources (awaits host tag) +- Reject unsupported HTTP response encodings before reading the body or + inferring an HTTP status: Declare/List return `DatasourceError` with no + receipt; Invoke/Lookup keep `OutcomeUnknown` and the original effect ID. + List returns `InputError` for an absent/unspecified host effect and + `DatasourceError` for an unknown future effect enum, with no partial list. - Fix cross-language parity at the error/receipt boundary: skip null and unknown details without losing effect IDs; scope `SOURCE_OPERATION_OUTCOME_UNKNOWN` to Invoke/Lookup; normalize UTF-8 charset, gzip case and BOM on Connect JSON diff --git a/README.md b/README.md index 5eebc06..ce6e50c 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,11 @@ The SDK surfaces are: ``` `ListOperations` includes each operation's description and host-computed - `Digest`; declarations ignore a caller-supplied digest. The host admits the + `Digest`; declarations ignore a caller-supplied digest. An absent or + `EFFECT_UNSPECIFIED` host effect returns `*InputError` with an empty pointer + and the message `host returned an unspecified effect`; an unknown future + numeric effect returns `*DatasourceError` with no receipt. Neither returns + a partial list. The host admits the schemas, route, effect and output cap. Invocation never accepts a provider URL or credential. The gateway's HTTP client carries the person's Work Context unchanged; this facade adds only @@ -178,14 +182,20 @@ The SDK surfaces are: Lookup. On Declare/List it is `OperationRefused` carrying that reason. Null, unknown or undecodable protobuf error details are skipped; valid neighboring details still determine the outcome. For Declare/List, - non-Connect HTTP 403 maps to `ErrNotPermitted` through Connect's HTTP fallback; + non-Connect HTTP 403 with a supported encoding maps to `ErrNotPermitted` + through Connect's HTTP fallback; on Invoke/Lookup the same non-Connect response remains unknown. Connect JSON errors accept a case-insensitive UTF-8 charset parameter, case-insensitive gzip encoding and a leading UTF-8 BOM. A response adapter normalizes those encodings before Connect decodes the envelope; it forwards - the request unchanged through the gateway's existing HTTP client. Other - charsets and media types remain non-Connect. Timestamp strings follow the + the request unchanged through the gateway's existing HTTP client. Empty, + identity and gzip encodings are accepted case-insensitively. Unsupported + encodings such as `br` are rejected before reading the body or inferring an + HTTP status: Declare/List return `*DatasourceError` with no receipt, while + Invoke/Lookup return `ErrOutcomeUnknown` with the original effect ID. The + diagnostic cause is retained and no retry occurs. Other charsets and media + types remain non-Connect. Timestamp strings follow the protobuf JSON profile (years 0001–9999, at most nine fractional digits). The recorded host ref emits structured input pointers and provider HTTP diff --git a/datasource/connect_response.go b/datasource/connect_response.go index 93d35a8..7cc8a26 100644 --- a/datasource/connect_response.go +++ b/datasource/connect_response.go @@ -5,12 +5,21 @@ import ( "bytes" "compress/gzip" "errors" + "fmt" "io" "mime" "net/http" "strings" + + "connectrpc.com/connect" ) +type unsupportedResponseEncoding struct{ encoding string } + +func (e *unsupportedResponseEncoding) Error() string { + return fmt.Sprintf("datasource: unsupported response encoding %q", e.encoding) +} + // connectResponseClient normalizes the shared SDK's accepted HTTP encodings // before Connect decodes an error. It delegates the original request unchanged // to the gateway client; Connect still owns envelope, detail and status decoding. @@ -18,19 +27,25 @@ type connectResponseClient struct{ client *http.Client } func (c connectResponseClient) Do(request *http.Request) (*http.Response, error) { response, err := c.client.Do(request) - if err != nil || response.StatusCode == http.StatusOK || + if err != nil || strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc") { return response, err } + encoding := strings.ToLower(response.Header.Get("Content-Encoding")) + if encoding != "" && encoding != "identity" && encoding != "gzip" { + // No body or HTTP status can establish an RPC outcome when its encoding + // is unsupported. Close here because Connect receives an error, not a body. + _ = response.Body.Close() + return nil, connect.NewError(connect.CodeInternal, &unsupportedResponseEncoding{encoding: response.Header.Get("Content-Encoding")}) + } + if response.StatusCode == http.StatusOK { + return response, nil + } mediaType, params, err := mime.ParseMediaType(response.Header.Get("Content-Type")) if err != nil || mediaType != "application/json" || len(params) > 1 || (len(params) == 1 && !strings.EqualFold(params["charset"], "utf-8")) { return response, nil } - encoding := strings.ToLower(response.Header.Get("Content-Encoding")) - if encoding != "" && encoding != "identity" && encoding != "gzip" { - return response, nil - } // Clone the response metadata, never the gateway's client/transport/request. normalized := *response normalized.Header = response.Header.Clone() diff --git a/datasource/connect_response_test.go b/datasource/connect_response_test.go new file mode 100644 index 0000000..f6d385d --- /dev/null +++ b/datasource/connect_response_test.go @@ -0,0 +1,44 @@ +package datasource + +import ( + "errors" + "io" + "net/http" + "strings" + "testing" + + "connectrpc.com/connect" +) + +type responseTransport func(*http.Request) (*http.Response, error) + +func (f responseTransport) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } + +type observedBody struct { + io.Reader + reads, closes int +} + +func (b *observedBody) Read(p []byte) (int, error) { b.reads++; return b.Reader.Read(p) } +func (b *observedBody) Close() error { b.closes++; return nil } + +func TestUnsupportedEncodingClosesResponseWithoutReading(t *testing.T) { + body := &observedBody{Reader: strings.NewReader(`{"code":"permission_denied"}`)} + httpClient := &http.Client{Transport: responseTransport(func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusForbidden, Request: req, Body: body, + Header: http.Header{"Content-Type": {"application/json"}, "Content-Encoding": {"br"}}, + }, nil + })} + req, err := http.NewRequest(http.MethodPost, "https://example.com/invoke", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", "application/proto") + response, err := (connectResponseClient{client: httpClient}).Do(req) + var unsupported *unsupportedResponseEncoding + if response != nil || !errors.As(err, &unsupported) || unsupported.encoding != "br" || + connect.CodeOf(err) != connect.CodeInternal || body.reads != 0 || body.closes != 1 { + t.Fatalf("unsupported response = %v, %v, reads=%d closes=%d", response, err, body.reads, body.closes) + } +} diff --git a/datasource/doc.go b/datasource/doc.go index 7c0b26d..76c7ab6 100644 --- a/datasource/doc.go +++ b/datasource/doc.go @@ -39,4 +39,8 @@ // DatasourceError retains a valid COMMITTED receipt when either output JSON // field is malformed. Receipt.Output uses receipt.output_json; Result.Output // uses top-level output_json. Bad output never authorizes repeating a commit. +// Unsupported HTTP response encodings return DatasourceError on Declare/List +// before inferring a status; Invoke/Lookup retain OutcomeUnknown and the ID. +// ListOperations rejects an unspecified effect with InputError and an unknown +// future effect enum with DatasourceError, without returning a partial list. package datasource diff --git a/datasource/errors.go b/datasource/errors.go index 80179d2..2eb03cf 100644 --- a/datasource/errors.go +++ b/datasource/errors.go @@ -25,9 +25,10 @@ var ( ErrOutcomeUnknown = errors.New("datasource: outcome unknown; look up the effect") ) -// DatasourceError reports invalid output accompanying a valid committed receipt. -// Receipt preserves the commit evidence and any valid receipt output. This is -// not an unknown effect or permission to retry it, especially under a fresh ID. +// DatasourceError reports an invalid or unsupported host response. Receipt is +// zero when no effect evidence exists. For invalid output on a valid COMMITTED +// receipt it preserves the commit evidence and any valid receipt output; the +// error never authorizes repeating that effect, especially under a fresh ID. type DatasourceError struct { Receipt Receipt cause error @@ -35,7 +36,7 @@ type DatasourceError struct { func (e *DatasourceError) Error() string { if e.cause == nil { - return "datasource: invalid committed output" + return "datasource: invalid host response" } return e.cause.Error() } @@ -43,6 +44,7 @@ func (e *DatasourceError) Unwrap() error { return e.cause } // InputError reports a rejected input. Pointer is the host's JSON Pointer from // BadRequest.FieldViolation.Field, or empty when no structured pointer is supplied. +// ListOperations also uses it when the host returns an unspecified effect. type InputError struct { Pointer string cause error @@ -168,6 +170,10 @@ func mapHostError(err error, effectOutcome bool, now time.Time) error { if effectOutcome && !connect.IsWireError(err) { return &OutcomeUnknown{cause: err} } + var encodingError *unsupportedResponseEncoding + if errors.As(err, &encodingError) { + return &DatasourceError{cause: err} + } var ce *connect.Error if !errors.As(err, &ce) { return err diff --git a/datasource/operations.go b/datasource/operations.go index 9a18b1f..f4cd44c 100644 --- a/datasource/operations.go +++ b/datasource/operations.go @@ -183,6 +183,8 @@ func (c *Client) DeclareOperations(ctx context.Context, orgID, sourceID string, } // ListOperations reads the host's admitted set without retaining a local copy. +// An unspecified host effect returns InputError; an unrecognized effect enum +// returns DatasourceError. Neither returns a partial list. func (c *Client) ListOperations(ctx context.Context, orgID, sourceID string) ([]Operation, error) { response, err := c.inner.ListSourceOperations(ctx, connect.NewRequest(&v1.ListSourceOperationsRequest{ OrgId: orgID, SourceId: sourceID, @@ -206,8 +208,10 @@ func (c *Client) ListOperations(ctx context.Context, orgID, sourceID string) ([] effect = EffectReadOnly case v1.SourceOperation_EFFECT_MUTATION: effect = EffectMutation + case v1.SourceOperation_EFFECT_UNSPECIFIED: + return nil, &InputError{cause: errors.New("host returned an unspecified effect")} default: - return nil, fmt.Errorf("datasource: unsupported host operation effect %v", value.GetEffect()) + return nil, &DatasourceError{cause: fmt.Errorf("datasource: unsupported host operation effect %v", value.GetEffect())} } operations = append(operations, Operation{ Name: value.GetName(), Description: value.GetDescription(), Digest: value.GetDigest(), diff --git a/datasource/parity_test.go b/datasource/parity_test.go index 3e101e6..e21e513 100644 --- a/datasource/parity_test.go +++ b/datasource/parity_test.go @@ -183,6 +183,48 @@ func TestNonWireForbiddenHasMethodSpecificMapping(t *testing.T) { } } +func TestUnsupportedResponseEncodingIsTypedBeforeHTTPFallback(t *testing.T) { + for _, encoding := range []string{"br", "BR", "deflate", "gzip, br"} { + for _, status := range []int{http.StatusForbidden, http.StatusInternalServerError} { + for _, contentType := range []string{"application/json", "text/plain"} { + for _, method := range []string{"invoke", "lookup", "declare", "list"} { + t.Run(fmt.Sprintf("%s/%s/%s/%d", method, encoding, contentType, status), func(t *testing.T) { + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Encoding", encoding) + w.WriteHeader(status) + fmt.Fprint(w, `{"code":"permission_denied","message":"not authoritative under unsupported encoding"}`) + })) + defer server.Close() + receipt, err := parityCall(datasource.New(gw{base: server.URL, client: server.Client()}), method) + var transport *connect.Error + if !errors.As(err, &transport) || transport.Code() != connect.CodeInternal || connect.IsWireError(err) || + errors.Is(err, datasource.ErrNotPermitted) || calls.Load() != 1 { + t.Fatalf("unsupported encoding inferred an outcome or retried: %v, calls %d", err, calls.Load()) + } + var responseErr *datasource.DatasourceError + if method == "declare" || method == "list" { + if !errors.As(err, &responseErr) || responseErr.Unwrap() != transport || + receipt != nil || !reflect.DeepEqual(responseErr.Receipt, datasource.Receipt{}) { + t.Fatalf("management encoding error = %T %v, receipt %+v", err, err, receipt) + } + } else { + var unknown *datasource.OutcomeUnknown + if !errors.As(err, &unknown) || errors.As(err, &responseErr) || + receipt == nil || receipt.EffectID == "" || receipt.Status != datasource.ReceiptStatusUnknown || + unknown.Receipt.EffectID != receipt.EffectID || unknown.Unwrap() != transport { + t.Fatalf("encoding error lost effect evidence: %+v, %v", receipt, err) + } + } + }) + } + } + } + } +} + func TestCommittedOutputFieldsRetainTheirOwnEvidence(t *testing.T) { for _, tt := range []struct { name, top, nested string @@ -313,3 +355,63 @@ func TestMissingSchemasRefuseDeclareAndNormalizeList(t *testing.T) { t.Fatalf("explicit empty schemas did not redeclare: %v", err) } } + +func checkInvalidListedEffect(t *testing.T, operations []datasource.Operation, err error, unspecified bool, calls int32) { + t.Helper() + if operations != nil || calls != 1 { + t.Fatalf("invalid listed effect returned partial data or retried: %+v, %v, calls %d", operations, err, calls) + } + var inputErr *datasource.InputError + var responseErr *datasource.DatasourceError + if unspecified { + if !errors.As(err, &inputErr) || inputErr.Pointer != "" || inputErr.Error() != "host returned an unspecified effect" || + inputErr.Unwrap() == nil || errors.As(err, &responseErr) { + t.Fatalf("unspecified listed effect = %T %v", err, err) + } + } else if !errors.As(err, &responseErr) || responseErr.Unwrap() == nil || + !reflect.DeepEqual(responseErr.Receipt, datasource.Receipt{}) || errors.As(err, &inputErr) { + t.Fatalf("unrecognized listed effect = %T %v", err, err) + } +} + +func TestInvalidListedEffectIsTyped(t *testing.T) { + for _, jsonCodec := range []bool{false, true} { + for _, effect := range []v1.SourceOperation_Effect{v1.SourceOperation_EFFECT_UNSPECIFIED, 42} { + t.Run(fmt.Sprintf("json=%t/effect=%d", jsonCodec, effect), func(t *testing.T) { + var calls atomic.Int32 + var opts []connect.ClientOption + if jsonCodec { + opts = append(opts, connect.WithProtoJSON()) + } + c := newOperationsClient(t, &operationsHandler{ + list: func(context.Context, *connect.Request[v1.ListSourceOperationsRequest]) (*connect.Response[v1.ListSourceOperationsResponse], error) { + calls.Add(1) + return connect.NewResponse(&v1.ListSourceOperationsResponse{Operations: []*v1.SourceOperation{ + {Name: "valid", Effect: v1.SourceOperation_EFFECT_READ_ONLY}, + {Name: "invalid", Effect: effect}, + }}), nil + }, + }, opts...) + operations, err := c.ListOperations(context.Background(), "org", "source") + checkInvalidListedEffect(t, operations, err, effect == v1.SourceOperation_EFFECT_UNSPECIFIED, calls.Load()) + }) + } + } +} + +func TestUnspecifiedListedEffectJSONRepresentations(t *testing.T) { + for _, operation := range []string{`{}`, `{"effect":0}`, `{"effect":"EFFECT_UNSPECIFIED"}`} { + t.Run(operation, func(t *testing.T) { + var calls atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"operations":[%s]}`, operation) + })) + defer server.Close() + c := datasource.New(gw{base: server.URL, client: server.Client()}, connect.WithProtoJSON()) + operations, err := c.ListOperations(context.Background(), "org", "source") + checkInvalidListedEffect(t, operations, err, true, calls.Load()) + }) + } +} From 69d5b061a3bb06a76d664340d35acd38665eb033 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:19:08 -0400 Subject: [PATCH 11/12] chore: regenerate from module-saas-starter v0.0.94 --- .claude/skills/refresh-generated-api/SKILL.md | 11 +- .github/workflows/ci.yml | 16 +- CHANGELOG.md | 25 +- README.md | 11 +- SOURCE.txt | 6 +- .../installations.connect.go | 187 +++++ .../module_capabilities.connect.go | 137 ++-- .../platform_admin.connect.go | 68 ++ gen/saas/accounts/v1/installations.pb.go | 700 ++++++++++++++++-- .../accounts/v1/module_capabilities.pb.go | 127 ++-- .../accounts/v1/module_registration.pb.go | 428 +++-------- gen/saas/accounts/v1/platform_admin.pb.go | 487 +++++++++--- moduleauthority/broker_test.go | 2 +- 13 files changed, 1531 insertions(+), 674 deletions(-) diff --git a/.claude/skills/refresh-generated-api/SKILL.md b/.claude/skills/refresh-generated-api/SKILL.md index d791682..db662c9 100644 --- a/.claude/skills/refresh-generated-api/SKILL.md +++ b/.claude/skills/refresh-generated-api/SKILL.md @@ -21,11 +21,14 @@ git clone --filter=blob:none --no-checkout \ https://github.com/codefly-dev/module-saas-starter.git starter cd starter git sparse-checkout set --no-cone 'module/services/accounts/proto' -git checkout # a commit SHA, so the result is reproducible +git fetch origin tag +git checkout --detach # use the published release tag when available ``` Put it outside this repo's tree — a scratch directory, not a sibling path a `./...` pattern can pick up. +Verify an annotated tag's target with `git rev-parse '^{commit}'`. +For development before a release, use an exact PR-head commit instead. ## 2. Generate @@ -79,8 +82,10 @@ of the several green-making changes is the real fix. Three files move with the tree, and a tag that skips one claims a contract it does not carry: -- **`SOURCE.txt`** — the ref you checked out, and the module version from - upstream's `module/module.package.codefly.yaml`. +- **`SOURCE.txt`** — the release tag you checked out (or exact PR-head commit + before a release), and the module version from upstream's + `module/module.package.codefly.yaml`. CI resolves release pins through + `refs/tags/` so a same-named branch cannot select the source. - **`CHANGELOG.md`** — under the unreleased heading. `### Added` for new services, facades, and fields; `### Removed (breaking)` for anything the regeneration deleted, naming the symbols a consumer will fail to compile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35966cf..35524c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: # gen/ must be exactly what buf.gen.yaml produces from the proto ref recorded # in SOURCE.txt. The recipe is the one in the refresh-generated-api skill: # this repo's buf.gen.yaml (plugin versions pinned there), run from - # module/services/accounts/proto of the source repo at the recorded commit. + # module/services/accounts/proto of the source repo at the recorded tag or commit. # clean: true in the template wipes gen/ first, so a stale, extra or edited # file all show up as a diff. gen-drift: @@ -59,9 +59,9 @@ jobs: id: source run: | set -euo pipefail - line=$(grep -m1 -oE 'codefly-dev/module-saas-starter@[0-9a-f]{40}' sdk/SOURCE.txt || true) + line=$(grep -m1 -oE 'codefly-dev/module-saas-starter@(v[0-9]+\.[0-9]+\.[0-9]+|[0-9a-f]{40})$' sdk/SOURCE.txt || true) if [ -z "$line" ]; then - echo "::error file=SOURCE.txt::no 'codefly-dev/module-saas-starter@<40-hex sha>' ref found" + echo "::error file=SOURCE.txt::no 'codefly-dev/module-saas-starter@' ref found" exit 1 fi proto=$(grep -m1 -oE 'accounts public proto: [^)]+' sdk/SOURCE.txt | sed 's/^accounts public proto: //') @@ -69,14 +69,18 @@ jobs: echo "::error file=SOURCE.txt::no '(accounts public proto: )' line found" exit 1 fi - echo "sha=${line#*@}" >> "$GITHUB_OUTPUT" + ref=${line#*@} + if [[ "$ref" == v* ]]; then + ref="refs/tags/$ref" + fi + echo "ref=$ref" >> "$GITHUB_OUTPUT" echo "proto=$proto" >> "$GITHUB_OUTPUT" echo "source: codefly-dev/module-saas-starter@${line#*@} ($proto)" # The source repo is public, so the default token is enough. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: codefly-dev/module-saas-starter - ref: ${{ steps.source.outputs.sha }} + ref: ${{ steps.source.outputs.ref }} path: source sparse-checkout: ${{ steps.source.outputs.proto }} sparse-checkout-cone-mode: false @@ -100,7 +104,7 @@ jobs: run: | set -euo pipefail if [ -z "$(git status --porcelain)" ]; then - echo "gen/ reproduces byte-for-byte from $(grep -m1 -oE 'module-saas-starter@[0-9a-f]{40}' SOURCE.txt)" + echo "gen/ reproduces byte-for-byte from $(grep -m1 -oE 'module-saas-starter@(v[0-9]+\.[0-9]+\.[0-9]+|[0-9a-f]{40})$' SOURCE.txt)" exit 0 fi echo "::error::gen/ does not match a regeneration from the ref in SOURCE.txt. Never hand-edit gen/: regenerate it (refresh-generated-api skill), or correct SOURCE.txt if it records the wrong ref." diff --git a/CHANGELOG.md b/CHANGELOG.md index c3d8192..c94a436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased — callable sources (awaits host tag) +## Unreleased — callable sources - Reject unsupported HTTP response encodings before reading the body or inferring an HTTP status: Declare/List return `DatasourceError` with no @@ -59,10 +59,14 @@ existing org/source arguments cannot override the host's derived authority. - Update `golang.org/x/tools` so the API boundary gate can read Go 1.27 export data; the gate and its fixtures are unchanged. -- Regenerate the full host API from PR #1052 head - `2a16c688c61d66645e1063c495fa38eaf2fa52de`, recorded in `SOURCE.txt` with module - version `0.1.0`. This is a development PR head, not a release tag. Release - waits for the host tag and regeneration from its commit. +- Regenerate the full host API from released tag `v0.0.94`, recorded in + `SOURCE.txt` with module version `0.1.0`. The datasource bindings are unchanged + from the reviewed PR-head contract. This refresh creates no SDK release. + CI and the regeneration procedure now accept release-tag provenance. +- Include the host's installation narrowing, organisation installation listing + and team grant/revoke RPCs, plus platform-admin module envelope reads/writes, + in the regenerated API. Module capability comments describe the host's + structured tenant-membership and idempotency refusals. - Include the runtime-facing `PruneSourceOperationReceipts` and `LookupPruneSourceOperationReceipts` RPCs in generated bindings only; no receipt retention methods are exposed by the caller facade. @@ -73,6 +77,17 @@ ### Removed / changed (breaking, inherited host contract) +- Host `v0.0.94` removes `MintModuleRegistration` and `MintSolutionRegistration` + RPCs, their procedure constants, and `ModuleMintRegistrationRequest/Response` + and `SolutionMintRegistrationRequest/Response`. Work Context mints now prove + workload identity with projected ServiceAccount tokens; there is no replacement + registration-credential mint RPC. +- Generated `ModuleMintWorkContextRequest`, `ModuleMintOperationContextRequest` + and `ModuleMintSourceOperationContextRequest` lose `Secret`/`GetSecret` and + gain `ServiceAccountToken`/`GetServiceAccountToken`. The first two requests + also gain `OrganizationId` for shared-principal installation selection. + The gateway route-rejection fixture now uses the current request field; + the hand-written gateway broker transport is unchanged. - `moduleauthority.CurrentInstallation.SolutionIdentifier` is replaced by `TargetID` and `BindingID`, reflecting the host's immutable authority identities. The SDK validates the target UUID and nonempty binding. diff --git a/README.md b/README.md index ce6e50c..e03b2a5 100644 --- a/README.md +++ b/README.md @@ -219,8 +219,8 @@ The SDK surfaces are: This call surface uses the four host RPCs from [module-saas-starter#1052](https://github.com/codefly-dev/module-saas-starter/pull/1052). - `SOURCE.txt` records its exact development PR head. It is not a release tag: - release waits for the host tag and regeneration from the tagged commit. + `SOURCE.txt` records the released host tag `v0.0.94` used to regenerate + the bindings. This regeneration does not publish an SDK release. The host's runtime-scheduled `PruneSourceOperationReceipts` and `LookupPruneSourceOperationReceipts` are included in the generated bindings only; this caller facade does not expose receipt retention operations. @@ -379,9 +379,11 @@ proto ref `gen/` was generated from. ## Regenerating `gen/` The proto source of truth is `module-saas-starter/module/services/accounts/proto`. -To refresh (from a checkout of that repo): +To refresh from a released tag (in a checkout of that repo): ```bash +git fetch origin tag v0.0.94 +git checkout --detach v0.0.94 cd module/services/accounts/proto buf generate --template /buf.gen.yaml -o ``` @@ -400,6 +402,9 @@ protobuf initialization. Core v0.3.41 supplies the required Runnable package; the SDK need not adopt the host's entire dependency generation. Verify the compatible dependency floor with SDK and consumer builds when refreshing it. The generation plugin versions remain pinned. +Record the release tag in `SOURCE.txt`; CI checks out that tag through +`refs/tags/` and verifies that regeneration leaves no diff. Before a host +release, an exact PR-head commit may be used for development. > Wiring this regen into module-saas-starter's release (so a saas tag publishes a > matching SDK tag) is tracked in the solutions EPIC (obin-ai/lodestar#53, item 4). diff --git a/SOURCE.txt b/SOURCE.txt index 4345fa7..995682c 100644 --- a/SOURCE.txt +++ b/SOURCE.txt @@ -1,8 +1,8 @@ -gen/ generated from codefly-dev/module-saas-starter@2a16c688c61d66645e1063c495fa38eaf2fa52de +gen/ generated from codefly-dev/module-saas-starter@v0.0.94 (accounts public proto: module/services/accounts/proto) module version: 0.1.0 (module/module.package.codefly.yaml) This SDK's release is matched to the saas-starter module version above. -Development ref: PR #1052 head, not a release tag. -Release waits on the host tag; regenerate from its commit before releasing. +Released upstream tag: v0.0.94. +Regeneration does not publish an SDK release. diff --git a/gen/saas/accounts/v1/accountsv1connect/installations.connect.go b/gen/saas/accounts/v1/accountsv1connect/installations.connect.go index c349fe2..82d172d 100644 --- a/gen/saas/accounts/v1/accountsv1connect/installations.connect.go +++ b/gen/saas/accounts/v1/accountsv1connect/installations.connect.go @@ -40,6 +40,9 @@ const ( // InstallationServiceUninstallSolutionProcedure is the fully-qualified name of the // InstallationService's UninstallSolution RPC. InstallationServiceUninstallSolutionProcedure = "/saas.accounts.v1.InstallationService/UninstallSolution" + // InstallationServiceNarrowInstallationProcedure is the fully-qualified name of the + // InstallationService's NarrowInstallation RPC. + InstallationServiceNarrowInstallationProcedure = "/saas.accounts.v1.InstallationService/NarrowInstallation" // InstallationServiceTransferInstallationOwnershipProcedure is the fully-qualified name of the // InstallationService's TransferInstallationOwnership RPC. InstallationServiceTransferInstallationOwnershipProcedure = "/saas.accounts.v1.InstallationService/TransferInstallationOwnership" @@ -52,6 +55,15 @@ const ( // InstallationServiceListAvailableSolutionsProcedure is the fully-qualified name of the // InstallationService's ListAvailableSolutions RPC. InstallationServiceListAvailableSolutionsProcedure = "/saas.accounts.v1.InstallationService/ListAvailableSolutions" + // InstallationServiceListOrganizationInstallationsProcedure is the fully-qualified name of the + // InstallationService's ListOrganizationInstallations RPC. + InstallationServiceListOrganizationInstallationsProcedure = "/saas.accounts.v1.InstallationService/ListOrganizationInstallations" + // InstallationServiceGrantInstallationToTeamProcedure is the fully-qualified name of the + // InstallationService's GrantInstallationToTeam RPC. + InstallationServiceGrantInstallationToTeamProcedure = "/saas.accounts.v1.InstallationService/GrantInstallationToTeam" + // InstallationServiceRevokeInstallationFromTeamProcedure is the fully-qualified name of the + // InstallationService's RevokeInstallationFromTeam RPC. + InstallationServiceRevokeInstallationFromTeamProcedure = "/saas.accounts.v1.InstallationService/RevokeInstallationFromTeam" ) // InstallationServiceClient is a client for the saas.accounts.v1.InstallationService service. @@ -66,6 +78,9 @@ type InstallationServiceClient interface { // is left in place (inert without a grant or a live agent) so a reinstall reuses // it. Idempotent on an already-revoked installation. UninstallSolution(context.Context, *connect.Request[v1.UninstallSolutionRequest]) (*connect.Response[emptypb.Empty], error) + // Narrowing is witnessed in the policy log before commit, then atomically + // bumps the installation revision, cancels its org's work and records audit. + NarrowInstallation(context.Context, *connect.Request[v1.NarrowInstallationRequest]) (*connect.Response[emptypb.Empty], error) // TransferInstallationOwnership reassigns the owner of record and replaces the // co-owner succession set. This is how a "connection owner left the org" // failure is repaired: an admin transfers the installation to a current admin, @@ -99,6 +114,42 @@ type InstallationServiceClient interface { // not: here the alias is a display and routing fact read from the target in // the same statement, not an identity anything joins on. ListAvailableSolutions(context.Context, *connect.Request[v1.ListAvailableSolutionsRequest]) (*connect.Response[v1.ListAvailableSolutionsResponse], error) + // ListOrganizationInstallations is an organisation administrator's view of + // what that organisation has installed: each active installation with its live + // health, the catalogue row of the target it installed, the scope path of its + // authority root, and the teams a grant reaches there. + // + // It is the org-admin companion to the internal ListInstallations, and it is a + // separate method rather than a relaxed policy on that one: ListInstallations + // is read by the gateway for an organisation it projected from a verified + // identity, and a bare tenant credential must never reach it. This one binds + // the organisation to the caller's own admin membership, exactly as + // ListAvailableSolutions does, so no organisation's administrator can read + // another's. The one exception is inherited, not added here: a platform + // super administrator (not impersonating) passes the organisation-admin check + // for every organisation, as on every other organisation-admin method, and so + // may list any organisation's installations. + ListOrganizationInstallations(context.Context, *connect.Request[v1.ListOrganizationInstallationsRequest]) (*connect.Response[v1.ListOrganizationInstallationsResponse], error) + // GrantInstallationToTeam exposes an installation to a team: a scope grant of + // the given role to the team at the installation's authority-root node. The + // host resolves that node from the installation, so a caller never names a + // scope path. The role must permit using a solution, or the grant would expose + // nothing; the team must belong to the organisation. Using a solution is the + // minimum the role must permit, not a ceiling: the team holds everything the + // role permits beneath the installation's authority root, exactly as a + // GrantScope of that role at that node would give it. Emits scope.granted, the + // same event and audit record as any other scope grant, because it is one. + // Organisation-admin, with the same platform super administrator exception + // ListOrganizationInstallations describes. + GrantInstallationToTeam(context.Context, *connect.Request[v1.GrantInstallationToTeamRequest]) (*connect.Response[v1.GrantInstallationToTeamResponse], error) + // RevokeInstallationFromTeam removes one team grant at an installation's + // authority-root node. It is a narrowing, so it runs under the policy log + // exactly as RevokeScope does: a host that cannot witness the narrowing refuses + // it, and says so, rather than revoking unwitnessed. It removes the one grant + // at the authority root that granted_teams reports; a team that also reaches + // the installation from a grant above keeps that reach. Organisation-admin, + // with the same platform super administrator exception. + RevokeInstallationFromTeam(context.Context, *connect.Request[v1.RevokeInstallationFromTeamRequest]) (*connect.Response[emptypb.Empty], error) } // NewInstallationServiceClient constructs a client for the saas.accounts.v1.InstallationService @@ -124,6 +175,12 @@ func NewInstallationServiceClient(httpClient connect.HTTPClient, baseURL string, connect.WithSchema(installationServiceMethods.ByName("UninstallSolution")), connect.WithClientOptions(opts...), ), + narrowInstallation: connect.NewClient[v1.NarrowInstallationRequest, emptypb.Empty]( + httpClient, + baseURL+InstallationServiceNarrowInstallationProcedure, + connect.WithSchema(installationServiceMethods.ByName("NarrowInstallation")), + connect.WithClientOptions(opts...), + ), transferInstallationOwnership: connect.NewClient[v1.TransferInstallationOwnershipRequest, v1.Installation]( httpClient, baseURL+InstallationServiceTransferInstallationOwnershipProcedure, @@ -148,6 +205,24 @@ func NewInstallationServiceClient(httpClient connect.HTTPClient, baseURL string, connect.WithSchema(installationServiceMethods.ByName("ListAvailableSolutions")), connect.WithClientOptions(opts...), ), + listOrganizationInstallations: connect.NewClient[v1.ListOrganizationInstallationsRequest, v1.ListOrganizationInstallationsResponse]( + httpClient, + baseURL+InstallationServiceListOrganizationInstallationsProcedure, + connect.WithSchema(installationServiceMethods.ByName("ListOrganizationInstallations")), + connect.WithClientOptions(opts...), + ), + grantInstallationToTeam: connect.NewClient[v1.GrantInstallationToTeamRequest, v1.GrantInstallationToTeamResponse]( + httpClient, + baseURL+InstallationServiceGrantInstallationToTeamProcedure, + connect.WithSchema(installationServiceMethods.ByName("GrantInstallationToTeam")), + connect.WithClientOptions(opts...), + ), + revokeInstallationFromTeam: connect.NewClient[v1.RevokeInstallationFromTeamRequest, emptypb.Empty]( + httpClient, + baseURL+InstallationServiceRevokeInstallationFromTeamProcedure, + connect.WithSchema(installationServiceMethods.ByName("RevokeInstallationFromTeam")), + connect.WithClientOptions(opts...), + ), } } @@ -155,10 +230,14 @@ func NewInstallationServiceClient(httpClient connect.HTTPClient, baseURL string, type installationServiceClient struct { installSolution *connect.Client[v1.InstallSolutionRequest, v1.Installation] uninstallSolution *connect.Client[v1.UninstallSolutionRequest, emptypb.Empty] + narrowInstallation *connect.Client[v1.NarrowInstallationRequest, emptypb.Empty] transferInstallationOwnership *connect.Client[v1.TransferInstallationOwnershipRequest, v1.Installation] getInstallation *connect.Client[v1.GetInstallationRequest, v1.GetInstallationResponse] listInstallations *connect.Client[v1.ListInstallationsRequest, v1.ListInstallationsResponse] listAvailableSolutions *connect.Client[v1.ListAvailableSolutionsRequest, v1.ListAvailableSolutionsResponse] + listOrganizationInstallations *connect.Client[v1.ListOrganizationInstallationsRequest, v1.ListOrganizationInstallationsResponse] + grantInstallationToTeam *connect.Client[v1.GrantInstallationToTeamRequest, v1.GrantInstallationToTeamResponse] + revokeInstallationFromTeam *connect.Client[v1.RevokeInstallationFromTeamRequest, emptypb.Empty] } // InstallSolution calls saas.accounts.v1.InstallationService.InstallSolution. @@ -171,6 +250,11 @@ func (c *installationServiceClient) UninstallSolution(ctx context.Context, req * return c.uninstallSolution.CallUnary(ctx, req) } +// NarrowInstallation calls saas.accounts.v1.InstallationService.NarrowInstallation. +func (c *installationServiceClient) NarrowInstallation(ctx context.Context, req *connect.Request[v1.NarrowInstallationRequest]) (*connect.Response[emptypb.Empty], error) { + return c.narrowInstallation.CallUnary(ctx, req) +} + // TransferInstallationOwnership calls // saas.accounts.v1.InstallationService.TransferInstallationOwnership. func (c *installationServiceClient) TransferInstallationOwnership(ctx context.Context, req *connect.Request[v1.TransferInstallationOwnershipRequest]) (*connect.Response[v1.Installation], error) { @@ -192,6 +276,22 @@ func (c *installationServiceClient) ListAvailableSolutions(ctx context.Context, return c.listAvailableSolutions.CallUnary(ctx, req) } +// ListOrganizationInstallations calls +// saas.accounts.v1.InstallationService.ListOrganizationInstallations. +func (c *installationServiceClient) ListOrganizationInstallations(ctx context.Context, req *connect.Request[v1.ListOrganizationInstallationsRequest]) (*connect.Response[v1.ListOrganizationInstallationsResponse], error) { + return c.listOrganizationInstallations.CallUnary(ctx, req) +} + +// GrantInstallationToTeam calls saas.accounts.v1.InstallationService.GrantInstallationToTeam. +func (c *installationServiceClient) GrantInstallationToTeam(ctx context.Context, req *connect.Request[v1.GrantInstallationToTeamRequest]) (*connect.Response[v1.GrantInstallationToTeamResponse], error) { + return c.grantInstallationToTeam.CallUnary(ctx, req) +} + +// RevokeInstallationFromTeam calls saas.accounts.v1.InstallationService.RevokeInstallationFromTeam. +func (c *installationServiceClient) RevokeInstallationFromTeam(ctx context.Context, req *connect.Request[v1.RevokeInstallationFromTeamRequest]) (*connect.Response[emptypb.Empty], error) { + return c.revokeInstallationFromTeam.CallUnary(ctx, req) +} + // InstallationServiceHandler is an implementation of the saas.accounts.v1.InstallationService // service. type InstallationServiceHandler interface { @@ -205,6 +305,9 @@ type InstallationServiceHandler interface { // is left in place (inert without a grant or a live agent) so a reinstall reuses // it. Idempotent on an already-revoked installation. UninstallSolution(context.Context, *connect.Request[v1.UninstallSolutionRequest]) (*connect.Response[emptypb.Empty], error) + // Narrowing is witnessed in the policy log before commit, then atomically + // bumps the installation revision, cancels its org's work and records audit. + NarrowInstallation(context.Context, *connect.Request[v1.NarrowInstallationRequest]) (*connect.Response[emptypb.Empty], error) // TransferInstallationOwnership reassigns the owner of record and replaces the // co-owner succession set. This is how a "connection owner left the org" // failure is repaired: an admin transfers the installation to a current admin, @@ -238,6 +341,42 @@ type InstallationServiceHandler interface { // not: here the alias is a display and routing fact read from the target in // the same statement, not an identity anything joins on. ListAvailableSolutions(context.Context, *connect.Request[v1.ListAvailableSolutionsRequest]) (*connect.Response[v1.ListAvailableSolutionsResponse], error) + // ListOrganizationInstallations is an organisation administrator's view of + // what that organisation has installed: each active installation with its live + // health, the catalogue row of the target it installed, the scope path of its + // authority root, and the teams a grant reaches there. + // + // It is the org-admin companion to the internal ListInstallations, and it is a + // separate method rather than a relaxed policy on that one: ListInstallations + // is read by the gateway for an organisation it projected from a verified + // identity, and a bare tenant credential must never reach it. This one binds + // the organisation to the caller's own admin membership, exactly as + // ListAvailableSolutions does, so no organisation's administrator can read + // another's. The one exception is inherited, not added here: a platform + // super administrator (not impersonating) passes the organisation-admin check + // for every organisation, as on every other organisation-admin method, and so + // may list any organisation's installations. + ListOrganizationInstallations(context.Context, *connect.Request[v1.ListOrganizationInstallationsRequest]) (*connect.Response[v1.ListOrganizationInstallationsResponse], error) + // GrantInstallationToTeam exposes an installation to a team: a scope grant of + // the given role to the team at the installation's authority-root node. The + // host resolves that node from the installation, so a caller never names a + // scope path. The role must permit using a solution, or the grant would expose + // nothing; the team must belong to the organisation. Using a solution is the + // minimum the role must permit, not a ceiling: the team holds everything the + // role permits beneath the installation's authority root, exactly as a + // GrantScope of that role at that node would give it. Emits scope.granted, the + // same event and audit record as any other scope grant, because it is one. + // Organisation-admin, with the same platform super administrator exception + // ListOrganizationInstallations describes. + GrantInstallationToTeam(context.Context, *connect.Request[v1.GrantInstallationToTeamRequest]) (*connect.Response[v1.GrantInstallationToTeamResponse], error) + // RevokeInstallationFromTeam removes one team grant at an installation's + // authority-root node. It is a narrowing, so it runs under the policy log + // exactly as RevokeScope does: a host that cannot witness the narrowing refuses + // it, and says so, rather than revoking unwitnessed. It removes the one grant + // at the authority root that granted_teams reports; a team that also reaches + // the installation from a grant above keeps that reach. Organisation-admin, + // with the same platform super administrator exception. + RevokeInstallationFromTeam(context.Context, *connect.Request[v1.RevokeInstallationFromTeamRequest]) (*connect.Response[emptypb.Empty], error) } // NewInstallationServiceHandler builds an HTTP handler from the service implementation. It returns @@ -259,6 +398,12 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne connect.WithSchema(installationServiceMethods.ByName("UninstallSolution")), connect.WithHandlerOptions(opts...), ) + installationServiceNarrowInstallationHandler := connect.NewUnaryHandler( + InstallationServiceNarrowInstallationProcedure, + svc.NarrowInstallation, + connect.WithSchema(installationServiceMethods.ByName("NarrowInstallation")), + connect.WithHandlerOptions(opts...), + ) installationServiceTransferInstallationOwnershipHandler := connect.NewUnaryHandler( InstallationServiceTransferInstallationOwnershipProcedure, svc.TransferInstallationOwnership, @@ -283,12 +428,32 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne connect.WithSchema(installationServiceMethods.ByName("ListAvailableSolutions")), connect.WithHandlerOptions(opts...), ) + installationServiceListOrganizationInstallationsHandler := connect.NewUnaryHandler( + InstallationServiceListOrganizationInstallationsProcedure, + svc.ListOrganizationInstallations, + connect.WithSchema(installationServiceMethods.ByName("ListOrganizationInstallations")), + connect.WithHandlerOptions(opts...), + ) + installationServiceGrantInstallationToTeamHandler := connect.NewUnaryHandler( + InstallationServiceGrantInstallationToTeamProcedure, + svc.GrantInstallationToTeam, + connect.WithSchema(installationServiceMethods.ByName("GrantInstallationToTeam")), + connect.WithHandlerOptions(opts...), + ) + installationServiceRevokeInstallationFromTeamHandler := connect.NewUnaryHandler( + InstallationServiceRevokeInstallationFromTeamProcedure, + svc.RevokeInstallationFromTeam, + connect.WithSchema(installationServiceMethods.ByName("RevokeInstallationFromTeam")), + connect.WithHandlerOptions(opts...), + ) return "/saas.accounts.v1.InstallationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case InstallationServiceInstallSolutionProcedure: installationServiceInstallSolutionHandler.ServeHTTP(w, r) case InstallationServiceUninstallSolutionProcedure: installationServiceUninstallSolutionHandler.ServeHTTP(w, r) + case InstallationServiceNarrowInstallationProcedure: + installationServiceNarrowInstallationHandler.ServeHTTP(w, r) case InstallationServiceTransferInstallationOwnershipProcedure: installationServiceTransferInstallationOwnershipHandler.ServeHTTP(w, r) case InstallationServiceGetInstallationProcedure: @@ -297,6 +462,12 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne installationServiceListInstallationsHandler.ServeHTTP(w, r) case InstallationServiceListAvailableSolutionsProcedure: installationServiceListAvailableSolutionsHandler.ServeHTTP(w, r) + case InstallationServiceListOrganizationInstallationsProcedure: + installationServiceListOrganizationInstallationsHandler.ServeHTTP(w, r) + case InstallationServiceGrantInstallationToTeamProcedure: + installationServiceGrantInstallationToTeamHandler.ServeHTTP(w, r) + case InstallationServiceRevokeInstallationFromTeamProcedure: + installationServiceRevokeInstallationFromTeamHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -314,6 +485,10 @@ func (UnimplementedInstallationServiceHandler) UninstallSolution(context.Context return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.UninstallSolution is not implemented")) } +func (UnimplementedInstallationServiceHandler) NarrowInstallation(context.Context, *connect.Request[v1.NarrowInstallationRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.NarrowInstallation is not implemented")) +} + func (UnimplementedInstallationServiceHandler) TransferInstallationOwnership(context.Context, *connect.Request[v1.TransferInstallationOwnershipRequest]) (*connect.Response[v1.Installation], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.TransferInstallationOwnership is not implemented")) } @@ -329,3 +504,15 @@ func (UnimplementedInstallationServiceHandler) ListInstallations(context.Context func (UnimplementedInstallationServiceHandler) ListAvailableSolutions(context.Context, *connect.Request[v1.ListAvailableSolutionsRequest]) (*connect.Response[v1.ListAvailableSolutionsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.ListAvailableSolutions is not implemented")) } + +func (UnimplementedInstallationServiceHandler) ListOrganizationInstallations(context.Context, *connect.Request[v1.ListOrganizationInstallationsRequest]) (*connect.Response[v1.ListOrganizationInstallationsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.ListOrganizationInstallations is not implemented")) +} + +func (UnimplementedInstallationServiceHandler) GrantInstallationToTeam(context.Context, *connect.Request[v1.GrantInstallationToTeamRequest]) (*connect.Response[v1.GrantInstallationToTeamResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.GrantInstallationToTeam is not implemented")) +} + +func (UnimplementedInstallationServiceHandler) RevokeInstallationFromTeam(context.Context, *connect.Request[v1.RevokeInstallationFromTeamRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.InstallationService.RevokeInstallationFromTeam is not implemented")) +} diff --git a/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go b/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go index ff44d6d..427ccc3 100644 --- a/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go +++ b/gen/saas/accounts/v1/accountsv1connect/module_capabilities.connect.go @@ -107,12 +107,6 @@ const ( // ModuleCapabilitiesServiceFetchDatasourceFilesProcedure is the fully-qualified name of the // ModuleCapabilitiesService's FetchDatasourceFiles RPC. ModuleCapabilitiesServiceFetchDatasourceFilesProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/FetchDatasourceFiles" - // ModuleCapabilitiesServiceMintModuleRegistrationProcedure is the fully-qualified name of the - // ModuleCapabilitiesService's MintModuleRegistration RPC. - ModuleCapabilitiesServiceMintModuleRegistrationProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/MintModuleRegistration" - // ModuleCapabilitiesServiceMintSolutionRegistrationProcedure is the fully-qualified name of the - // ModuleCapabilitiesService's MintSolutionRegistration RPC. - ModuleCapabilitiesServiceMintSolutionRegistrationProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/MintSolutionRegistration" // ModuleCapabilitiesServiceMintModuleWorkContextProcedure is the fully-qualified name of the // ModuleCapabilitiesService's MintModuleWorkContext RPC. ModuleCapabilitiesServiceMintModuleWorkContextProcedure = "/saas.accounts.v1.ModuleCapabilitiesService/MintModuleWorkContext" @@ -170,7 +164,10 @@ type ModuleCapabilitiesServiceClient interface { // access oracles can resolve it. Bounded by the resource types the caller // principal's grant declares. PlaceRecord(context.Context, *connect.Request[v1.ModulePlaceRecordRequest]) (*connect.Response[v1.ModulePlaceRecordResponse], error) - // EnqueueJob appends durable work for a tenant- or subject-scoped queue. + // EnqueueJob appends durable work for a tenant- or subject-scoped queue. A + // subject-scoped job whose subject is not a member of the tenant is + // FAILED_PRECONDITION with reason TENANT_MEMBERSHIP_MISSING; see REFUSALS, + // and key on the reason rather than the code. EnqueueJob(context.Context, *connect.Request[v1.ModuleEnqueueJobRequest]) (*connect.Response[v1.ModuleEnqueueJobResponse], error) // ClaimJobs leases a bounded batch of ready jobs from an allowed queue. ClaimJobs(context.Context, *connect.Request[v1.ModuleClaimJobsRequest]) (*connect.Response[v1.ModuleClaimJobsResponse], error) @@ -180,10 +177,17 @@ type ModuleCapabilitiesServiceClient interface { AckJob(context.Context, *connect.Request[v1.ModuleAckJobRequest]) (*connect.Response[emptypb.Empty], error) // NackJob fails a leased job as retryable or permanent. NackJob(context.Context, *connect.Request[v1.ModuleNackJobRequest]) (*connect.Response[emptypb.Empty], error) - // NotifyUser delivers a notification subject to category policy. + // NotifyUser delivers a notification subject to category policy. A recipient + // who is not a member of the tenant is FAILED_PRECONDITION with reason + // TENANT_MEMBERSHIP_MISSING, never PERMISSION_DENIED. An idempotency_key + // already used for different content, and a host that cannot re-read live + // authority, are FAILED_PRECONDITION too under their own reasons, so the + // reason — not the code — says which happened; see REFUSALS. NotifyUser(context.Context, *connect.Request[v1.ModuleNotifyUserRequest]) (*connect.Response[v1.ModuleNotifyUserResponse], error) // NotifyOrgAdmins notifies a tenant's administrators, resolved at send time; - // see the request. + // see the request. It names no recipient, so it has no non-member refusal; an + // idempotency_key already used for different content is FAILED_PRECONDITION + // with reason NOTIFICATION_IDEMPOTENCY_CONFLICT. NotifyOrgAdmins(context.Context, *connect.Request[v1.ModuleNotifyOrgAdminsRequest]) (*connect.Response[v1.ModuleNotifyOrgAdminsResponse], error) // RequestApproval opens a pending approval whose resume job the module claims. RequestApproval(context.Context, *connect.Request[v1.ModuleRequestApprovalRequest]) (*connect.Response[v1.ModuleRequestApprovalResponse], error) @@ -197,7 +201,11 @@ type ModuleCapabilitiesServiceClient interface { // into the namespaces the operator bound to it; see the request. DeclareAuditEventTypes(context.Context, *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) // ListSubjectVisibility projects the tenant's team tree onto one viewer: the - // whole set of other subjects whose rows that viewer may read. + // whole set of other subjects whose rows that viewer may read. A viewer who + // is not a member of the tenant is FAILED_PRECONDITION with reason + // TENANT_MEMBERSHIP_MISSING; a set too large to serve whole is + // FAILED_PRECONDITION with reason SUBJECT_VISIBILITY_SET_TOO_LARGE, and only + // the reason tells a consumer which it got. ListSubjectVisibility(context.Context, *connect.Request[v1.ModuleListSubjectVisibilityRequest]) (*connect.Response[v1.ModuleListSubjectVisibilityResponse], error) // FetchDatasourceBlob streams one datasource file blob to the module that // resolves a change set's blob sha. Authorized by the caller principal's @@ -215,24 +223,15 @@ type ModuleCapabilitiesServiceClient interface { // metadata reset_at) and a RetryInfo; a batch past its limits is // FAILED_PRECONDITION carrying an ErrorInfo naming the limit. FetchDatasourceFiles(context.Context, *connect.Request[v1.FetchDatasourceFilesRequest]) (*connect.ServerStreamForClient[v1.FetchDatasourceFilesFrame], error) - // MintModuleRegistration issues the signed, prefix-bound credential a composed - // module presents to the gateway to federate its REST surface. Authorized by - // the module's own registration secret, not the shared cluster token. - MintModuleRegistration(context.Context, *connect.Request[v1.ModuleMintRegistrationRequest]) (*connect.Response[v1.ModuleMintRegistrationResponse], error) - // MintSolutionRegistration issues the signed, solution-bound credential a - // solution presents to the gateway and to the frontend to register, update, or - // delete its upstream and its Module-Federation remote. Authorized by the - // solution's own registration secret, declared separately from the module - // secrets because a solution remote executes in the host origin. - MintSolutionRegistration(context.Context, *connect.Request[v1.SolutionMintRegistrationRequest]) (*connect.Response[v1.SolutionMintRegistrationResponse], error) // MintModuleWorkContext issues the Work Context a composed module presents to // this surface: owner and sole actor are the module service principal derived - // from its registration prefix. Authorized by the module's own registration - // secret, like the credential exchange above. + // from its prefix. Authorized by the pod's projected ServiceAccount token, held + // against the workload the principal declares and the build the authority + // document approves; a pod running anything else is issued nothing. MintModuleWorkContext(context.Context, *connect.Request[v1.ModuleMintWorkContextRequest]) (*connect.Response[v1.ModuleMintWorkContextResponse], error) // MintModuleOperationContext issues, with no person present, a Work Context // addressed to one of the calling module's installed operation audiences. - // Authorized by the module's identity secret like MintModuleWorkContext; owner + // Authorized by the pod's projected token like MintModuleWorkContext; owner // and sole actor are the module service principal, the tenant is the one its // principal declares, and the scopes are exactly the binding's declared // headless scopes. A binding declaring none is refused. @@ -241,7 +240,7 @@ type ModuleCapabilitiesServiceClient interface { // sync runs with: owned by the person who connected the source, actored by // the calling module's service principal, in the source's organization, with // exactly the source_delegation_scopes of the binding the person delegated - // to. Authorized by the module's identity secret like MintModuleWorkContext; + // to. Authorized by the pod's projected token like MintModuleWorkContext; // every mint re-checks that the delegation is active, the source exists, the // person is still an owner or admin of the organization, and the binding is // unchanged. FAILED_PRECONDITION with reason DELEGATION_MISSING means the @@ -415,18 +414,6 @@ func NewModuleCapabilitiesServiceClient(httpClient connect.HTTPClient, baseURL s connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("FetchDatasourceFiles")), connect.WithClientOptions(opts...), ), - mintModuleRegistration: connect.NewClient[v1.ModuleMintRegistrationRequest, v1.ModuleMintRegistrationResponse]( - httpClient, - baseURL+ModuleCapabilitiesServiceMintModuleRegistrationProcedure, - connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("MintModuleRegistration")), - connect.WithClientOptions(opts...), - ), - mintSolutionRegistration: connect.NewClient[v1.SolutionMintRegistrationRequest, v1.SolutionMintRegistrationResponse]( - httpClient, - baseURL+ModuleCapabilitiesServiceMintSolutionRegistrationProcedure, - connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("MintSolutionRegistration")), - connect.WithClientOptions(opts...), - ), mintModuleWorkContext: connect.NewClient[v1.ModuleMintWorkContextRequest, v1.ModuleMintWorkContextResponse]( httpClient, baseURL+ModuleCapabilitiesServiceMintModuleWorkContextProcedure, @@ -504,8 +491,6 @@ type moduleCapabilitiesServiceClient struct { listSubjectVisibility *connect.Client[v1.ModuleListSubjectVisibilityRequest, v1.ModuleListSubjectVisibilityResponse] fetchDatasourceBlob *connect.Client[v1.FetchDatasourceBlobRequest, v1.FetchDatasourceBlobChunk] fetchDatasourceFiles *connect.Client[v1.FetchDatasourceFilesRequest, v1.FetchDatasourceFilesFrame] - mintModuleRegistration *connect.Client[v1.ModuleMintRegistrationRequest, v1.ModuleMintRegistrationResponse] - mintSolutionRegistration *connect.Client[v1.SolutionMintRegistrationRequest, v1.SolutionMintRegistrationResponse] mintModuleWorkContext *connect.Client[v1.ModuleMintWorkContextRequest, v1.ModuleMintWorkContextResponse] mintModuleOperationContext *connect.Client[v1.ModuleMintOperationContextRequest, v1.ModuleMintOperationContextResponse] mintSourceOperationContext *connect.Client[v1.ModuleMintSourceOperationContextRequest, v1.ModuleMintSourceOperationContextResponse] @@ -645,17 +630,6 @@ func (c *moduleCapabilitiesServiceClient) FetchDatasourceFiles(ctx context.Conte return c.fetchDatasourceFiles.CallServerStream(ctx, req) } -// MintModuleRegistration calls saas.accounts.v1.ModuleCapabilitiesService.MintModuleRegistration. -func (c *moduleCapabilitiesServiceClient) MintModuleRegistration(ctx context.Context, req *connect.Request[v1.ModuleMintRegistrationRequest]) (*connect.Response[v1.ModuleMintRegistrationResponse], error) { - return c.mintModuleRegistration.CallUnary(ctx, req) -} - -// MintSolutionRegistration calls -// saas.accounts.v1.ModuleCapabilitiesService.MintSolutionRegistration. -func (c *moduleCapabilitiesServiceClient) MintSolutionRegistration(ctx context.Context, req *connect.Request[v1.SolutionMintRegistrationRequest]) (*connect.Response[v1.SolutionMintRegistrationResponse], error) { - return c.mintSolutionRegistration.CallUnary(ctx, req) -} - // MintModuleWorkContext calls saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext. func (c *moduleCapabilitiesServiceClient) MintModuleWorkContext(ctx context.Context, req *connect.Request[v1.ModuleMintWorkContextRequest]) (*connect.Response[v1.ModuleMintWorkContextResponse], error) { return c.mintModuleWorkContext.CallUnary(ctx, req) @@ -729,7 +703,10 @@ type ModuleCapabilitiesServiceHandler interface { // access oracles can resolve it. Bounded by the resource types the caller // principal's grant declares. PlaceRecord(context.Context, *connect.Request[v1.ModulePlaceRecordRequest]) (*connect.Response[v1.ModulePlaceRecordResponse], error) - // EnqueueJob appends durable work for a tenant- or subject-scoped queue. + // EnqueueJob appends durable work for a tenant- or subject-scoped queue. A + // subject-scoped job whose subject is not a member of the tenant is + // FAILED_PRECONDITION with reason TENANT_MEMBERSHIP_MISSING; see REFUSALS, + // and key on the reason rather than the code. EnqueueJob(context.Context, *connect.Request[v1.ModuleEnqueueJobRequest]) (*connect.Response[v1.ModuleEnqueueJobResponse], error) // ClaimJobs leases a bounded batch of ready jobs from an allowed queue. ClaimJobs(context.Context, *connect.Request[v1.ModuleClaimJobsRequest]) (*connect.Response[v1.ModuleClaimJobsResponse], error) @@ -739,10 +716,17 @@ type ModuleCapabilitiesServiceHandler interface { AckJob(context.Context, *connect.Request[v1.ModuleAckJobRequest]) (*connect.Response[emptypb.Empty], error) // NackJob fails a leased job as retryable or permanent. NackJob(context.Context, *connect.Request[v1.ModuleNackJobRequest]) (*connect.Response[emptypb.Empty], error) - // NotifyUser delivers a notification subject to category policy. + // NotifyUser delivers a notification subject to category policy. A recipient + // who is not a member of the tenant is FAILED_PRECONDITION with reason + // TENANT_MEMBERSHIP_MISSING, never PERMISSION_DENIED. An idempotency_key + // already used for different content, and a host that cannot re-read live + // authority, are FAILED_PRECONDITION too under their own reasons, so the + // reason — not the code — says which happened; see REFUSALS. NotifyUser(context.Context, *connect.Request[v1.ModuleNotifyUserRequest]) (*connect.Response[v1.ModuleNotifyUserResponse], error) // NotifyOrgAdmins notifies a tenant's administrators, resolved at send time; - // see the request. + // see the request. It names no recipient, so it has no non-member refusal; an + // idempotency_key already used for different content is FAILED_PRECONDITION + // with reason NOTIFICATION_IDEMPOTENCY_CONFLICT. NotifyOrgAdmins(context.Context, *connect.Request[v1.ModuleNotifyOrgAdminsRequest]) (*connect.Response[v1.ModuleNotifyOrgAdminsResponse], error) // RequestApproval opens a pending approval whose resume job the module claims. RequestApproval(context.Context, *connect.Request[v1.ModuleRequestApprovalRequest]) (*connect.Response[v1.ModuleRequestApprovalResponse], error) @@ -756,7 +740,11 @@ type ModuleCapabilitiesServiceHandler interface { // into the namespaces the operator bound to it; see the request. DeclareAuditEventTypes(context.Context, *connect.Request[v1.ModuleDeclareAuditEventTypesRequest]) (*connect.Response[v1.ModuleDeclareAuditEventTypesResponse], error) // ListSubjectVisibility projects the tenant's team tree onto one viewer: the - // whole set of other subjects whose rows that viewer may read. + // whole set of other subjects whose rows that viewer may read. A viewer who + // is not a member of the tenant is FAILED_PRECONDITION with reason + // TENANT_MEMBERSHIP_MISSING; a set too large to serve whole is + // FAILED_PRECONDITION with reason SUBJECT_VISIBILITY_SET_TOO_LARGE, and only + // the reason tells a consumer which it got. ListSubjectVisibility(context.Context, *connect.Request[v1.ModuleListSubjectVisibilityRequest]) (*connect.Response[v1.ModuleListSubjectVisibilityResponse], error) // FetchDatasourceBlob streams one datasource file blob to the module that // resolves a change set's blob sha. Authorized by the caller principal's @@ -774,24 +762,15 @@ type ModuleCapabilitiesServiceHandler interface { // metadata reset_at) and a RetryInfo; a batch past its limits is // FAILED_PRECONDITION carrying an ErrorInfo naming the limit. FetchDatasourceFiles(context.Context, *connect.Request[v1.FetchDatasourceFilesRequest], *connect.ServerStream[v1.FetchDatasourceFilesFrame]) error - // MintModuleRegistration issues the signed, prefix-bound credential a composed - // module presents to the gateway to federate its REST surface. Authorized by - // the module's own registration secret, not the shared cluster token. - MintModuleRegistration(context.Context, *connect.Request[v1.ModuleMintRegistrationRequest]) (*connect.Response[v1.ModuleMintRegistrationResponse], error) - // MintSolutionRegistration issues the signed, solution-bound credential a - // solution presents to the gateway and to the frontend to register, update, or - // delete its upstream and its Module-Federation remote. Authorized by the - // solution's own registration secret, declared separately from the module - // secrets because a solution remote executes in the host origin. - MintSolutionRegistration(context.Context, *connect.Request[v1.SolutionMintRegistrationRequest]) (*connect.Response[v1.SolutionMintRegistrationResponse], error) // MintModuleWorkContext issues the Work Context a composed module presents to // this surface: owner and sole actor are the module service principal derived - // from its registration prefix. Authorized by the module's own registration - // secret, like the credential exchange above. + // from its prefix. Authorized by the pod's projected ServiceAccount token, held + // against the workload the principal declares and the build the authority + // document approves; a pod running anything else is issued nothing. MintModuleWorkContext(context.Context, *connect.Request[v1.ModuleMintWorkContextRequest]) (*connect.Response[v1.ModuleMintWorkContextResponse], error) // MintModuleOperationContext issues, with no person present, a Work Context // addressed to one of the calling module's installed operation audiences. - // Authorized by the module's identity secret like MintModuleWorkContext; owner + // Authorized by the pod's projected token like MintModuleWorkContext; owner // and sole actor are the module service principal, the tenant is the one its // principal declares, and the scopes are exactly the binding's declared // headless scopes. A binding declaring none is refused. @@ -800,7 +779,7 @@ type ModuleCapabilitiesServiceHandler interface { // sync runs with: owned by the person who connected the source, actored by // the calling module's service principal, in the source's organization, with // exactly the source_delegation_scopes of the binding the person delegated - // to. Authorized by the module's identity secret like MintModuleWorkContext; + // to. Authorized by the pod's projected token like MintModuleWorkContext; // every mint re-checks that the delegation is active, the source exists, the // person is still an owner or admin of the organization, and the binding is // unchanged. FAILED_PRECONDITION with reason DELEGATION_MISSING means the @@ -970,18 +949,6 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("FetchDatasourceFiles")), connect.WithHandlerOptions(opts...), ) - moduleCapabilitiesServiceMintModuleRegistrationHandler := connect.NewUnaryHandler( - ModuleCapabilitiesServiceMintModuleRegistrationProcedure, - svc.MintModuleRegistration, - connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("MintModuleRegistration")), - connect.WithHandlerOptions(opts...), - ) - moduleCapabilitiesServiceMintSolutionRegistrationHandler := connect.NewUnaryHandler( - ModuleCapabilitiesServiceMintSolutionRegistrationProcedure, - svc.MintSolutionRegistration, - connect.WithSchema(moduleCapabilitiesServiceMethods.ByName("MintSolutionRegistration")), - connect.WithHandlerOptions(opts...), - ) moduleCapabilitiesServiceMintModuleWorkContextHandler := connect.NewUnaryHandler( ModuleCapabilitiesServiceMintModuleWorkContextProcedure, svc.MintModuleWorkContext, @@ -1080,10 +1047,6 @@ func NewModuleCapabilitiesServiceHandler(svc ModuleCapabilitiesServiceHandler, o moduleCapabilitiesServiceFetchDatasourceBlobHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceFetchDatasourceFilesProcedure: moduleCapabilitiesServiceFetchDatasourceFilesHandler.ServeHTTP(w, r) - case ModuleCapabilitiesServiceMintModuleRegistrationProcedure: - moduleCapabilitiesServiceMintModuleRegistrationHandler.ServeHTTP(w, r) - case ModuleCapabilitiesServiceMintSolutionRegistrationProcedure: - moduleCapabilitiesServiceMintSolutionRegistrationHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceMintModuleWorkContextProcedure: moduleCapabilitiesServiceMintModuleWorkContextHandler.ServeHTTP(w, r) case ModuleCapabilitiesServiceMintModuleOperationContextProcedure: @@ -1205,14 +1168,6 @@ func (UnimplementedModuleCapabilitiesServiceHandler) FetchDatasourceFiles(contex return connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles is not implemented")) } -func (UnimplementedModuleCapabilitiesServiceHandler) MintModuleRegistration(context.Context, *connect.Request[v1.ModuleMintRegistrationRequest]) (*connect.Response[v1.ModuleMintRegistrationResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.MintModuleRegistration is not implemented")) -} - -func (UnimplementedModuleCapabilitiesServiceHandler) MintSolutionRegistration(context.Context, *connect.Request[v1.SolutionMintRegistrationRequest]) (*connect.Response[v1.SolutionMintRegistrationResponse], error) { - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.MintSolutionRegistration is not implemented")) -} - func (UnimplementedModuleCapabilitiesServiceHandler) MintModuleWorkContext(context.Context, *connect.Request[v1.ModuleMintWorkContextRequest]) (*connect.Response[v1.ModuleMintWorkContextResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext is not implemented")) } diff --git a/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go b/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go index d5b42b9..d8d88d5 100644 --- a/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go +++ b/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go @@ -105,6 +105,12 @@ const ( // PlatformAdminServiceListPlatformCatalogueProcedure is the fully-qualified name of the // PlatformAdminService's ListPlatformCatalogue RPC. PlatformAdminServiceListPlatformCatalogueProcedure = "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue" + // PlatformAdminServiceGetModuleEnvelopeProcedure is the fully-qualified name of the + // PlatformAdminService's GetModuleEnvelope RPC. + PlatformAdminServiceGetModuleEnvelopeProcedure = "/saas.accounts.v1.PlatformAdminService/GetModuleEnvelope" + // PlatformAdminServicePutModuleEnvelopeProcedure is the fully-qualified name of the + // PlatformAdminService's PutModuleEnvelope RPC. + PlatformAdminServicePutModuleEnvelopeProcedure = "/saas.accounts.v1.PlatformAdminService/PutModuleEnvelope" ) // PlatformAdminServiceClient is a client for the saas.accounts.v1.PlatformAdminService service. @@ -160,6 +166,13 @@ type PlatformAdminServiceClient interface { // ListPlatformCatalogue lists every composed module and solution with what // it declares, what runs, and which organizations have it installed. ListPlatformCatalogue(context.Context, *connect.Request[v1.ListPlatformCatalogueRequest]) (*connect.Response[v1.ListPlatformCatalogueResponse], error) + // GetModuleEnvelope reads the current module envelope: the authority ceiling + // every module principal on this host is derived inside. + GetModuleEnvelope(context.Context, *connect.Request[v1.GetModuleEnvelopeRequest]) (*connect.Response[v1.GetModuleEnvelopeResponse], error) + // PutModuleEnvelope writes the next revision of the module envelope. It takes + // effect at the next check with no restart: the module registry is derived + // again from it, so a narrowing removes what the new ceiling no longer allows. + PutModuleEnvelope(context.Context, *connect.Request[v1.PutModuleEnvelopeRequest]) (*connect.Response[v1.PutModuleEnvelopeResponse], error) } // NewPlatformAdminServiceClient constructs a client for the saas.accounts.v1.PlatformAdminService @@ -311,6 +324,18 @@ func NewPlatformAdminServiceClient(httpClient connect.HTTPClient, baseURL string connect.WithSchema(platformAdminServiceMethods.ByName("ListPlatformCatalogue")), connect.WithClientOptions(opts...), ), + getModuleEnvelope: connect.NewClient[v1.GetModuleEnvelopeRequest, v1.GetModuleEnvelopeResponse]( + httpClient, + baseURL+PlatformAdminServiceGetModuleEnvelopeProcedure, + connect.WithSchema(platformAdminServiceMethods.ByName("GetModuleEnvelope")), + connect.WithClientOptions(opts...), + ), + putModuleEnvelope: connect.NewClient[v1.PutModuleEnvelopeRequest, v1.PutModuleEnvelopeResponse]( + httpClient, + baseURL+PlatformAdminServicePutModuleEnvelopeProcedure, + connect.WithSchema(platformAdminServiceMethods.ByName("PutModuleEnvelope")), + connect.WithClientOptions(opts...), + ), } } @@ -339,6 +364,8 @@ type platformAdminServiceClient struct { getEventOperations *connect.Client[v12.GetEventOperationsRequest, v12.GetEventOperationsResponse] listEventSubscriptions *connect.Client[v12.ListEventSubscriptionsRequest, v12.ListEventSubscriptionsResponse] listPlatformCatalogue *connect.Client[v1.ListPlatformCatalogueRequest, v1.ListPlatformCatalogueResponse] + getModuleEnvelope *connect.Client[v1.GetModuleEnvelopeRequest, v1.GetModuleEnvelopeResponse] + putModuleEnvelope *connect.Client[v1.PutModuleEnvelopeRequest, v1.PutModuleEnvelopeResponse] } // SearchUsers calls saas.accounts.v1.PlatformAdminService.SearchUsers. @@ -458,6 +485,16 @@ func (c *platformAdminServiceClient) ListPlatformCatalogue(ctx context.Context, return c.listPlatformCatalogue.CallUnary(ctx, req) } +// GetModuleEnvelope calls saas.accounts.v1.PlatformAdminService.GetModuleEnvelope. +func (c *platformAdminServiceClient) GetModuleEnvelope(ctx context.Context, req *connect.Request[v1.GetModuleEnvelopeRequest]) (*connect.Response[v1.GetModuleEnvelopeResponse], error) { + return c.getModuleEnvelope.CallUnary(ctx, req) +} + +// PutModuleEnvelope calls saas.accounts.v1.PlatformAdminService.PutModuleEnvelope. +func (c *platformAdminServiceClient) PutModuleEnvelope(ctx context.Context, req *connect.Request[v1.PutModuleEnvelopeRequest]) (*connect.Response[v1.PutModuleEnvelopeResponse], error) { + return c.putModuleEnvelope.CallUnary(ctx, req) +} + // PlatformAdminServiceHandler is an implementation of the saas.accounts.v1.PlatformAdminService // service. type PlatformAdminServiceHandler interface { @@ -512,6 +549,13 @@ type PlatformAdminServiceHandler interface { // ListPlatformCatalogue lists every composed module and solution with what // it declares, what runs, and which organizations have it installed. ListPlatformCatalogue(context.Context, *connect.Request[v1.ListPlatformCatalogueRequest]) (*connect.Response[v1.ListPlatformCatalogueResponse], error) + // GetModuleEnvelope reads the current module envelope: the authority ceiling + // every module principal on this host is derived inside. + GetModuleEnvelope(context.Context, *connect.Request[v1.GetModuleEnvelopeRequest]) (*connect.Response[v1.GetModuleEnvelopeResponse], error) + // PutModuleEnvelope writes the next revision of the module envelope. It takes + // effect at the next check with no restart: the module registry is derived + // again from it, so a narrowing removes what the new ceiling no longer allows. + PutModuleEnvelope(context.Context, *connect.Request[v1.PutModuleEnvelopeRequest]) (*connect.Response[v1.PutModuleEnvelopeResponse], error) } // NewPlatformAdminServiceHandler builds an HTTP handler from the service implementation. It returns @@ -659,6 +703,18 @@ func NewPlatformAdminServiceHandler(svc PlatformAdminServiceHandler, opts ...con connect.WithSchema(platformAdminServiceMethods.ByName("ListPlatformCatalogue")), connect.WithHandlerOptions(opts...), ) + platformAdminServiceGetModuleEnvelopeHandler := connect.NewUnaryHandler( + PlatformAdminServiceGetModuleEnvelopeProcedure, + svc.GetModuleEnvelope, + connect.WithSchema(platformAdminServiceMethods.ByName("GetModuleEnvelope")), + connect.WithHandlerOptions(opts...), + ) + platformAdminServicePutModuleEnvelopeHandler := connect.NewUnaryHandler( + PlatformAdminServicePutModuleEnvelopeProcedure, + svc.PutModuleEnvelope, + connect.WithSchema(platformAdminServiceMethods.ByName("PutModuleEnvelope")), + connect.WithHandlerOptions(opts...), + ) return "/saas.accounts.v1.PlatformAdminService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case PlatformAdminServiceSearchUsersProcedure: @@ -707,6 +763,10 @@ func NewPlatformAdminServiceHandler(svc PlatformAdminServiceHandler, opts ...con platformAdminServiceListEventSubscriptionsHandler.ServeHTTP(w, r) case PlatformAdminServiceListPlatformCatalogueProcedure: platformAdminServiceListPlatformCatalogueHandler.ServeHTTP(w, r) + case PlatformAdminServiceGetModuleEnvelopeProcedure: + platformAdminServiceGetModuleEnvelopeHandler.ServeHTTP(w, r) + case PlatformAdminServicePutModuleEnvelopeProcedure: + platformAdminServicePutModuleEnvelopeHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -807,3 +867,11 @@ func (UnimplementedPlatformAdminServiceHandler) ListEventSubscriptions(context.C func (UnimplementedPlatformAdminServiceHandler) ListPlatformCatalogue(context.Context, *connect.Request[v1.ListPlatformCatalogueRequest]) (*connect.Response[v1.ListPlatformCatalogueResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue is not implemented")) } + +func (UnimplementedPlatformAdminServiceHandler) GetModuleEnvelope(context.Context, *connect.Request[v1.GetModuleEnvelopeRequest]) (*connect.Response[v1.GetModuleEnvelopeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.PlatformAdminService.GetModuleEnvelope is not implemented")) +} + +func (UnimplementedPlatformAdminServiceHandler) PutModuleEnvelope(context.Context, *connect.Request[v1.PutModuleEnvelopeRequest]) (*connect.Response[v1.PutModuleEnvelopeResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.PlatformAdminService.PutModuleEnvelope is not implemented")) +} diff --git a/gen/saas/accounts/v1/installations.pb.go b/gen/saas/accounts/v1/installations.pb.go index d68dad8..8f04e14 100644 --- a/gen/saas/accounts/v1/installations.pb.go +++ b/gen/saas/accounts/v1/installations.pb.go @@ -319,7 +319,13 @@ type InstallSolutionRequest struct { // record consent to a presence that already ended, and installing a target // whose newest generation was refused would record consent to a release this // host never admitted. - TargetId string `protobuf:"bytes,12,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + TargetId string `protobuf:"bytes,12,opt,name=target_id,json=targetId,proto3" json:"target_id,omitempty"` + // Installed consent as JSON: capabilities, queues, namespaces, + // external_namespaces, resources, read_audiences, operation_audiences and + // artifact_policies, with the same shapes as the module envelope grant. + // Must be a subset of the current ceiling. Omitted selects that ceiling; + // an explicit {} grants nothing. Tenant and identity are never scope fields. + Scope *string `protobuf:"bytes,13,opt,name=scope,proto3,oneof" json:"scope,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -424,6 +430,76 @@ func (x *InstallSolutionRequest) GetTargetId() string { return "" } +func (x *InstallSolutionRequest) GetScope() string { + if x != nil && x.Scope != nil { + return *x.Scope + } + return "" +} + +// An org administrator may only remove installed authority. The host reads +// the current revision and serializes the narrowing with other lifecycle acts. +type NarrowInstallationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + InstallationId string `protobuf:"bytes,2,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + // Same JSON shape as InstallSolution.scope; {} removes all capability access. + Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NarrowInstallationRequest) Reset() { + *x = NarrowInstallationRequest{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NarrowInstallationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NarrowInstallationRequest) ProtoMessage() {} + +func (x *NarrowInstallationRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NarrowInstallationRequest.ProtoReflect.Descriptor instead. +func (*NarrowInstallationRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} +} + +func (x *NarrowInstallationRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *NarrowInstallationRequest) GetInstallationId() string { + if x != nil { + return x.InstallationId + } + return "" +} + +func (x *NarrowInstallationRequest) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + type UninstallSolutionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -434,7 +510,7 @@ type UninstallSolutionRequest struct { func (x *UninstallSolutionRequest) Reset() { *x = UninstallSolutionRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -446,7 +522,7 @@ func (x *UninstallSolutionRequest) String() string { func (*UninstallSolutionRequest) ProtoMessage() {} func (x *UninstallSolutionRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[2] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -459,7 +535,7 @@ func (x *UninstallSolutionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UninstallSolutionRequest.ProtoReflect.Descriptor instead. func (*UninstallSolutionRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{2} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{3} } func (x *UninstallSolutionRequest) GetOrgId() string { @@ -490,7 +566,7 @@ type TransferInstallationOwnershipRequest struct { func (x *TransferInstallationOwnershipRequest) Reset() { *x = TransferInstallationOwnershipRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -502,7 +578,7 @@ func (x *TransferInstallationOwnershipRequest) String() string { func (*TransferInstallationOwnershipRequest) ProtoMessage() {} func (x *TransferInstallationOwnershipRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[3] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -515,7 +591,7 @@ func (x *TransferInstallationOwnershipRequest) ProtoReflect() protoreflect.Messa // Deprecated: Use TransferInstallationOwnershipRequest.ProtoReflect.Descriptor instead. func (*TransferInstallationOwnershipRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{3} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{4} } func (x *TransferInstallationOwnershipRequest) GetOrgId() string { @@ -556,7 +632,7 @@ type GetInstallationRequest struct { func (x *GetInstallationRequest) Reset() { *x = GetInstallationRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -568,7 +644,7 @@ func (x *GetInstallationRequest) String() string { func (*GetInstallationRequest) ProtoMessage() {} func (x *GetInstallationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[4] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -581,7 +657,7 @@ func (x *GetInstallationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetInstallationRequest.ProtoReflect.Descriptor instead. func (*GetInstallationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{4} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{5} } func (x *GetInstallationRequest) GetOrgId() string { @@ -609,7 +685,7 @@ type GetInstallationResponse struct { func (x *GetInstallationResponse) Reset() { *x = GetInstallationResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -621,7 +697,7 @@ func (x *GetInstallationResponse) String() string { func (*GetInstallationResponse) ProtoMessage() {} func (x *GetInstallationResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[5] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -634,7 +710,7 @@ func (x *GetInstallationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetInstallationResponse.ProtoReflect.Descriptor instead. func (*GetInstallationResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{5} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{6} } func (x *GetInstallationResponse) GetInstallation() *Installation { @@ -673,7 +749,7 @@ type ListInstallationsRequest struct { func (x *ListInstallationsRequest) Reset() { *x = ListInstallationsRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -685,7 +761,7 @@ func (x *ListInstallationsRequest) String() string { func (*ListInstallationsRequest) ProtoMessage() {} func (x *ListInstallationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[6] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -698,7 +774,7 @@ func (x *ListInstallationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstallationsRequest.ProtoReflect.Descriptor instead. func (*ListInstallationsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{6} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{7} } func (x *ListInstallationsRequest) GetOrgId() string { @@ -743,7 +819,7 @@ type InstallationSummary struct { func (x *InstallationSummary) Reset() { *x = InstallationSummary{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -755,7 +831,7 @@ func (x *InstallationSummary) String() string { func (*InstallationSummary) ProtoMessage() {} func (x *InstallationSummary) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[7] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -768,7 +844,7 @@ func (x *InstallationSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use InstallationSummary.ProtoReflect.Descriptor instead. func (*InstallationSummary) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{7} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{8} } func (x *InstallationSummary) GetInstallation() *Installation { @@ -797,7 +873,7 @@ type ListInstallationsResponse struct { func (x *ListInstallationsResponse) Reset() { *x = ListInstallationsResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -809,7 +885,7 @@ func (x *ListInstallationsResponse) String() string { func (*ListInstallationsResponse) ProtoMessage() {} func (x *ListInstallationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[8] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -822,7 +898,7 @@ func (x *ListInstallationsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstallationsResponse.ProtoReflect.Descriptor instead. func (*ListInstallationsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{8} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{9} } func (x *ListInstallationsResponse) GetInstallations() []*InstallationSummary { @@ -839,6 +915,405 @@ func (x *ListInstallationsResponse) GetNextPageToken() string { return "" } +type ListOrganizationInstallationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + // Page bound. 0 -> server default; capped as ListInstallations is. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque cursor from a previous response's next_page_token; empty for page one. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListOrganizationInstallationsRequest) Reset() { + *x = ListOrganizationInstallationsRequest{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListOrganizationInstallationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListOrganizationInstallationsRequest) ProtoMessage() {} + +func (x *ListOrganizationInstallationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListOrganizationInstallationsRequest.ProtoReflect.Descriptor instead. +func (*ListOrganizationInstallationsRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{10} +} + +func (x *ListOrganizationInstallationsRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *ListOrganizationInstallationsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListOrganizationInstallationsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// OrganizationInstallation is one active installation as its organisation's +// administrator manages it. +type OrganizationInstallation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` + // Resolved live, exactly as ListInstallations resolves it. + Health InstallationHealth `protobuf:"varint,2,opt,name=health,proto3,enum=saas.accounts.v1.InstallationHealth" json:"health,omitempty"` + // The catalogue row of the installed target, from the same accepted applied + // state ListAvailableSolutions serves: its route alias, applied release and + // generations. Absent when this host no longer offers the target — the + // installation outlives the presence it was consented to, and an absent row + // says so rather than naming a release the host does not apply. + Target *AvailableSolution `protobuf:"bytes,3,opt,name=target,proto3" json:"target,omitempty"` + // The scope path of the installation's authority-root node: where a grant + // that exposes it is written. Read from the node, never derived from its id. + RootScopePath string `protobuf:"bytes,4,opt,name=root_scope_path,json=rootScopePath,proto3" json:"root_scope_path,omitempty"` + // Teams an active grant reaches at this installation, in the two kinds the + // platform Catalogue keeps apart: granted at its authority root, and inherited + // from a grant above it. Only the first is exposure granted to this + // installation; RevokeInstallationFromTeam removes those. + GrantedTeams []*CollectionReadGrant `protobuf:"bytes,5,rep,name=granted_teams,json=grantedTeams,proto3" json:"granted_teams,omitempty"` + InheritedTeams []*CollectionReadGrant `protobuf:"bytes,6,rep,name=inherited_teams,json=inheritedTeams,proto3" json:"inherited_teams,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OrganizationInstallation) Reset() { + *x = OrganizationInstallation{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OrganizationInstallation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OrganizationInstallation) ProtoMessage() {} + +func (x *OrganizationInstallation) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OrganizationInstallation.ProtoReflect.Descriptor instead. +func (*OrganizationInstallation) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{11} +} + +func (x *OrganizationInstallation) GetInstallation() *Installation { + if x != nil { + return x.Installation + } + return nil +} + +func (x *OrganizationInstallation) GetHealth() InstallationHealth { + if x != nil { + return x.Health + } + return InstallationHealth_INSTALLATION_HEALTH_UNSPECIFIED +} + +func (x *OrganizationInstallation) GetTarget() *AvailableSolution { + if x != nil { + return x.Target + } + return nil +} + +func (x *OrganizationInstallation) GetRootScopePath() string { + if x != nil { + return x.RootScopePath + } + return "" +} + +func (x *OrganizationInstallation) GetGrantedTeams() []*CollectionReadGrant { + if x != nil { + return x.GrantedTeams + } + return nil +} + +func (x *OrganizationInstallation) GetInheritedTeams() []*CollectionReadGrant { + if x != nil { + return x.InheritedTeams + } + return nil +} + +type ListOrganizationInstallationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installations []*OrganizationInstallation `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` + // Set when more installations remain; pass it back as page_token. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListOrganizationInstallationsResponse) Reset() { + *x = ListOrganizationInstallationsResponse{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListOrganizationInstallationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListOrganizationInstallationsResponse) ProtoMessage() {} + +func (x *ListOrganizationInstallationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListOrganizationInstallationsResponse.ProtoReflect.Descriptor instead. +func (*ListOrganizationInstallationsResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{12} +} + +func (x *ListOrganizationInstallationsResponse) GetInstallations() []*OrganizationInstallation { + if x != nil { + return x.Installations + } + return nil +} + +func (x *ListOrganizationInstallationsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +type GrantInstallationToTeamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + InstallationId string `protobuf:"bytes,2,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + TeamId string `protobuf:"bytes,3,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + // The role the team holds at the installation's authority root. It must permit + // using a solution (or carry the wildcard); a role that does not would expose + // nothing, and is refused rather than written. Everything else the role + // permits is granted too, beneath that root. + RoleId string `protobuf:"bytes,4,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GrantInstallationToTeamRequest) Reset() { + *x = GrantInstallationToTeamRequest{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GrantInstallationToTeamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GrantInstallationToTeamRequest) ProtoMessage() {} + +func (x *GrantInstallationToTeamRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GrantInstallationToTeamRequest.ProtoReflect.Descriptor instead. +func (*GrantInstallationToTeamRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{13} +} + +func (x *GrantInstallationToTeamRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *GrantInstallationToTeamRequest) GetInstallationId() string { + if x != nil { + return x.InstallationId + } + return "" +} + +func (x *GrantInstallationToTeamRequest) GetTeamId() string { + if x != nil { + return x.TeamId + } + return "" +} + +func (x *GrantInstallationToTeamRequest) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + +type GrantInstallationToTeamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Grant *ScopeGrant `protobuf:"bytes,1,opt,name=grant,proto3" json:"grant,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GrantInstallationToTeamResponse) Reset() { + *x = GrantInstallationToTeamResponse{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GrantInstallationToTeamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GrantInstallationToTeamResponse) ProtoMessage() {} + +func (x *GrantInstallationToTeamResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GrantInstallationToTeamResponse.ProtoReflect.Descriptor instead. +func (*GrantInstallationToTeamResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{14} +} + +func (x *GrantInstallationToTeamResponse) GetGrant() *ScopeGrant { + if x != nil { + return x.Grant + } + return nil +} + +type RevokeInstallationFromTeamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` + InstallationId string `protobuf:"bytes,2,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + TeamId string `protobuf:"bytes,3,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + // The role of the grant being removed, as granted_teams reports it. A team may + // hold more than one role at the same node, and each is its own grant. + RoleId string `protobuf:"bytes,4,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeInstallationFromTeamRequest) Reset() { + *x = RevokeInstallationFromTeamRequest{} + mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeInstallationFromTeamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeInstallationFromTeamRequest) ProtoMessage() {} + +func (x *RevokeInstallationFromTeamRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_installations_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeInstallationFromTeamRequest.ProtoReflect.Descriptor instead. +func (*RevokeInstallationFromTeamRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{15} +} + +func (x *RevokeInstallationFromTeamRequest) GetOrgId() string { + if x != nil { + return x.OrgId + } + return "" +} + +func (x *RevokeInstallationFromTeamRequest) GetInstallationId() string { + if x != nil { + return x.InstallationId + } + return "" +} + +func (x *RevokeInstallationFromTeamRequest) GetTeamId() string { + if x != nil { + return x.TeamId + } + return "" +} + +func (x *RevokeInstallationFromTeamRequest) GetRoleId() string { + if x != nil { + return x.RoleId + } + return "" +} + // AvailableSolution is one installable solution target: its immutable identity, // the release the host currently has applied for it, and where it is routed. type AvailableSolution struct { @@ -871,7 +1346,7 @@ type AvailableSolution struct { func (x *AvailableSolution) Reset() { *x = AvailableSolution{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -883,7 +1358,7 @@ func (x *AvailableSolution) String() string { func (*AvailableSolution) ProtoMessage() {} func (x *AvailableSolution) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[9] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -896,7 +1371,7 @@ func (x *AvailableSolution) ProtoReflect() protoreflect.Message { // Deprecated: Use AvailableSolution.ProtoReflect.Descriptor instead. func (*AvailableSolution) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{9} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{16} } func (x *AvailableSolution) GetTargetId() string { @@ -975,7 +1450,7 @@ type ListAvailableSolutionsRequest struct { func (x *ListAvailableSolutionsRequest) Reset() { *x = ListAvailableSolutionsRequest{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -987,7 +1462,7 @@ func (x *ListAvailableSolutionsRequest) String() string { func (*ListAvailableSolutionsRequest) ProtoMessage() {} func (x *ListAvailableSolutionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[10] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1000,7 +1475,7 @@ func (x *ListAvailableSolutionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAvailableSolutionsRequest.ProtoReflect.Descriptor instead. func (*ListAvailableSolutionsRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{10} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{17} } func (x *ListAvailableSolutionsRequest) GetOrgId() string { @@ -1034,7 +1509,7 @@ type ListAvailableSolutionsResponse struct { func (x *ListAvailableSolutionsResponse) Reset() { *x = ListAvailableSolutionsResponse{} - mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1046,7 +1521,7 @@ func (x *ListAvailableSolutionsResponse) String() string { func (*ListAvailableSolutionsResponse) ProtoMessage() {} func (x *ListAvailableSolutionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_installations_proto_msgTypes[11] + mi := &file_saas_accounts_v1_installations_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1059,7 +1534,7 @@ func (x *ListAvailableSolutionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAvailableSolutionsResponse.ProtoReflect.Descriptor instead. func (*ListAvailableSolutionsResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{11} + return file_saas_accounts_v1_installations_proto_rawDescGZIP(), []int{18} } func (x *ListAvailableSolutionsResponse) GetSolutions() []*AvailableSolution { @@ -1080,7 +1555,7 @@ var File_saas_accounts_v1_installations_proto protoreflect.FileDescriptor const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\n" + - "$saas/accounts/v1/installations.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1csaas/policy/v1/options.proto\"\xe3\x04\n" + + "$saas/accounts/v1/installations.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$saas/accounts/v1/authorization.proto\x1a\x1csaas/policy/v1/options.proto\"\xe3\x04\n" + "\fInstallation\x12\x18\n" + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1f\n" + "\x06org_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x126\n" + @@ -1096,7 +1571,7 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + " \x01(\v2\x1a.google.protobuf.TimestampH\x00R\trevokedAt\x88\x01\x01\x12(\n" + "\ttarget_id\x18\v \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\btargetId\x12/\n" + "\x0erevoked_reason\x18\f \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\rrevokedReasonB\r\n" + - "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xc4\x04\n" + + "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xf6\x04\n" + "\x16InstallSolutionRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x125\n" + "\x10agent_identifier\x18\x02 \x01(\tB\n" + @@ -1109,7 +1584,13 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x11allowed_audiences\x18\n" + " \x03(\tB\x0f\xbaH\f\x92\x01\t\x10@\"\x05r\x03\x18\x80\x04R\x10allowedAudiences\x126\n" + "\x0eallowed_scopes\x18\v \x03(\tB\x0f\xbaH\f\x92\x01\t\x10@\"\x05r\x03\x18\x80\x01R\rallowedScopes\x12%\n" + - "\ttarget_id\x18\f \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetIdJ\x04\b\x03\x10\x04J\x04\b\x05\x10\x06R\x13solution_identifierR\x0froot_scope_path\"n\n" + + "\ttarget_id\x18\f \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetId\x12&\n" + + "\x05scope\x18\r \x01(\tB\v\xbaH\br\x06\x10\x02(\x80\x80@H\x00R\x05scope\x88\x01\x01B\b\n" + + "\x06_scopeJ\x04\b\x03\x10\x04J\x04\b\x05\x10\x06R\x13solution_identifierR\x0froot_scope_path\"\x92\x01\n" + + "\x19NarrowInstallationRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x121\n" + + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\x12!\n" + + "\x05scope\x18\x03 \x01(\tB\v\xbaH\br\x06\x10\x02(\x80\x80@R\x05scope\"n\n" + "\x18UninstallSolutionRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x121\n" + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\"\xff\x01\n" + @@ -1136,7 +1617,35 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\"\x90\x01\n" + "\x19ListInstallationsResponse\x12K\n" + "\rinstallations\x18\x01 \x03(\v2%.saas.accounts.v1.InstallationSummaryR\rinstallations\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xed\x02\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x8f\x01\n" + + "$ListOrganizationInstallationsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12'\n" + + "\tpage_size\x18\x02 \x01(\x05B\n" + + "\xbaH\a\x1a\x05\x18\xf4\x03(\x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\"\x9d\x03\n" + + "\x18OrganizationInstallation\x12B\n" + + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12<\n" + + "\x06health\x18\x02 \x01(\x0e2$.saas.accounts.v1.InstallationHealthR\x06health\x12;\n" + + "\x06target\x18\x03 \x01(\v2#.saas.accounts.v1.AvailableSolutionR\x06target\x12&\n" + + "\x0froot_scope_path\x18\x04 \x01(\tR\rrootScopePath\x12J\n" + + "\rgranted_teams\x18\x05 \x03(\v2%.saas.accounts.v1.CollectionReadGrantR\fgrantedTeams\x12N\n" + + "\x0finherited_teams\x18\x06 \x03(\v2%.saas.accounts.v1.CollectionReadGrantR\x0einheritedTeams\"\xa1\x01\n" + + "%ListOrganizationInstallationsResponse\x12P\n" + + "\rinstallations\x18\x01 \x03(\v2*.saas.accounts.v1.OrganizationInstallationR\rinstallations\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xba\x01\n" + + "\x1eGrantInstallationToTeamRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x121\n" + + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\x12!\n" + + "\ateam_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06teamId\x12!\n" + + "\arole_id\x18\x04 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06roleId\"U\n" + + "\x1fGrantInstallationToTeamResponse\x122\n" + + "\x05grant\x18\x01 \x01(\v2\x1c.saas.accounts.v1.ScopeGrantR\x05grant\"\xbd\x01\n" + + "!RevokeInstallationFromTeamRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x121\n" + + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\x12!\n" + + "\ateam_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06teamId\x12!\n" + + "\arole_id\x18\x04 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06roleId\"\xed\x02\n" + "\x11AvailableSolution\x12%\n" + "\ttarget_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetId\x12\x1d\n" + "\n" + @@ -1168,14 +1677,17 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "%INSTALLATION_HEALTH_NO_ELIGIBLE_OWNER\x10\x02\x12%\n" + "!INSTALLATION_HEALTH_AGENT_REVOKED\x10\x03\x12&\n" + "\"INSTALLATION_HEALTH_AGENT_DISABLED\x10\x04\x12.\n" + - "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x052\xc5\t\n" + + "*INSTALLATION_HEALTH_STANDING_GRANT_MISSING\x10\x052\xd1\x10\n" + "\x13InstallationService\x12\xba\x01\n" + "\x0fInstallSolution\x12(.saas.accounts.v1.InstallSolutionRequest\x1a\x1e.saas.accounts.v1.Installation\"]\xc2\xf3\x18=\b\x02\x10\x04*\f\n" + "\x06org_id\x10\x02\x18\x010\x01:\x1d\n" + "\x19saas.installation.created\x10\x02@\x01H\x04P\x03X\x03`\x01\x82\xd3\xe4\x93\x02\x16:\x01*\"\x11/v1/installations\x12\xd2\x01\n" + "\x11UninstallSolution\x12*.saas.accounts.v1.UninstallSolutionRequest\x1a\x16.google.protobuf.Empty\"y\xc2\xf3\x18=\b\x02\x10\x04*\f\n" + "\x06org_id\x10\x02\x18\x010\x01:\x1d\n" + - "\x19saas.installation.revoked\x10\x02@\x01H\x04P\x03X\x03`\x01\x82\xd3\xe4\x93\x022:\x01*\"-/v1/installations/{installation_id}:uninstall\x12\x89\x02\n" + + "\x19saas.installation.revoked\x10\x02@\x01H\x04P\x03X\x03`\x01\x82\xd3\xe4\x93\x022:\x01*\"-/v1/installations/{installation_id}:uninstall\x12\xd2\x01\n" + + "\x12NarrowInstallation\x12+.saas.accounts.v1.NarrowInstallationRequest\x1a\x16.google.protobuf.Empty\"w\xc2\xf3\x18>\b\x02\x10\x04*\f\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x1e\n" + + "\x1asaas.installation.narrowed\x10\x02@\x01H\x04P\x03X\x03`\x01\x82\xd3\xe4\x93\x02/:\x01*\"*/v1/installations/{installation_id}:narrow\x12\x89\x02\n" + "\x1dTransferInstallationOwnership\x126.saas.accounts.v1.TransferInstallationOwnershipRequest\x1a\x1e.saas.accounts.v1.Installation\"\x8f\x01\xc2\xf3\x18K\b\x02\x10\x04*\f\n" + "\x06org_id\x10\x02\x18\x010\x01:+\n" + "'saas.installation.ownership_transferred\x10\x02@\x01H\x04P\x03X\x03`\x01\x82\xd3\xe4\x93\x02::\x01*\"5/v1/installations/{installation_id}:transferOwnership\x12\xb9\x01\n" + @@ -1183,7 +1695,15 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02%\x12#/v1/installations/{installation_id}\x12\x86\x01\n" + "\x11ListInstallations\x12*.saas.accounts.v1.ListInstallationsRequest\x1a+.saas.accounts.v1.ListInstallationsResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\xca\x01\n" + "\x16ListAvailableSolutions\x12/.saas.accounts.v1.ListAvailableSolutionsRequest\x1a0.saas.accounts.v1.ListAvailableSolutionsResponse\"M\xc2\xf3\x18\"\b\x02\x10\x04*\f\n" + - "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02!\x12\x1f/v1/installations:listAvailableB\xd0\x01\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02!\x12\x1f/v1/installations:listAvailable\x12\xd1\x01\n" + + "\x1dListOrganizationInstallations\x126.saas.accounts.v1.ListOrganizationInstallationsRequest\x1a7.saas.accounts.v1.ListOrganizationInstallationsResponse\"?\xc2\xf3\x18\"\b\x02\x10\x04*\f\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x02\x13\x12\x11/v1/installations\x12\xf0\x01\n" + + "\x17GrantInstallationToTeam\x120.saas.accounts.v1.GrantInstallationToTeamRequest\x1a1.saas.accounts.v1.GrantInstallationToTeamResponse\"p\xc2\xf3\x188\b\x02\x10\x04*\f\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x16\n" + + "\x12saas.scope.granted\x10\x02@\x01H\x04P\x03X\x03`\x01x\x02\x82\xd3\xe4\x93\x02.:\x01*\")/v1/installations/{installation_id}/teams\x12\xed\x01\n" + + "\x1aRevokeInstallationFromTeam\x123.saas.accounts.v1.RevokeInstallationFromTeamRequest\x1a\x16.google.protobuf.Empty\"\x81\x01\xc2\xf3\x188\b\x02\x10\x04*\f\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x16\n" + + "\x12saas.scope.revoked\x10\x02@\x01H\x04P\x03X\x03`\x01x\x02\x82\xd3\xe4\x93\x02?:\x01*\":/v1/installations/{installation_id}/teams/{team_id}:revokeB\xd0\x01\n" + "\x14com.saas.accounts.v1B\x12InstallationsProtoP\x01ZBgithub.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" var ( @@ -1199,53 +1719,77 @@ func file_saas_accounts_v1_installations_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_installations_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_saas_accounts_v1_installations_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_saas_accounts_v1_installations_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_saas_accounts_v1_installations_proto_goTypes = []any{ - (InstallationStatus)(0), // 0: saas.accounts.v1.InstallationStatus - (InstallationHealth)(0), // 1: saas.accounts.v1.InstallationHealth - (*Installation)(nil), // 2: saas.accounts.v1.Installation - (*InstallSolutionRequest)(nil), // 3: saas.accounts.v1.InstallSolutionRequest - (*UninstallSolutionRequest)(nil), // 4: saas.accounts.v1.UninstallSolutionRequest - (*TransferInstallationOwnershipRequest)(nil), // 5: saas.accounts.v1.TransferInstallationOwnershipRequest - (*GetInstallationRequest)(nil), // 6: saas.accounts.v1.GetInstallationRequest - (*GetInstallationResponse)(nil), // 7: saas.accounts.v1.GetInstallationResponse - (*ListInstallationsRequest)(nil), // 8: saas.accounts.v1.ListInstallationsRequest - (*InstallationSummary)(nil), // 9: saas.accounts.v1.InstallationSummary - (*ListInstallationsResponse)(nil), // 10: saas.accounts.v1.ListInstallationsResponse - (*AvailableSolution)(nil), // 11: saas.accounts.v1.AvailableSolution - (*ListAvailableSolutionsRequest)(nil), // 12: saas.accounts.v1.ListAvailableSolutionsRequest - (*ListAvailableSolutionsResponse)(nil), // 13: saas.accounts.v1.ListAvailableSolutionsResponse - (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 15: google.protobuf.Empty + (InstallationStatus)(0), // 0: saas.accounts.v1.InstallationStatus + (InstallationHealth)(0), // 1: saas.accounts.v1.InstallationHealth + (*Installation)(nil), // 2: saas.accounts.v1.Installation + (*InstallSolutionRequest)(nil), // 3: saas.accounts.v1.InstallSolutionRequest + (*NarrowInstallationRequest)(nil), // 4: saas.accounts.v1.NarrowInstallationRequest + (*UninstallSolutionRequest)(nil), // 5: saas.accounts.v1.UninstallSolutionRequest + (*TransferInstallationOwnershipRequest)(nil), // 6: saas.accounts.v1.TransferInstallationOwnershipRequest + (*GetInstallationRequest)(nil), // 7: saas.accounts.v1.GetInstallationRequest + (*GetInstallationResponse)(nil), // 8: saas.accounts.v1.GetInstallationResponse + (*ListInstallationsRequest)(nil), // 9: saas.accounts.v1.ListInstallationsRequest + (*InstallationSummary)(nil), // 10: saas.accounts.v1.InstallationSummary + (*ListInstallationsResponse)(nil), // 11: saas.accounts.v1.ListInstallationsResponse + (*ListOrganizationInstallationsRequest)(nil), // 12: saas.accounts.v1.ListOrganizationInstallationsRequest + (*OrganizationInstallation)(nil), // 13: saas.accounts.v1.OrganizationInstallation + (*ListOrganizationInstallationsResponse)(nil), // 14: saas.accounts.v1.ListOrganizationInstallationsResponse + (*GrantInstallationToTeamRequest)(nil), // 15: saas.accounts.v1.GrantInstallationToTeamRequest + (*GrantInstallationToTeamResponse)(nil), // 16: saas.accounts.v1.GrantInstallationToTeamResponse + (*RevokeInstallationFromTeamRequest)(nil), // 17: saas.accounts.v1.RevokeInstallationFromTeamRequest + (*AvailableSolution)(nil), // 18: saas.accounts.v1.AvailableSolution + (*ListAvailableSolutionsRequest)(nil), // 19: saas.accounts.v1.ListAvailableSolutionsRequest + (*ListAvailableSolutionsResponse)(nil), // 20: saas.accounts.v1.ListAvailableSolutionsResponse + (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp + (*CollectionReadGrant)(nil), // 22: saas.accounts.v1.CollectionReadGrant + (*ScopeGrant)(nil), // 23: saas.accounts.v1.ScopeGrant + (*emptypb.Empty)(nil), // 24: google.protobuf.Empty } var file_saas_accounts_v1_installations_proto_depIdxs = []int32{ 0, // 0: saas.accounts.v1.Installation.status:type_name -> saas.accounts.v1.InstallationStatus - 14, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp - 14, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp + 21, // 1: saas.accounts.v1.Installation.created_at:type_name -> google.protobuf.Timestamp + 21, // 2: saas.accounts.v1.Installation.revoked_at:type_name -> google.protobuf.Timestamp 2, // 3: saas.accounts.v1.GetInstallationResponse.installation:type_name -> saas.accounts.v1.Installation 1, // 4: saas.accounts.v1.GetInstallationResponse.health:type_name -> saas.accounts.v1.InstallationHealth 0, // 5: saas.accounts.v1.ListInstallationsRequest.status:type_name -> saas.accounts.v1.InstallationStatus 2, // 6: saas.accounts.v1.InstallationSummary.installation:type_name -> saas.accounts.v1.Installation 1, // 7: saas.accounts.v1.InstallationSummary.health:type_name -> saas.accounts.v1.InstallationHealth - 9, // 8: saas.accounts.v1.ListInstallationsResponse.installations:type_name -> saas.accounts.v1.InstallationSummary - 11, // 9: saas.accounts.v1.ListAvailableSolutionsResponse.solutions:type_name -> saas.accounts.v1.AvailableSolution - 3, // 10: saas.accounts.v1.InstallationService.InstallSolution:input_type -> saas.accounts.v1.InstallSolutionRequest - 4, // 11: saas.accounts.v1.InstallationService.UninstallSolution:input_type -> saas.accounts.v1.UninstallSolutionRequest - 5, // 12: saas.accounts.v1.InstallationService.TransferInstallationOwnership:input_type -> saas.accounts.v1.TransferInstallationOwnershipRequest - 6, // 13: saas.accounts.v1.InstallationService.GetInstallation:input_type -> saas.accounts.v1.GetInstallationRequest - 8, // 14: saas.accounts.v1.InstallationService.ListInstallations:input_type -> saas.accounts.v1.ListInstallationsRequest - 12, // 15: saas.accounts.v1.InstallationService.ListAvailableSolutions:input_type -> saas.accounts.v1.ListAvailableSolutionsRequest - 2, // 16: saas.accounts.v1.InstallationService.InstallSolution:output_type -> saas.accounts.v1.Installation - 15, // 17: saas.accounts.v1.InstallationService.UninstallSolution:output_type -> google.protobuf.Empty - 2, // 18: saas.accounts.v1.InstallationService.TransferInstallationOwnership:output_type -> saas.accounts.v1.Installation - 7, // 19: saas.accounts.v1.InstallationService.GetInstallation:output_type -> saas.accounts.v1.GetInstallationResponse - 10, // 20: saas.accounts.v1.InstallationService.ListInstallations:output_type -> saas.accounts.v1.ListInstallationsResponse - 13, // 21: saas.accounts.v1.InstallationService.ListAvailableSolutions:output_type -> saas.accounts.v1.ListAvailableSolutionsResponse - 16, // [16:22] is the sub-list for method output_type - 10, // [10:16] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 10, // 8: saas.accounts.v1.ListInstallationsResponse.installations:type_name -> saas.accounts.v1.InstallationSummary + 2, // 9: saas.accounts.v1.OrganizationInstallation.installation:type_name -> saas.accounts.v1.Installation + 1, // 10: saas.accounts.v1.OrganizationInstallation.health:type_name -> saas.accounts.v1.InstallationHealth + 18, // 11: saas.accounts.v1.OrganizationInstallation.target:type_name -> saas.accounts.v1.AvailableSolution + 22, // 12: saas.accounts.v1.OrganizationInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 22, // 13: saas.accounts.v1.OrganizationInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 13, // 14: saas.accounts.v1.ListOrganizationInstallationsResponse.installations:type_name -> saas.accounts.v1.OrganizationInstallation + 23, // 15: saas.accounts.v1.GrantInstallationToTeamResponse.grant:type_name -> saas.accounts.v1.ScopeGrant + 18, // 16: saas.accounts.v1.ListAvailableSolutionsResponse.solutions:type_name -> saas.accounts.v1.AvailableSolution + 3, // 17: saas.accounts.v1.InstallationService.InstallSolution:input_type -> saas.accounts.v1.InstallSolutionRequest + 5, // 18: saas.accounts.v1.InstallationService.UninstallSolution:input_type -> saas.accounts.v1.UninstallSolutionRequest + 4, // 19: saas.accounts.v1.InstallationService.NarrowInstallation:input_type -> saas.accounts.v1.NarrowInstallationRequest + 6, // 20: saas.accounts.v1.InstallationService.TransferInstallationOwnership:input_type -> saas.accounts.v1.TransferInstallationOwnershipRequest + 7, // 21: saas.accounts.v1.InstallationService.GetInstallation:input_type -> saas.accounts.v1.GetInstallationRequest + 9, // 22: saas.accounts.v1.InstallationService.ListInstallations:input_type -> saas.accounts.v1.ListInstallationsRequest + 19, // 23: saas.accounts.v1.InstallationService.ListAvailableSolutions:input_type -> saas.accounts.v1.ListAvailableSolutionsRequest + 12, // 24: saas.accounts.v1.InstallationService.ListOrganizationInstallations:input_type -> saas.accounts.v1.ListOrganizationInstallationsRequest + 15, // 25: saas.accounts.v1.InstallationService.GrantInstallationToTeam:input_type -> saas.accounts.v1.GrantInstallationToTeamRequest + 17, // 26: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:input_type -> saas.accounts.v1.RevokeInstallationFromTeamRequest + 2, // 27: saas.accounts.v1.InstallationService.InstallSolution:output_type -> saas.accounts.v1.Installation + 24, // 28: saas.accounts.v1.InstallationService.UninstallSolution:output_type -> google.protobuf.Empty + 24, // 29: saas.accounts.v1.InstallationService.NarrowInstallation:output_type -> google.protobuf.Empty + 2, // 30: saas.accounts.v1.InstallationService.TransferInstallationOwnership:output_type -> saas.accounts.v1.Installation + 8, // 31: saas.accounts.v1.InstallationService.GetInstallation:output_type -> saas.accounts.v1.GetInstallationResponse + 11, // 32: saas.accounts.v1.InstallationService.ListInstallations:output_type -> saas.accounts.v1.ListInstallationsResponse + 20, // 33: saas.accounts.v1.InstallationService.ListAvailableSolutions:output_type -> saas.accounts.v1.ListAvailableSolutionsResponse + 14, // 34: saas.accounts.v1.InstallationService.ListOrganizationInstallations:output_type -> saas.accounts.v1.ListOrganizationInstallationsResponse + 16, // 35: saas.accounts.v1.InstallationService.GrantInstallationToTeam:output_type -> saas.accounts.v1.GrantInstallationToTeamResponse + 24, // 36: saas.accounts.v1.InstallationService.RevokeInstallationFromTeam:output_type -> google.protobuf.Empty + 27, // [27:37] is the sub-list for method output_type + 17, // [17:27] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_saas_accounts_v1_installations_proto_init() } @@ -1253,14 +1797,16 @@ func file_saas_accounts_v1_installations_proto_init() { if File_saas_accounts_v1_installations_proto != nil { return } + file_saas_accounts_v1_authorization_proto_init() file_saas_accounts_v1_installations_proto_msgTypes[0].OneofWrappers = []any{} + file_saas_accounts_v1_installations_proto_msgTypes[1].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_installations_proto_rawDesc), len(file_saas_accounts_v1_installations_proto_rawDesc)), NumEnums: 2, - NumMessages: 12, + NumMessages: 19, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/saas/accounts/v1/module_capabilities.pb.go b/gen/saas/accounts/v1/module_capabilities.pb.go index 4a6eefe..9bc51b1 100644 --- a/gen/saas/accounts/v1/module_capabilities.pb.go +++ b/gen/saas/accounts/v1/module_capabilities.pb.go @@ -115,7 +115,7 @@ const ( ModuleAuditEventVisibility_MODULE_AUDIT_EVENT_VISIBILITY_TENANT ModuleAuditEventVisibility = 1 // May additionally be delivered to a tenant's outbound webhook endpoint. // Admitted only in a namespace the operator marked externally deliverable - // (`external_namespaces` of the declaring principal's MODULE_PRINCIPALS + // (`external_namespaces` of the declaring principal's module registry // entry): the producer decides which of its facts are customer-facing, and // the operator decides whether that producer may send anything out at all. ModuleAuditEventVisibility_MODULE_AUDIT_EVENT_VISIBILITY_EXTERNAL ModuleAuditEventVisibility = 2 @@ -1846,7 +1846,7 @@ type ModuleAuditEventTypeDeclaration struct { state protoimpl.MessageState `protogen:"open.v1"` // .., lowercase snake_case segments. The // namespace must be one the operator bound to this module (the `namespaces` - // of its MODULE_PRINCIPALS entry) and may not be the host's own. + // of its module registry entry) and may not be the host's own. Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` Fields []*ModuleAuditFieldDeclaration `protobuf:"bytes,3,rep,name=fields,proto3" json:"fields,omitempty"` @@ -3342,9 +3342,10 @@ func (x *ModuleReplayEventsResponse) GetRedelivered() int32 { // ModuleCapabilitiesService is the module-facing platform surface. Every RPC is // internal-tier and identifies its caller from the forwarded Work Context — -// except MintModuleRegistration and MintSolutionRegistration, which a registrant -// calls at startup, before any user request exists, and which authorize on the -// registrant's own registration secret. +// except the three mints, which are how a module obtains one: each presents the +// module pod's projected ServiceAccount token, and accounts establishes what the +// pod is running (TokenReview, the pod's running image, the approved build) +// before it issues anything. // Non-secret, current-viewer source read projection. // // One row per feeding source, so a boundary fed by several sources is reported @@ -5020,7 +5021,7 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + "\x1cMETADATA_DISCLOSURE_WITHHELD\x10\x02*\\\n" + "\x0fSourceSyncStage\x12!\n" + "\x1dSOURCE_SYNC_STAGE_UNSPECIFIED\x10\x00\x12&\n" + - "\"SOURCE_SYNC_STAGE_CHANGES_ENQUEUED\x10\x012\xa7)\n" + + "\"SOURCE_SYNC_STAGE_CHANGES_ENQUEUED\x10\x012\xad&\n" + "\x19ModuleCapabilitiesService\x12\x9b\x01\n" + "\x16GetCurrentInstallation\x122.saas.accounts.v1.ModuleCurrentInstallationRequest\x1a3.saas.accounts.v1.ModuleCurrentInstallationResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x04X\x03`\x01\x12\xc6\x01\n" + "\x19ApproveExecutableArtifact\x121.saas.accounts.v1.ModuleExecutableArtifactRequest\x1a2.saas.accounts.v1.ModuleExecutableArtifactResponse\"B\xc2\xf3\x18>\b\x03\x10\x010\x01:,\n" + @@ -5053,11 +5054,7 @@ const file_saas_accounts_v1_module_capabilities_proto_rawDesc = "" + " saas.module.audit_types_declared\x10\x02@\x01H\aP\x03X\x03`\x01\x12\x9e\x01\n" + "\x15ListSubjectVisibility\x124.saas.accounts.v1.ModuleListSubjectVisibilityRequest\x1a5.saas.accounts.v1.ModuleListSubjectVisibilityResponse\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x12\x8e\x01\n" + "\x13FetchDatasourceBlob\x12,.saas.accounts.v1.FetchDatasourceBlobRequest\x1a*.saas.accounts.v1.FetchDatasourceBlobChunk\"\x1b\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x01\x88\x02\x010\x01\x12\x8e\x01\n" + - "\x14FetchDatasourceFiles\x12-.saas.accounts.v1.FetchDatasourceFilesRequest\x1a+.saas.accounts.v1.FetchDatasourceFilesFrame\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x010\x01\x12\xb6\x01\n" + - "\x16MintModuleRegistration\x12/.saas.accounts.v1.ModuleMintRegistrationRequest\x1a0.saas.accounts.v1.ModuleMintRegistrationResponse\"9\xc2\xf3\x185\b\x03\x10\x010\x01:#\n" + - "\x1fsaas.module.registration_minted\x10\x02@\x01H\aP\x04X\x04`\x01\x12\xbe\x01\n" + - "\x18MintSolutionRegistration\x121.saas.accounts.v1.SolutionMintRegistrationRequest\x1a2.saas.accounts.v1.SolutionMintRegistrationResponse\";\xc2\xf3\x187\b\x03\x10\x010\x01:%\n" + - "!saas.solution.registration_minted\x10\x02@\x01H\aP\x04X\x04`\x01\x12\xb3\x01\n" + + "\x14FetchDatasourceFiles\x12-.saas.accounts.v1.FetchDatasourceFilesRequest\x1a+.saas.accounts.v1.FetchDatasourceFilesFrame\"\x18\xc2\xf3\x18\x14\b\x03\x10\x010\x01:\x02\x10\x01@\x01H\aP\x03X\x03`\x010\x01\x12\xb3\x01\n" + "\x15MintModuleWorkContext\x12..saas.accounts.v1.ModuleMintWorkContextRequest\x1a/.saas.accounts.v1.ModuleMintWorkContextResponse\"9\xc2\xf3\x185\b\x03\x10\x010\x01:#\n" + "\x1fsaas.module.work_context_minted\x10\x02@\x01H\aP\x04X\x04`\x01\x12\xc7\x01\n" + "\x1aMintModuleOperationContext\x123.saas.accounts.v1.ModuleMintOperationContextRequest\x1a4.saas.accounts.v1.ModuleMintOperationContextResponse\">\xc2\xf3\x18:\b\x03\x10\x010\x01:(\n" + @@ -5165,18 +5162,14 @@ var file_saas_accounts_v1_module_capabilities_proto_goTypes = []any{ (*timestamppb.Timestamp)(nil), // 76: google.protobuf.Timestamp (*structpb.Struct)(nil), // 77: google.protobuf.Struct (*v11.EventEnvelope)(nil), // 78: saas.events.v1.EventEnvelope - (*ModuleMintRegistrationRequest)(nil), // 79: saas.accounts.v1.ModuleMintRegistrationRequest - (*SolutionMintRegistrationRequest)(nil), // 80: saas.accounts.v1.SolutionMintRegistrationRequest - (*ModuleMintWorkContextRequest)(nil), // 81: saas.accounts.v1.ModuleMintWorkContextRequest - (*ModuleMintOperationContextRequest)(nil), // 82: saas.accounts.v1.ModuleMintOperationContextRequest - (*ModuleMintSourceOperationContextRequest)(nil), // 83: saas.accounts.v1.ModuleMintSourceOperationContextRequest - (*IssuedWorkContext)(nil), // 84: saas.accounts.v1.IssuedWorkContext - (*emptypb.Empty)(nil), // 85: google.protobuf.Empty - (*ModuleMintRegistrationResponse)(nil), // 86: saas.accounts.v1.ModuleMintRegistrationResponse - (*SolutionMintRegistrationResponse)(nil), // 87: saas.accounts.v1.SolutionMintRegistrationResponse - (*ModuleMintWorkContextResponse)(nil), // 88: saas.accounts.v1.ModuleMintWorkContextResponse - (*ModuleMintOperationContextResponse)(nil), // 89: saas.accounts.v1.ModuleMintOperationContextResponse - (*ModuleMintSourceOperationContextResponse)(nil), // 90: saas.accounts.v1.ModuleMintSourceOperationContextResponse + (*ModuleMintWorkContextRequest)(nil), // 79: saas.accounts.v1.ModuleMintWorkContextRequest + (*ModuleMintOperationContextRequest)(nil), // 80: saas.accounts.v1.ModuleMintOperationContextRequest + (*ModuleMintSourceOperationContextRequest)(nil), // 81: saas.accounts.v1.ModuleMintSourceOperationContextRequest + (*IssuedWorkContext)(nil), // 82: saas.accounts.v1.IssuedWorkContext + (*emptypb.Empty)(nil), // 83: google.protobuf.Empty + (*ModuleMintWorkContextResponse)(nil), // 84: saas.accounts.v1.ModuleMintWorkContextResponse + (*ModuleMintOperationContextResponse)(nil), // 85: saas.accounts.v1.ModuleMintOperationContextResponse + (*ModuleMintSourceOperationContextResponse)(nil), // 86: saas.accounts.v1.ModuleMintSourceOperationContextResponse } var file_saas_accounts_v1_module_capabilities_proto_depIdxs = []int32{ 69, // 0: saas.accounts.v1.ModuleEnqueueJobRequest.job:type_name -> saas.jobs.v1.NewJob @@ -5255,52 +5248,48 @@ var file_saas_accounts_v1_module_capabilities_proto_depIdxs = []int32{ 31, // 73: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:input_type -> saas.accounts.v1.ModuleListSubjectVisibilityRequest 33, // 74: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:input_type -> saas.accounts.v1.FetchDatasourceBlobRequest 36, // 75: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:input_type -> saas.accounts.v1.FetchDatasourceFilesRequest - 79, // 76: saas.accounts.v1.ModuleCapabilitiesService.MintModuleRegistration:input_type -> saas.accounts.v1.ModuleMintRegistrationRequest - 80, // 77: saas.accounts.v1.ModuleCapabilitiesService.MintSolutionRegistration:input_type -> saas.accounts.v1.SolutionMintRegistrationRequest - 81, // 78: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:input_type -> saas.accounts.v1.ModuleMintWorkContextRequest - 82, // 79: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:input_type -> saas.accounts.v1.ModuleMintOperationContextRequest - 83, // 80: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:input_type -> saas.accounts.v1.ModuleMintSourceOperationContextRequest - 41, // 81: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:input_type -> saas.accounts.v1.ModulePublishEventRequest - 43, // 82: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:input_type -> saas.accounts.v1.ModuleSubscribeRequest - 46, // 83: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:input_type -> saas.accounts.v1.ModuleUnsubscribeRequest - 47, // 84: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:input_type -> saas.accounts.v1.ModuleListSubscriptionsRequest - 49, // 85: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:input_type -> saas.accounts.v1.ModuleReplayEventsRequest - 68, // 86: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:output_type -> saas.accounts.v1.ModuleCurrentInstallationResponse - 66, // 87: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse - 66, // 88: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse - 66, // 89: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse - 84, // 90: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:output_type -> saas.accounts.v1.IssuedWorkContext - 84, // 91: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:output_type -> saas.accounts.v1.IssuedWorkContext - 59, // 92: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:output_type -> saas.accounts.v1.CheckWorkContextRecordAccessResponse - 55, // 93: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:output_type -> saas.accounts.v1.ListReadableSourceCollectionsResponse - 57, // 94: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:output_type -> saas.accounts.v1.ModulePlaceRecordResponse - 7, // 95: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:output_type -> saas.accounts.v1.ModuleEnqueueJobResponse - 9, // 96: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:output_type -> saas.accounts.v1.ModuleClaimJobsResponse - 11, // 97: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:output_type -> saas.accounts.v1.ModuleHeartbeatJobResponse - 85, // 98: saas.accounts.v1.ModuleCapabilitiesService.AckJob:output_type -> google.protobuf.Empty - 85, // 99: saas.accounts.v1.ModuleCapabilitiesService.NackJob:output_type -> google.protobuf.Empty - 15, // 100: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:output_type -> saas.accounts.v1.ModuleNotifyUserResponse - 17, // 101: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:output_type -> saas.accounts.v1.ModuleNotifyOrgAdminsResponse - 21, // 102: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:output_type -> saas.accounts.v1.ModuleRequestApprovalResponse - 23, // 103: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:output_type -> saas.accounts.v1.ModuleApproval - 85, // 104: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:output_type -> google.protobuf.Empty - 85, // 105: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:output_type -> google.protobuf.Empty - 29, // 106: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:output_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesResponse - 32, // 107: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:output_type -> saas.accounts.v1.ModuleListSubjectVisibilityResponse - 34, // 108: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:output_type -> saas.accounts.v1.FetchDatasourceBlobChunk - 40, // 109: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:output_type -> saas.accounts.v1.FetchDatasourceFilesFrame - 86, // 110: saas.accounts.v1.ModuleCapabilitiesService.MintModuleRegistration:output_type -> saas.accounts.v1.ModuleMintRegistrationResponse - 87, // 111: saas.accounts.v1.ModuleCapabilitiesService.MintSolutionRegistration:output_type -> saas.accounts.v1.SolutionMintRegistrationResponse - 88, // 112: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:output_type -> saas.accounts.v1.ModuleMintWorkContextResponse - 89, // 113: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:output_type -> saas.accounts.v1.ModuleMintOperationContextResponse - 90, // 114: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:output_type -> saas.accounts.v1.ModuleMintSourceOperationContextResponse - 42, // 115: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:output_type -> saas.accounts.v1.ModulePublishEventResponse - 45, // 116: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:output_type -> saas.accounts.v1.ModuleSubscribeResponse - 85, // 117: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:output_type -> google.protobuf.Empty - 48, // 118: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:output_type -> saas.accounts.v1.ModuleListSubscriptionsResponse - 50, // 119: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:output_type -> saas.accounts.v1.ModuleReplayEventsResponse - 86, // [86:120] is the sub-list for method output_type - 52, // [52:86] is the sub-list for method input_type + 79, // 76: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:input_type -> saas.accounts.v1.ModuleMintWorkContextRequest + 80, // 77: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:input_type -> saas.accounts.v1.ModuleMintOperationContextRequest + 81, // 78: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:input_type -> saas.accounts.v1.ModuleMintSourceOperationContextRequest + 41, // 79: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:input_type -> saas.accounts.v1.ModulePublishEventRequest + 43, // 80: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:input_type -> saas.accounts.v1.ModuleSubscribeRequest + 46, // 81: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:input_type -> saas.accounts.v1.ModuleUnsubscribeRequest + 47, // 82: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:input_type -> saas.accounts.v1.ModuleListSubscriptionsRequest + 49, // 83: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:input_type -> saas.accounts.v1.ModuleReplayEventsRequest + 68, // 84: saas.accounts.v1.ModuleCapabilitiesService.GetCurrentInstallation:output_type -> saas.accounts.v1.ModuleCurrentInstallationResponse + 66, // 85: saas.accounts.v1.ModuleCapabilitiesService.ApproveExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse + 66, // 86: saas.accounts.v1.ModuleCapabilitiesService.AuthorizeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse + 66, // 87: saas.accounts.v1.ModuleCapabilitiesService.RevokeExecutableArtifact:output_type -> saas.accounts.v1.ModuleExecutableArtifactResponse + 82, // 88: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedReadAudience:output_type -> saas.accounts.v1.IssuedWorkContext + 82, // 89: saas.accounts.v1.ModuleCapabilitiesService.ExchangeDelegatedOperationAudience:output_type -> saas.accounts.v1.IssuedWorkContext + 59, // 90: saas.accounts.v1.ModuleCapabilitiesService.CheckWorkContextRecordAccess:output_type -> saas.accounts.v1.CheckWorkContextRecordAccessResponse + 55, // 91: saas.accounts.v1.ModuleCapabilitiesService.ListReadableSourceCollections:output_type -> saas.accounts.v1.ListReadableSourceCollectionsResponse + 57, // 92: saas.accounts.v1.ModuleCapabilitiesService.PlaceRecord:output_type -> saas.accounts.v1.ModulePlaceRecordResponse + 7, // 93: saas.accounts.v1.ModuleCapabilitiesService.EnqueueJob:output_type -> saas.accounts.v1.ModuleEnqueueJobResponse + 9, // 94: saas.accounts.v1.ModuleCapabilitiesService.ClaimJobs:output_type -> saas.accounts.v1.ModuleClaimJobsResponse + 11, // 95: saas.accounts.v1.ModuleCapabilitiesService.HeartbeatJob:output_type -> saas.accounts.v1.ModuleHeartbeatJobResponse + 83, // 96: saas.accounts.v1.ModuleCapabilitiesService.AckJob:output_type -> google.protobuf.Empty + 83, // 97: saas.accounts.v1.ModuleCapabilitiesService.NackJob:output_type -> google.protobuf.Empty + 15, // 98: saas.accounts.v1.ModuleCapabilitiesService.NotifyUser:output_type -> saas.accounts.v1.ModuleNotifyUserResponse + 17, // 99: saas.accounts.v1.ModuleCapabilitiesService.NotifyOrgAdmins:output_type -> saas.accounts.v1.ModuleNotifyOrgAdminsResponse + 21, // 100: saas.accounts.v1.ModuleCapabilitiesService.RequestApproval:output_type -> saas.accounts.v1.ModuleRequestApprovalResponse + 23, // 101: saas.accounts.v1.ModuleCapabilitiesService.GetApproval:output_type -> saas.accounts.v1.ModuleApproval + 83, // 102: saas.accounts.v1.ModuleCapabilitiesService.CancelApproval:output_type -> google.protobuf.Empty + 83, // 103: saas.accounts.v1.ModuleCapabilitiesService.EmitAuditEvent:output_type -> google.protobuf.Empty + 29, // 104: saas.accounts.v1.ModuleCapabilitiesService.DeclareAuditEventTypes:output_type -> saas.accounts.v1.ModuleDeclareAuditEventTypesResponse + 32, // 105: saas.accounts.v1.ModuleCapabilitiesService.ListSubjectVisibility:output_type -> saas.accounts.v1.ModuleListSubjectVisibilityResponse + 34, // 106: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceBlob:output_type -> saas.accounts.v1.FetchDatasourceBlobChunk + 40, // 107: saas.accounts.v1.ModuleCapabilitiesService.FetchDatasourceFiles:output_type -> saas.accounts.v1.FetchDatasourceFilesFrame + 84, // 108: saas.accounts.v1.ModuleCapabilitiesService.MintModuleWorkContext:output_type -> saas.accounts.v1.ModuleMintWorkContextResponse + 85, // 109: saas.accounts.v1.ModuleCapabilitiesService.MintModuleOperationContext:output_type -> saas.accounts.v1.ModuleMintOperationContextResponse + 86, // 110: saas.accounts.v1.ModuleCapabilitiesService.MintSourceOperationContext:output_type -> saas.accounts.v1.ModuleMintSourceOperationContextResponse + 42, // 111: saas.accounts.v1.ModuleCapabilitiesService.PublishEvent:output_type -> saas.accounts.v1.ModulePublishEventResponse + 45, // 112: saas.accounts.v1.ModuleCapabilitiesService.Subscribe:output_type -> saas.accounts.v1.ModuleSubscribeResponse + 83, // 113: saas.accounts.v1.ModuleCapabilitiesService.Unsubscribe:output_type -> google.protobuf.Empty + 48, // 114: saas.accounts.v1.ModuleCapabilitiesService.ListSubscriptions:output_type -> saas.accounts.v1.ModuleListSubscriptionsResponse + 50, // 115: saas.accounts.v1.ModuleCapabilitiesService.ReplayEvents:output_type -> saas.accounts.v1.ModuleReplayEventsResponse + 84, // [84:116] is the sub-list for method output_type + 52, // [52:84] is the sub-list for method input_type 52, // [52:52] is the sub-list for extension type_name 52, // [52:52] is the sub-list for extension extendee 0, // [0:52] is the sub-list for field type_name diff --git a/gen/saas/accounts/v1/module_registration.pb.go b/gen/saas/accounts/v1/module_registration.pb.go index 9aa8a8c..7308e17 100644 --- a/gen/saas/accounts/v1/module_registration.pb.go +++ b/gen/saas/accounts/v1/module_registration.pb.go @@ -23,245 +23,33 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// ModuleMintRegistrationRequest asks for the credential a module presents to -// federate its REST prefix with the gateway. secret is the module's own -// registration secret, provisioned by the composition; accounts compares it -// against the digest declared for `prefix`, so the credential for one module -// never yields a token for another. -type ModuleMintRegistrationRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` - Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ModuleMintRegistrationRequest) Reset() { - *x = ModuleMintRegistrationRequest{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ModuleMintRegistrationRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ModuleMintRegistrationRequest) ProtoMessage() {} - -func (x *ModuleMintRegistrationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ModuleMintRegistrationRequest.ProtoReflect.Descriptor instead. -func (*ModuleMintRegistrationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{0} -} - -func (x *ModuleMintRegistrationRequest) GetPrefix() string { - if x != nil { - return x.Prefix - } - return "" -} - -func (x *ModuleMintRegistrationRequest) GetSecret() string { - if x != nil { - return x.Secret - } - return "" -} - -type ModuleMintRegistrationResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` - ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ModuleMintRegistrationResponse) Reset() { - *x = ModuleMintRegistrationResponse{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ModuleMintRegistrationResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ModuleMintRegistrationResponse) ProtoMessage() {} - -func (x *ModuleMintRegistrationResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ModuleMintRegistrationResponse.ProtoReflect.Descriptor instead. -func (*ModuleMintRegistrationResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{1} -} - -func (x *ModuleMintRegistrationResponse) GetToken() string { - if x != nil { - return x.Token - } - return "" -} - -func (x *ModuleMintRegistrationResponse) GetExpiresAt() *timestamppb.Timestamp { - if x != nil { - return x.ExpiresAt - } - return nil -} - -// SolutionMintRegistrationRequest asks for the credential a solution presents to -// register — and later update or delete — its gateway upstream and its frontend -// Module-Federation remote. secret is the solution's own registration secret, -// provisioned by the composition; accounts compares it against the digest -// declared for `solution_id`, so the credential for one solution never yields a -// token for another. -type SolutionMintRegistrationRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - SolutionId string `protobuf:"bytes,1,opt,name=solution_id,json=solutionId,proto3" json:"solution_id,omitempty"` - Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SolutionMintRegistrationRequest) Reset() { - *x = SolutionMintRegistrationRequest{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SolutionMintRegistrationRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SolutionMintRegistrationRequest) ProtoMessage() {} - -func (x *SolutionMintRegistrationRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SolutionMintRegistrationRequest.ProtoReflect.Descriptor instead. -func (*SolutionMintRegistrationRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{2} -} - -func (x *SolutionMintRegistrationRequest) GetSolutionId() string { - if x != nil { - return x.SolutionId - } - return "" -} - -func (x *SolutionMintRegistrationRequest) GetSecret() string { - if x != nil { - return x.Secret - } - return "" -} - -type SolutionMintRegistrationResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` - ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *SolutionMintRegistrationResponse) Reset() { - *x = SolutionMintRegistrationResponse{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *SolutionMintRegistrationResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SolutionMintRegistrationResponse) ProtoMessage() {} - -func (x *SolutionMintRegistrationResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SolutionMintRegistrationResponse.ProtoReflect.Descriptor instead. -func (*SolutionMintRegistrationResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{3} -} - -func (x *SolutionMintRegistrationResponse) GetToken() string { - if x != nil { - return x.Token - } - return "" -} - -func (x *SolutionMintRegistrationResponse) GetExpiresAt() *timestamppb.Timestamp { - if x != nil { - return x.ExpiresAt - } - return nil -} - // ModuleMintWorkContextRequest asks for the Work Context a composed module -// presents to the module-facing capability surface. It carries the same -// registration secret the credential exchange above uses: a module proves which -// module it is once, with one credential, and the principal it acts as is -// derived from that prefix rather than configured separately. +// presents to the module-facing capability surface. The principal it acts as is +// derived from `prefix`, and `service_account_token` must prove the caller is +// the workload that principal declares, running the build its authority +// document approves. // -// The tenant the capability is bound to is not requested. It is the one declared -// for that principal by the deployment, so a module cannot name a tenant by -// asking, and the response reports which one was sealed. +// A tenant-pinned principal keeps its declared tenant. A shared principal must +// select an organization whose live installation consents to this principal; +// requesting an organization is not authority over it. type ModuleMintWorkContextRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` - Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` + // service_account_token is the pod's projected ServiceAccount token, minted + // for the `accounts` audience and read from its projection on every mint + // (the kubelet rotates it under the running process). + ServiceAccountToken string `protobuf:"bytes,3,opt,name=service_account_token,json=serviceAccountToken,proto3" json:"service_account_token,omitempty"` + // Required for a shared principal with cross_tenant in its envelope. For a + // tenant-pinned principal, omit or match its tenant. Accounts validates the + // requested organization's live installation after authenticating execution. + OrganizationId string `protobuf:"bytes,4,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ModuleMintWorkContextRequest) Reset() { *x = ModuleMintWorkContextRequest{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[4] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -273,7 +61,7 @@ func (x *ModuleMintWorkContextRequest) String() string { func (*ModuleMintWorkContextRequest) ProtoMessage() {} func (x *ModuleMintWorkContextRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[4] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -286,7 +74,7 @@ func (x *ModuleMintWorkContextRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleMintWorkContextRequest.ProtoReflect.Descriptor instead. func (*ModuleMintWorkContextRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{4} + return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{0} } func (x *ModuleMintWorkContextRequest) GetPrefix() string { @@ -296,9 +84,16 @@ func (x *ModuleMintWorkContextRequest) GetPrefix() string { return "" } -func (x *ModuleMintWorkContextRequest) GetSecret() string { +func (x *ModuleMintWorkContextRequest) GetServiceAccountToken() string { if x != nil { - return x.Secret + return x.ServiceAccountToken + } + return "" +} + +func (x *ModuleMintWorkContextRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId } return "" } @@ -317,7 +112,7 @@ type ModuleMintWorkContextResponse struct { func (x *ModuleMintWorkContextResponse) Reset() { *x = ModuleMintWorkContextResponse{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[5] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -329,7 +124,7 @@ func (x *ModuleMintWorkContextResponse) String() string { func (*ModuleMintWorkContextResponse) ProtoMessage() {} func (x *ModuleMintWorkContextResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[5] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -342,7 +137,7 @@ func (x *ModuleMintWorkContextResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleMintWorkContextResponse.ProtoReflect.Descriptor instead. func (*ModuleMintWorkContextResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{5} + return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{1} } func (x *ModuleMintWorkContextResponse) GetToken() string { @@ -377,24 +172,30 @@ func (x *ModuleMintWorkContextResponse) GetTenant() string { // the calling module's installed operation audiences, with no person present — // the capability background work (a journal replay, a scheduled refresh) uses to // call another module's service. It authenticates exactly like -// ModuleMintWorkContextRequest, with the module's identity secret for `prefix`. +// ModuleMintWorkContextRequest, with the pod's projected token for `prefix`. // // The caller names only the binding. Audience, scopes, tenant, actor and // lifetime are deployment policy: the binding's `headless_scopes`, the tenant // declared for the module principal, and a short fixed lifetime. A binding that // declares no headless scopes cannot be minted this way at all. type ModuleMintOperationContextRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` - Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` - Binding string `protobuf:"bytes,3,opt,name=binding,proto3" json:"binding,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` + Binding string `protobuf:"bytes,3,opt,name=binding,proto3" json:"binding,omitempty"` + // service_account_token is the pod's projected ServiceAccount token, as on + // ModuleMintWorkContextRequest. + ServiceAccountToken string `protobuf:"bytes,4,opt,name=service_account_token,json=serviceAccountToken,proto3" json:"service_account_token,omitempty"` + // Required for a shared principal with cross_tenant in its envelope. For a + // tenant-pinned principal, omit or match its tenant. Accounts validates the + // requested organization's live installation after authenticating execution. + OrganizationId string `protobuf:"bytes,5,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ModuleMintOperationContextRequest) Reset() { *x = ModuleMintOperationContextRequest{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[6] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -406,7 +207,7 @@ func (x *ModuleMintOperationContextRequest) String() string { func (*ModuleMintOperationContextRequest) ProtoMessage() {} func (x *ModuleMintOperationContextRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[6] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -419,7 +220,7 @@ func (x *ModuleMintOperationContextRequest) ProtoReflect() protoreflect.Message // Deprecated: Use ModuleMintOperationContextRequest.ProtoReflect.Descriptor instead. func (*ModuleMintOperationContextRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{6} + return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{2} } func (x *ModuleMintOperationContextRequest) GetPrefix() string { @@ -429,16 +230,23 @@ func (x *ModuleMintOperationContextRequest) GetPrefix() string { return "" } -func (x *ModuleMintOperationContextRequest) GetSecret() string { +func (x *ModuleMintOperationContextRequest) GetBinding() string { if x != nil { - return x.Secret + return x.Binding } return "" } -func (x *ModuleMintOperationContextRequest) GetBinding() string { +func (x *ModuleMintOperationContextRequest) GetServiceAccountToken() string { if x != nil { - return x.Binding + return x.ServiceAccountToken + } + return "" +} + +func (x *ModuleMintOperationContextRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId } return "" } @@ -459,7 +267,7 @@ type ModuleMintOperationContextResponse struct { func (x *ModuleMintOperationContextResponse) Reset() { *x = ModuleMintOperationContextResponse{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[7] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -471,7 +279,7 @@ func (x *ModuleMintOperationContextResponse) String() string { func (*ModuleMintOperationContextResponse) ProtoMessage() {} func (x *ModuleMintOperationContextResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[7] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -484,7 +292,7 @@ func (x *ModuleMintOperationContextResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ModuleMintOperationContextResponse.ProtoReflect.Descriptor instead. func (*ModuleMintOperationContextResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{7} + return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{3} } func (x *ModuleMintOperationContextResponse) GetToken() string { @@ -532,7 +340,7 @@ func (x *ModuleMintOperationContextResponse) GetBinding() string { // ModuleMintSourceOperationContextRequest asks for a Work Context that runs one // datasource source's sync with the authority a person delegated when they // connected (or reconnected) the source. It authenticates exactly like -// ModuleMintWorkContextRequest, with the module's identity secret for `prefix`. +// ModuleMintWorkContextRequest, with the pod's projected token for `prefix`. // // The caller names the delegation — by its id, or by the source whose active // delegation to this module is meant — and nothing else. Tenant, owner, @@ -542,7 +350,9 @@ func (x *ModuleMintOperationContextResponse) GetBinding() string { type ModuleMintSourceOperationContextRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` - Secret string `protobuf:"bytes,2,opt,name=secret,proto3" json:"secret,omitempty"` + // service_account_token is the pod's projected ServiceAccount token, as on + // ModuleMintWorkContextRequest. + ServiceAccountToken string `protobuf:"bytes,5,opt,name=service_account_token,json=serviceAccountToken,proto3" json:"service_account_token,omitempty"` // Types that are valid to be assigned to Delegation: // // *ModuleMintSourceOperationContextRequest_DelegationId @@ -554,7 +364,7 @@ type ModuleMintSourceOperationContextRequest struct { func (x *ModuleMintSourceOperationContextRequest) Reset() { *x = ModuleMintSourceOperationContextRequest{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[8] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -566,7 +376,7 @@ func (x *ModuleMintSourceOperationContextRequest) String() string { func (*ModuleMintSourceOperationContextRequest) ProtoMessage() {} func (x *ModuleMintSourceOperationContextRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[8] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -579,7 +389,7 @@ func (x *ModuleMintSourceOperationContextRequest) ProtoReflect() protoreflect.Me // Deprecated: Use ModuleMintSourceOperationContextRequest.ProtoReflect.Descriptor instead. func (*ModuleMintSourceOperationContextRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{8} + return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{4} } func (x *ModuleMintSourceOperationContextRequest) GetPrefix() string { @@ -589,9 +399,9 @@ func (x *ModuleMintSourceOperationContextRequest) GetPrefix() string { return "" } -func (x *ModuleMintSourceOperationContextRequest) GetSecret() string { +func (x *ModuleMintSourceOperationContextRequest) GetServiceAccountToken() string { if x != nil { - return x.Secret + return x.ServiceAccountToken } return "" } @@ -669,7 +479,7 @@ type ModuleMintSourceOperationContextResponse struct { func (x *ModuleMintSourceOperationContextResponse) Reset() { *x = ModuleMintSourceOperationContextResponse{} - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[9] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -681,7 +491,7 @@ func (x *ModuleMintSourceOperationContextResponse) String() string { func (*ModuleMintSourceOperationContextResponse) ProtoMessage() {} func (x *ModuleMintSourceOperationContextResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[9] + mi := &file_saas_accounts_v1_module_registration_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -694,7 +504,7 @@ func (x *ModuleMintSourceOperationContextResponse) ProtoReflect() protoreflect.M // Deprecated: Use ModuleMintSourceOperationContextResponse.ProtoReflect.Descriptor instead. func (*ModuleMintSourceOperationContextResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{9} + return file_saas_accounts_v1_module_registration_proto_rawDescGZIP(), []int{5} } func (x *ModuleMintSourceOperationContextResponse) GetToken() string { @@ -764,40 +574,23 @@ var File_saas_accounts_v1_module_registration_proto protoreflect.FileDescriptor const file_saas_accounts_v1_module_registration_proto_rawDesc = "" + "\n" + - "*saas/accounts/v1/module_registration.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x89\x01\n" + - "\x1dModuleMintRegistrationRequest\x12D\n" + - "\x06prefix\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$R\x06prefix\x12\"\n" + - "\x06secret\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x06secret\"q\n" + - "\x1eModuleMintRegistrationResponse\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\x129\n" + - "\n" + - "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\x94\x01\n" + - "\x1fSolutionMintRegistrationRequest\x12M\n" + - "\vsolution_id\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$R\n" + - "solutionId\x12\"\n" + - "\x06secret\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x06secret\"s\n" + - " SolutionMintRegistrationResponse\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\x129\n" + - "\n" + - "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\x88\x01\n" + + "*saas/accounts/v1/module_registration.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe9\x01\n" + "\x1cModuleMintWorkContextRequest\x12D\n" + - "\x06prefix\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$R\x06prefix\x12\"\n" + - "\x06secret\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x06secret\"\xab\x01\n" + + "\x06prefix\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$R\x06prefix\x12?\n" + + "\x15service_account_token\x18\x03 \x01(\tB\v\xbaH\br\x06\x10\x01\x18\x80\x80\x04R\x13serviceAccountToken\x124\n" + + "\x0forganization_id\x18\x04 \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\x0eorganizationIdJ\x04\b\x02\x10\x03R\x06secret\"\xab\x01\n" + "\x1dModuleMintWorkContextResponse\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x129\n" + "\n" + "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12!\n" + "\fprincipal_id\x18\x03 \x01(\tR\vprincipalId\x12\x16\n" + - "\x06tenant\x18\x04 \x01(\tR\x06tenant\"\xb3\x01\n" + + "\x06tenant\x18\x04 \x01(\tR\x06tenant\"\x94\x02\n" + "!ModuleMintOperationContextRequest\x12D\n" + - "\x06prefix\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$R\x06prefix\x12\"\n" + - "\x06secret\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x06secret\x12$\n" + + "\x06prefix\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$R\x06prefix\x12$\n" + "\abinding\x18\x03 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x01R\abinding\"\xe6\x01\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\abinding\x12?\n" + + "\x15service_account_token\x18\x04 \x01(\tB\v\xbaH\br\x06\x10\x01\x18\x80\x80\x04R\x13serviceAccountToken\x124\n" + + "\x0forganization_id\x18\x05 \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\x0eorganizationIdJ\x04\b\x02\x10\x03R\x06secret\"\xe6\x01\n" + "\"ModuleMintOperationContextResponse\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x129\n" + "\n" + @@ -805,15 +598,14 @@ const file_saas_accounts_v1_module_registration_proto_rawDesc = "" + "\fprincipal_id\x18\x03 \x01(\tR\vprincipalId\x12\x16\n" + "\x06tenant\x18\x04 \x01(\tR\x06tenant\x12\x1a\n" + "\baudience\x18\x05 \x01(\tR\baudience\x12\x18\n" + - "\abinding\x18\x06 \x01(\tR\abinding\"\x82\x02\n" + + "\abinding\x18\x06 \x01(\tR\abinding\"\xad\x02\n" + "'ModuleMintSourceOperationContextRequest\x12D\n" + - "\x06prefix\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$R\x06prefix\x12\"\n" + - "\x06secret\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x06secret\x12/\n" + + "\x06prefix\x18\x01 \x01(\tB,\xbaH)r'\x10\x01\x18?2!^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$R\x06prefix\x12?\n" + + "\x15service_account_token\x18\x05 \x01(\tB\v\xbaH\br\x06\x10\x01\x18\x80\x80\x04R\x13serviceAccountToken\x12/\n" + "\rdelegation_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01H\x00R\fdelegationId\x12'\n" + "\tsource_id\x18\x04 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01H\x00R\bsourceIdB\x13\n" + "\n" + - "delegation\x12\x05\xbaH\x02\b\x01\"\xdc\x02\n" + + "delegation\x12\x05\xbaH\x02\b\x01J\x04\b\x02\x10\x03R\x06secret\"\xdc\x02\n" + "(ModuleMintSourceOperationContextResponse\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x129\n" + "\n" + @@ -839,31 +631,25 @@ func file_saas_accounts_v1_module_registration_proto_rawDescGZIP() []byte { return file_saas_accounts_v1_module_registration_proto_rawDescData } -var file_saas_accounts_v1_module_registration_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_saas_accounts_v1_module_registration_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_saas_accounts_v1_module_registration_proto_goTypes = []any{ - (*ModuleMintRegistrationRequest)(nil), // 0: saas.accounts.v1.ModuleMintRegistrationRequest - (*ModuleMintRegistrationResponse)(nil), // 1: saas.accounts.v1.ModuleMintRegistrationResponse - (*SolutionMintRegistrationRequest)(nil), // 2: saas.accounts.v1.SolutionMintRegistrationRequest - (*SolutionMintRegistrationResponse)(nil), // 3: saas.accounts.v1.SolutionMintRegistrationResponse - (*ModuleMintWorkContextRequest)(nil), // 4: saas.accounts.v1.ModuleMintWorkContextRequest - (*ModuleMintWorkContextResponse)(nil), // 5: saas.accounts.v1.ModuleMintWorkContextResponse - (*ModuleMintOperationContextRequest)(nil), // 6: saas.accounts.v1.ModuleMintOperationContextRequest - (*ModuleMintOperationContextResponse)(nil), // 7: saas.accounts.v1.ModuleMintOperationContextResponse - (*ModuleMintSourceOperationContextRequest)(nil), // 8: saas.accounts.v1.ModuleMintSourceOperationContextRequest - (*ModuleMintSourceOperationContextResponse)(nil), // 9: saas.accounts.v1.ModuleMintSourceOperationContextResponse - (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp + (*ModuleMintWorkContextRequest)(nil), // 0: saas.accounts.v1.ModuleMintWorkContextRequest + (*ModuleMintWorkContextResponse)(nil), // 1: saas.accounts.v1.ModuleMintWorkContextResponse + (*ModuleMintOperationContextRequest)(nil), // 2: saas.accounts.v1.ModuleMintOperationContextRequest + (*ModuleMintOperationContextResponse)(nil), // 3: saas.accounts.v1.ModuleMintOperationContextResponse + (*ModuleMintSourceOperationContextRequest)(nil), // 4: saas.accounts.v1.ModuleMintSourceOperationContextRequest + (*ModuleMintSourceOperationContextResponse)(nil), // 5: saas.accounts.v1.ModuleMintSourceOperationContextResponse + (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp } var file_saas_accounts_v1_module_registration_proto_depIdxs = []int32{ - 10, // 0: saas.accounts.v1.ModuleMintRegistrationResponse.expires_at:type_name -> google.protobuf.Timestamp - 10, // 1: saas.accounts.v1.SolutionMintRegistrationResponse.expires_at:type_name -> google.protobuf.Timestamp - 10, // 2: saas.accounts.v1.ModuleMintWorkContextResponse.expires_at:type_name -> google.protobuf.Timestamp - 10, // 3: saas.accounts.v1.ModuleMintOperationContextResponse.expires_at:type_name -> google.protobuf.Timestamp - 10, // 4: saas.accounts.v1.ModuleMintSourceOperationContextResponse.expires_at:type_name -> google.protobuf.Timestamp - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 6, // 0: saas.accounts.v1.ModuleMintWorkContextResponse.expires_at:type_name -> google.protobuf.Timestamp + 6, // 1: saas.accounts.v1.ModuleMintOperationContextResponse.expires_at:type_name -> google.protobuf.Timestamp + 6, // 2: saas.accounts.v1.ModuleMintSourceOperationContextResponse.expires_at:type_name -> google.protobuf.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_saas_accounts_v1_module_registration_proto_init() } @@ -871,7 +657,7 @@ func file_saas_accounts_v1_module_registration_proto_init() { if File_saas_accounts_v1_module_registration_proto != nil { return } - file_saas_accounts_v1_module_registration_proto_msgTypes[8].OneofWrappers = []any{ + file_saas_accounts_v1_module_registration_proto_msgTypes[4].OneofWrappers = []any{ (*ModuleMintSourceOperationContextRequest_DelegationId)(nil), (*ModuleMintSourceOperationContextRequest_SourceId)(nil), } @@ -881,7 +667,7 @@ func file_saas_accounts_v1_module_registration_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_module_registration_proto_rawDesc), len(file_saas_accounts_v1_module_registration_proto_rawDesc)), NumEnums: 0, - NumMessages: 10, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/saas/accounts/v1/platform_admin.pb.go b/gen/saas/accounts/v1/platform_admin.pb.go index 89c4ee3..5caaea1 100644 --- a/gen/saas/accounts/v1/platform_admin.pb.go +++ b/gen/saas/accounts/v1/platform_admin.pb.go @@ -3485,6 +3485,271 @@ func (x *ListPlatformCatalogueResponse) GetApprovedInventories() map[string][]by return nil } +// ModuleEnvelope is one revision of the module envelope: the authority ceiling a +// platform super_admin writes, which every module principal on this host is +// derived inside. Revisions are append-only; the current ceiling is the +// highest. +type ModuleEnvelope struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The revision, from 1. A delivered authority document names the revision + // it was rendered against and is inside this one only. + Revision int64 `protobuf:"varint,1,opt,name=revision,proto3" json:"revision,omitempty"` + // The whole ceiling at this revision, as JSON: `revision`, + // `approved_builds`, and `principals` keyed by module prefix, each with the + // whole authority units this host admits (`bindings`: id, revision, scope, + // queue, namespace, binding_key, lookup_method), the workloads it may run as + // (`workload_identities`: spiffe_id, container), its `scope_ceilings` and its + // `grant` ceiling (tenant, cross_tenant, queues, namespaces, + // external_namespaces, resources, read_audiences, operation_audiences, + // artifact_policies). + Document string `protobuf:"bytes,2,opt,name=document,proto3" json:"document,omitempty"` + // The platform administrator who wrote this revision. + WrittenBy string `protobuf:"bytes,3,opt,name=written_by,json=writtenBy,proto3" json:"written_by,omitempty"` + WrittenAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=written_at,json=writtenAt,proto3" json:"written_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleEnvelope) Reset() { + *x = ModuleEnvelope{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleEnvelope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleEnvelope) ProtoMessage() {} + +func (x *ModuleEnvelope) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModuleEnvelope.ProtoReflect.Descriptor instead. +func (*ModuleEnvelope) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{47} +} + +func (x *ModuleEnvelope) GetRevision() int64 { + if x != nil { + return x.Revision + } + return 0 +} + +func (x *ModuleEnvelope) GetDocument() string { + if x != nil { + return x.Document + } + return "" +} + +func (x *ModuleEnvelope) GetWrittenBy() string { + if x != nil { + return x.WrittenBy + } + return "" +} + +func (x *ModuleEnvelope) GetWrittenAt() *timestamppb.Timestamp { + if x != nil { + return x.WrittenAt + } + return nil +} + +type GetModuleEnvelopeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetModuleEnvelopeRequest) Reset() { + *x = GetModuleEnvelopeRequest{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetModuleEnvelopeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetModuleEnvelopeRequest) ProtoMessage() {} + +func (x *GetModuleEnvelopeRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetModuleEnvelopeRequest.ProtoReflect.Descriptor instead. +func (*GetModuleEnvelopeRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{48} +} + +type GetModuleEnvelopeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The current revision. Absent when no envelope has been written, which + // grants no module anything. + Envelope *ModuleEnvelope `protobuf:"bytes,1,opt,name=envelope,proto3,oneof" json:"envelope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetModuleEnvelopeResponse) Reset() { + *x = GetModuleEnvelopeResponse{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetModuleEnvelopeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetModuleEnvelopeResponse) ProtoMessage() {} + +func (x *GetModuleEnvelopeResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetModuleEnvelopeResponse.ProtoReflect.Descriptor instead. +func (*GetModuleEnvelopeResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{49} +} + +func (x *GetModuleEnvelopeResponse) GetEnvelope() *ModuleEnvelope { + if x != nil { + return x.Envelope + } + return nil +} + +type PutModuleEnvelopeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The revision this write replaces: the one the caller read, 0 when none has + // been written. A write whose expected revision is not the current one is + // refused with ABORTED, and nothing is written. + ExpectedRevision int64 `protobuf:"varint,1,opt,name=expected_revision,json=expectedRevision,proto3" json:"expected_revision,omitempty"` + // The whole ceiling, as JSON, carrying revision expected_revision + 1. + Document string `protobuf:"bytes,2,opt,name=document,proto3" json:"document,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PutModuleEnvelopeRequest) Reset() { + *x = PutModuleEnvelopeRequest{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PutModuleEnvelopeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutModuleEnvelopeRequest) ProtoMessage() {} + +func (x *PutModuleEnvelopeRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutModuleEnvelopeRequest.ProtoReflect.Descriptor instead. +func (*PutModuleEnvelopeRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{50} +} + +func (x *PutModuleEnvelopeRequest) GetExpectedRevision() int64 { + if x != nil { + return x.ExpectedRevision + } + return 0 +} + +func (x *PutModuleEnvelopeRequest) GetDocument() string { + if x != nil { + return x.Document + } + return "" +} + +type PutModuleEnvelopeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Envelope *ModuleEnvelope `protobuf:"bytes,1,opt,name=envelope,proto3" json:"envelope,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PutModuleEnvelopeResponse) Reset() { + *x = PutModuleEnvelopeResponse{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PutModuleEnvelopeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutModuleEnvelopeResponse) ProtoMessage() {} + +func (x *PutModuleEnvelopeResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutModuleEnvelopeResponse.ProtoReflect.Descriptor instead. +func (*PutModuleEnvelopeResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{51} +} + +func (x *PutModuleEnvelopeResponse) GetEnvelope() *ModuleEnvelope { + if x != nil { + return x.Envelope + } + return nil +} + var File_saas_accounts_v1_platform_admin_proto protoreflect.FileDescriptor const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + @@ -3711,7 +3976,23 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\x14approved_inventories\x18\x03 \x03(\v2H.saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntryR\x13approvedInventories\x1aF\n" + "\x18ApprovedInventoriesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01*~\n" + + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"\xa2\x01\n" + + "\x0eModuleEnvelope\x12\x1a\n" + + "\brevision\x18\x01 \x01(\x03R\brevision\x12\x1a\n" + + "\bdocument\x18\x02 \x01(\tR\bdocument\x12\x1d\n" + + "\n" + + "written_by\x18\x03 \x01(\tR\twrittenBy\x129\n" + + "\n" + + "written_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\twrittenAt\"\x1a\n" + + "\x18GetModuleEnvelopeRequest\"k\n" + + "\x19GetModuleEnvelopeResponse\x12A\n" + + "\benvelope\x18\x01 \x01(\v2 .saas.accounts.v1.ModuleEnvelopeH\x00R\benvelope\x88\x01\x01B\v\n" + + "\t_envelope\"y\n" + + "\x18PutModuleEnvelopeRequest\x124\n" + + "\x11expected_revision\x18\x01 \x01(\x03B\a\xbaH\x04\"\x02(\x00R\x10expectedRevision\x12'\n" + + "\bdocument\x18\x02 \x01(\tB\v\xbaH\br\x06\x10\x01(\x80\x80@R\bdocument\"Y\n" + + "\x19PutModuleEnvelopeResponse\x12<\n" + + "\benvelope\x18\x01 \x01(\v2 .saas.accounts.v1.ModuleEnvelopeR\benvelope*~\n" + "\x12CatalogueEntryKind\x12$\n" + " CATALOGUE_ENTRY_KIND_UNSPECIFIED\x10\x00\x12\x1f\n" + "\x1bCATALOGUE_ENTRY_KIND_MODULE\x10\x01\x12!\n" + @@ -3725,7 +4006,7 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "&CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED\x10\x00\x121\n" + "-CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED\x10\x01\x12.\n" + "*CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS\x10\x02\x123\n" + - "/CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED\x10\x032\xef\x1f\n" + + "/CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED\x10\x032\xf5\"\n" + "\x14PlatformAdminService\x12\x8e\x01\n" + "\vSearchUsers\x12$.saas.accounts.v1.SearchUsersRequest\x1a%.saas.accounts.v1.SearchUsersResponse\"2\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/platform/users\x12\xab\x01\n" + "\vSuspendUser\x12$.saas.accounts.v1.SuspendUserRequest\x1a\x16.google.protobuf.Empty\"^\xc2\xf3\x18+\b\x02\x10\x010\x01:\x17\n" + @@ -3760,7 +4041,10 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\x11saas.job.replayed\x10\x02@\x03H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02-:\x01*\"(/v1/platform/jobs/{source_job_id}:replay\x12\xab\x01\n" + "\x12GetEventOperations\x12).saas.events.v1.GetEventOperationsRequest\x1a*.saas.events.v1.GetEventOperationsResponse\">\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02 \x12\x1e/v1/platform/events/operations\x12\xba\x01\n" + "\x16ListEventSubscriptions\x12-.saas.events.v1.ListEventSubscriptionsRequest\x1a..saas.events.v1.ListEventSubscriptionsResponse\"A\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02#\x12!/v1/platform/events/subscriptions\x12\xb0\x01\n" + - "\x15ListPlatformCatalogue\x12..saas.accounts.v1.ListPlatformCatalogueRequest\x1a/.saas.accounts.v1.ListPlatformCatalogueResponse\"6\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x18\x12\x16/v1/platform/catalogueB\xd0\x01\n" + + "\x15ListPlatformCatalogue\x12..saas.accounts.v1.ListPlatformCatalogueRequest\x1a/.saas.accounts.v1.ListPlatformCatalogueResponse\"6\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x18\x12\x16/v1/platform/catalogue\x12\xaa\x01\n" + + "\x11GetModuleEnvelope\x12*.saas.accounts.v1.GetModuleEnvelopeRequest\x1a+.saas.accounts.v1.GetModuleEnvelopeResponse\"<\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/platform/module-envelope\x12\xd6\x01\n" + + "\x11PutModuleEnvelope\x12*.saas.accounts.v1.PutModuleEnvelopeRequest\x1a+.saas.accounts.v1.PutModuleEnvelopeResponse\"h\xc2\xf3\x18=\b\x02\x10\x010\x03:)\n" + + "%saas.platform.module_envelope_written\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02!:\x01*\x1a\x1c/v1/platform/module-envelopeB\xd0\x01\n" + "\x14com.saas.accounts.v1B\x12PlatformAdminProtoP\x01ZBgithub.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" var ( @@ -3776,7 +4060,7 @@ func file_saas_accounts_v1_platform_admin_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_platform_admin_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 50) +var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 55) var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ (CatalogueEntryKind)(0), // 0: saas.accounts.v1.CatalogueEntryKind (CatalogueGapReason)(0), // 1: saas.accounts.v1.CatalogueGapReason @@ -3828,43 +4112,48 @@ var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ (*CatalogueEntry)(nil), // 47: saas.accounts.v1.CatalogueEntry (*ListPlatformCatalogueRequest)(nil), // 48: saas.accounts.v1.ListPlatformCatalogueRequest (*ListPlatformCatalogueResponse)(nil), // 49: saas.accounts.v1.ListPlatformCatalogueResponse - nil, // 50: saas.accounts.v1.SessionInfo.DeviceInfoEntry - nil, // 51: saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry - nil, // 52: saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry - (*User)(nil), // 53: saas.accounts.v1.User - (*Organization)(nil), // 54: saas.accounts.v1.Organization - (*OrgMembership)(nil), // 55: saas.accounts.v1.OrgMembership - (*timestamppb.Timestamp)(nil), // 56: google.protobuf.Timestamp - (*Installation)(nil), // 57: saas.accounts.v1.Installation - (*CollectionReadGrant)(nil), // 58: saas.accounts.v1.CollectionReadGrant - (*SolutionRegistration)(nil), // 59: saas.accounts.v1.SolutionRegistration - (*v1.GetJobOperationsRequest)(nil), // 60: saas.jobs.v1.GetJobOperationsRequest - (*v1.ListJobsRequest)(nil), // 61: saas.jobs.v1.ListJobsRequest - (*v1.GetJobRequest)(nil), // 62: saas.jobs.v1.GetJobRequest - (*v1.ReplayJobRequest)(nil), // 63: saas.jobs.v1.ReplayJobRequest - (*v11.GetEventOperationsRequest)(nil), // 64: saas.events.v1.GetEventOperationsRequest - (*v11.ListEventSubscriptionsRequest)(nil), // 65: saas.events.v1.ListEventSubscriptionsRequest - (*emptypb.Empty)(nil), // 66: google.protobuf.Empty - (*v1.GetJobOperationsResponse)(nil), // 67: saas.jobs.v1.GetJobOperationsResponse - (*v1.ListJobsResponse)(nil), // 68: saas.jobs.v1.ListJobsResponse - (*v1.GetJobResponse)(nil), // 69: saas.jobs.v1.GetJobResponse - (*v1.ReplayJobResponse)(nil), // 70: saas.jobs.v1.ReplayJobResponse - (*v11.GetEventOperationsResponse)(nil), // 71: saas.events.v1.GetEventOperationsResponse - (*v11.ListEventSubscriptionsResponse)(nil), // 72: saas.events.v1.ListEventSubscriptionsResponse + (*ModuleEnvelope)(nil), // 50: saas.accounts.v1.ModuleEnvelope + (*GetModuleEnvelopeRequest)(nil), // 51: saas.accounts.v1.GetModuleEnvelopeRequest + (*GetModuleEnvelopeResponse)(nil), // 52: saas.accounts.v1.GetModuleEnvelopeResponse + (*PutModuleEnvelopeRequest)(nil), // 53: saas.accounts.v1.PutModuleEnvelopeRequest + (*PutModuleEnvelopeResponse)(nil), // 54: saas.accounts.v1.PutModuleEnvelopeResponse + nil, // 55: saas.accounts.v1.SessionInfo.DeviceInfoEntry + nil, // 56: saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry + nil, // 57: saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry + (*User)(nil), // 58: saas.accounts.v1.User + (*Organization)(nil), // 59: saas.accounts.v1.Organization + (*OrgMembership)(nil), // 60: saas.accounts.v1.OrgMembership + (*timestamppb.Timestamp)(nil), // 61: google.protobuf.Timestamp + (*Installation)(nil), // 62: saas.accounts.v1.Installation + (*CollectionReadGrant)(nil), // 63: saas.accounts.v1.CollectionReadGrant + (*SolutionRegistration)(nil), // 64: saas.accounts.v1.SolutionRegistration + (*v1.GetJobOperationsRequest)(nil), // 65: saas.jobs.v1.GetJobOperationsRequest + (*v1.ListJobsRequest)(nil), // 66: saas.jobs.v1.ListJobsRequest + (*v1.GetJobRequest)(nil), // 67: saas.jobs.v1.GetJobRequest + (*v1.ReplayJobRequest)(nil), // 68: saas.jobs.v1.ReplayJobRequest + (*v11.GetEventOperationsRequest)(nil), // 69: saas.events.v1.GetEventOperationsRequest + (*v11.ListEventSubscriptionsRequest)(nil), // 70: saas.events.v1.ListEventSubscriptionsRequest + (*emptypb.Empty)(nil), // 71: google.protobuf.Empty + (*v1.GetJobOperationsResponse)(nil), // 72: saas.jobs.v1.GetJobOperationsResponse + (*v1.ListJobsResponse)(nil), // 73: saas.jobs.v1.ListJobsResponse + (*v1.GetJobResponse)(nil), // 74: saas.jobs.v1.GetJobResponse + (*v1.ReplayJobResponse)(nil), // 75: saas.jobs.v1.ReplayJobResponse + (*v11.GetEventOperationsResponse)(nil), // 76: saas.events.v1.GetEventOperationsResponse + (*v11.ListEventSubscriptionsResponse)(nil), // 77: saas.events.v1.ListEventSubscriptionsResponse } var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ - 53, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User - 54, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization + 58, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User + 59, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization 6, // 2: saas.accounts.v1.ListAllOrganizationsResponse.organizations:type_name -> saas.accounts.v1.PlatformOrganization - 55, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership - 50, // 4: saas.accounts.v1.SessionInfo.device_info:type_name -> saas.accounts.v1.SessionInfo.DeviceInfoEntry - 56, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp - 56, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp - 56, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp - 56, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp + 60, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership + 55, // 4: saas.accounts.v1.SessionInfo.device_info:type_name -> saas.accounts.v1.SessionInfo.DeviceInfoEntry + 61, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp + 61, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp + 61, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp + 61, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp 17, // 9: saas.accounts.v1.ListActiveSessionsResponse.sessions:type_name -> saas.accounts.v1.SessionInfo 22, // 10: saas.accounts.v1.GetOrgEntitlementsResponse.entitlements:type_name -> saas.accounts.v1.EntitlementInfo - 56, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp + 61, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp 28, // 12: saas.accounts.v1.ListPlatformAdminsResponse.admins:type_name -> saas.accounts.v1.PlatformAdminEntry 31, // 13: saas.accounts.v1.ListFeatureFlagsResponse.flags:type_name -> saas.accounts.v1.FeatureFlagEntry 1, // 14: saas.accounts.v1.CatalogueGap.reason:type_name -> saas.accounts.v1.CatalogueGapReason @@ -3875,9 +4164,9 @@ var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ 39, // 19: saas.accounts.v1.CatalogueAuthorized.not_authorized:type_name -> saas.accounts.v1.CatalogueNotAuthorized 35, // 20: saas.accounts.v1.CatalogueAuthorized.authorization_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 21: saas.accounts.v1.CatalogueApplied.applied_revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 51, // 22: saas.accounts.v1.CatalogueObservedExecution.container_image_digests:type_name -> saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry + 56, // 22: saas.accounts.v1.CatalogueObservedExecution.container_image_digests:type_name -> saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry 35, // 23: saas.accounts.v1.CatalogueObserved.observed_revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 56, // 24: saas.accounts.v1.CatalogueObserved.observed_at:type_name -> google.protobuf.Timestamp + 61, // 24: saas.accounts.v1.CatalogueObserved.observed_at:type_name -> google.protobuf.Timestamp 35, // 25: saas.accounts.v1.CatalogueObserved.observation_freshness_gap:type_name -> saas.accounts.v1.CatalogueGap 42, // 26: saas.accounts.v1.CatalogueObserved.observed_execution:type_name -> saas.accounts.v1.CatalogueObservedExecution 35, // 27: saas.accounts.v1.CatalogueObserved.observed_execution_gap:type_name -> saas.accounts.v1.CatalogueGap @@ -3886,12 +4175,12 @@ var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ 35, // 30: saas.accounts.v1.CatalogueWithdrawing.withdrawal_state_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 31: saas.accounts.v1.CatalogueWithdrawing.credential_revocation_state_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 32: saas.accounts.v1.CatalogueRetired.retirement_state_gap:type_name -> saas.accounts.v1.CatalogueGap - 57, // 33: saas.accounts.v1.CatalogueInstallation.installation:type_name -> saas.accounts.v1.Installation + 62, // 33: saas.accounts.v1.CatalogueInstallation.installation:type_name -> saas.accounts.v1.Installation 36, // 34: saas.accounts.v1.CatalogueInstallation.agent_release:type_name -> saas.accounts.v1.CatalogueRelease 35, // 35: saas.accounts.v1.CatalogueInstallation.agent_release_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 36: saas.accounts.v1.CatalogueInstallation.revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 58, // 37: saas.accounts.v1.CatalogueInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant - 58, // 38: saas.accounts.v1.CatalogueInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 63, // 37: saas.accounts.v1.CatalogueInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 63, // 38: saas.accounts.v1.CatalogueInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant 0, // 39: saas.accounts.v1.CatalogueEntry.kind:type_name -> saas.accounts.v1.CatalogueEntryKind 35, // 40: saas.accounts.v1.CatalogueEntry.publisher_gap:type_name -> saas.accounts.v1.CatalogueGap 37, // 41: saas.accounts.v1.CatalogueEntry.desired:type_name -> saas.accounts.v1.CatalogueDesired @@ -3901,61 +4190,68 @@ var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ 44, // 45: saas.accounts.v1.CatalogueEntry.withdrawing:type_name -> saas.accounts.v1.CatalogueWithdrawing 45, // 46: saas.accounts.v1.CatalogueEntry.retired:type_name -> saas.accounts.v1.CatalogueRetired 35, // 47: saas.accounts.v1.CatalogueEntry.build_size_gap:type_name -> saas.accounts.v1.CatalogueGap - 59, // 48: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration + 64, // 48: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration 46, // 49: saas.accounts.v1.CatalogueEntry.installations:type_name -> saas.accounts.v1.CatalogueInstallation 47, // 50: saas.accounts.v1.ListPlatformCatalogueResponse.entries:type_name -> saas.accounts.v1.CatalogueEntry - 52, // 51: saas.accounts.v1.ListPlatformCatalogueResponse.approved_inventories:type_name -> saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry - 3, // 52: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest - 10, // 53: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest - 11, // 54: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest - 12, // 55: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest - 14, // 56: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest - 16, // 57: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest - 19, // 58: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest - 20, // 59: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest - 23, // 60: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest - 25, // 61: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest - 26, // 62: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest - 27, // 63: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest - 5, // 64: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest - 8, // 65: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest - 30, // 66: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest - 33, // 67: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest - 60, // 68: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest - 61, // 69: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest - 62, // 70: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest - 63, // 71: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest - 64, // 72: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest - 65, // 73: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest - 48, // 74: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest - 4, // 75: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse - 66, // 76: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty - 66, // 77: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty - 13, // 78: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse - 15, // 79: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse - 18, // 80: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse - 66, // 81: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty - 21, // 82: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse - 24, // 83: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse - 66, // 84: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty - 66, // 85: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty - 29, // 86: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse - 7, // 87: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse - 9, // 88: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse - 32, // 89: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse - 34, // 90: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse - 67, // 91: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse - 68, // 92: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse - 69, // 93: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse - 70, // 94: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse - 71, // 95: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse - 72, // 96: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse - 49, // 97: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse - 75, // [75:98] is the sub-list for method output_type - 52, // [52:75] is the sub-list for method input_type - 52, // [52:52] is the sub-list for extension type_name - 52, // [52:52] is the sub-list for extension extendee - 0, // [0:52] is the sub-list for field type_name + 57, // 51: saas.accounts.v1.ListPlatformCatalogueResponse.approved_inventories:type_name -> saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry + 61, // 52: saas.accounts.v1.ModuleEnvelope.written_at:type_name -> google.protobuf.Timestamp + 50, // 53: saas.accounts.v1.GetModuleEnvelopeResponse.envelope:type_name -> saas.accounts.v1.ModuleEnvelope + 50, // 54: saas.accounts.v1.PutModuleEnvelopeResponse.envelope:type_name -> saas.accounts.v1.ModuleEnvelope + 3, // 55: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest + 10, // 56: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest + 11, // 57: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest + 12, // 58: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest + 14, // 59: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest + 16, // 60: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest + 19, // 61: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest + 20, // 62: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest + 23, // 63: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest + 25, // 64: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest + 26, // 65: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest + 27, // 66: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest + 5, // 67: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest + 8, // 68: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest + 30, // 69: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest + 33, // 70: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest + 65, // 71: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest + 66, // 72: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest + 67, // 73: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest + 68, // 74: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest + 69, // 75: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest + 70, // 76: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest + 48, // 77: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest + 51, // 78: saas.accounts.v1.PlatformAdminService.GetModuleEnvelope:input_type -> saas.accounts.v1.GetModuleEnvelopeRequest + 53, // 79: saas.accounts.v1.PlatformAdminService.PutModuleEnvelope:input_type -> saas.accounts.v1.PutModuleEnvelopeRequest + 4, // 80: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse + 71, // 81: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty + 71, // 82: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty + 13, // 83: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse + 15, // 84: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse + 18, // 85: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse + 71, // 86: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty + 21, // 87: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse + 24, // 88: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse + 71, // 89: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty + 71, // 90: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty + 29, // 91: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse + 7, // 92: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse + 9, // 93: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse + 32, // 94: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse + 34, // 95: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse + 72, // 96: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse + 73, // 97: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse + 74, // 98: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse + 75, // 99: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse + 76, // 100: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse + 77, // 101: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse + 49, // 102: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse + 52, // 103: saas.accounts.v1.PlatformAdminService.GetModuleEnvelope:output_type -> saas.accounts.v1.GetModuleEnvelopeResponse + 54, // 104: saas.accounts.v1.PlatformAdminService.PutModuleEnvelope:output_type -> saas.accounts.v1.PutModuleEnvelopeResponse + 80, // [80:105] is the sub-list for method output_type + 55, // [55:80] is the sub-list for method input_type + 55, // [55:55] is the sub-list for extension type_name + 55, // [55:55] is the sub-list for extension extendee + 0, // [0:55] is the sub-list for field type_name } func init() { file_saas_accounts_v1_platform_admin_proto_init() } @@ -4009,13 +4305,14 @@ func file_saas_accounts_v1_platform_admin_proto_init() { (*CatalogueEntry_PublisherGap)(nil), (*CatalogueEntry_BuildSizeGap)(nil), } + file_saas_accounts_v1_platform_admin_proto_msgTypes[49].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_platform_admin_proto_rawDesc), len(file_saas_accounts_v1_platform_admin_proto_rawDesc)), NumEnums: 3, - NumMessages: 50, + NumMessages: 55, NumExtensions: 0, NumServices: 1, }, diff --git a/moduleauthority/broker_test.go b/moduleauthority/broker_test.go index b7ddb75..43fa557 100644 --- a/moduleauthority/broker_test.go +++ b/moduleauthority/broker_test.go @@ -17,7 +17,7 @@ func TestTheGatewayFakeRefusesTheConnectPathTheOldClientUsed(t *testing.T) { // way the real gateway does, or every other test here proves nothing. h := newHost(t) old := accountsv1connect.NewModuleCapabilitiesServiceClient(h.gateway.HTTPClient(), h.gateway.BaseURL()) - _, err := old.MintModuleWorkContext(t.Context(), connect.NewRequest(&ModuleMintWorkContextRequest{Prefix: testPrefix, Secret: testSecret})) + _, err := old.MintModuleWorkContext(t.Context(), connect.NewRequest(&ModuleMintWorkContextRequest{Prefix: testPrefix, ServiceAccountToken: "projected-service-account-token"})) if connect.CodeOf(err) != connect.CodeUnimplemented { // Connect's mapping of HTTP 404 t.Fatalf("a Connect call at the gateway: %v, want 404 (unimplemented)", err) } From 2a6450bf58cd9b6c6421d6c4c4a74d914438d11c Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:35:57 -0400 Subject: [PATCH 12/12] fix(moduleauthority): match the v0.0.94 projected-token broker --- CHANGELOG.md | 32 ++-- README.md | 36 ++++- moduleauthority/broker.go | 22 ++- moduleauthority/broker_test.go | 2 +- moduleauthority/client.go | 39 +++-- moduleauthority/host_fake_test.go | 45 +++--- moduleauthority/projected_token_test.go | 191 ++++++++++++++++++++++++ moduleauthority/source_context.go | 6 +- moduleauthority/transport.go | 4 +- 9 files changed, 330 insertions(+), 47 deletions(-) create mode 100644 moduleauthority/projected_token_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b061d88..663cd1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,8 +86,9 @@ and `ModuleMintSourceOperationContextRequest` lose `Secret`/`GetSecret` and gain `ServiceAccountToken`/`GetServiceAccountToken`. The first two requests also gain `OrganizationId` for shared-principal installation selection. - The gateway route-rejection fixture now uses the current request field; - the hand-written gateway broker transport is unchanged. + The gateway route-rejection fixture uses the current request field. The + hand-written broker sends the projected token as its sole Bearer credential, + and includes the organization selector on the first two REST mint bodies. - `moduleauthority.CurrentInstallation.SolutionIdentifier` is replaced by `TargetID` and `BindingID`, reflecting the host's immutable authority identities. The SDK validates the target UUID and nonempty binding. @@ -124,21 +125,32 @@ ## Unreleased — mint once: the projected ServiceAccount token replaces the module secret -- **Breaking.** `moduleauthority.Credentials` is `{Prefix, Token}`: `Token` is a - `ProjectedTokenSource` that reads the module pod's projected ServiceAccount +- **Breaking.** `moduleauthority.Credentials` is `{Prefix, Token, OrganizationID}`. + `Token` is a `ProjectedTokenSource` that reads the module pod's projected ServiceAccount token (audience `accounts`). `Secret` is deleted with the host's `MODULE_IDENTITY_SECRETS`. Every broker mint sends the token as `Authorization: Bearer`, read from its source before that mint (the kubelet rotates it); `X-Codefly-Module-Secret` is no longer sent. +- `OrganizationID` selects the installation for module Work Context and + operation-context mints. Shared modules must supply it; tenant-pinned modules + may omit it or name their own tenant. Use a separate client per organization + so its cached module Work Context stays scoped. Source-context mints send only + `prefix` and `source_id`; their delegation determines the organization. +- Every token source is trimmed and checked before dispatch: empty or oversized + (more than 64 KiB) tokens return `ErrInvalidCredentials` with zero broker calls. + The host requires audience `accounts` and lifetime at most 600 seconds. No + token path is defaulted. Tests cover file rotation and exact REST bodies on + all three mints; the host fake rejects missing, duplicate and non-Bearer + Authorization, and any legacy secret header as a regression guard. - Added `ProjectedTokenFile`, the file-backed source. Its method set is `sdk-go/workcontext`'s `ProjectedTokenSource`, so that implementation is accepted once this module moves off the pre-cutover `sdk-go/workcontext` pin. - Added `ErrExecutionUnestablished` for the broker's `503`: the host could not establish what the pod is running, nothing was decided, and a retry may succeed. `401` still maps to `ErrInvalidCredentials`. -- `gen/` is unchanged: the broker client speaks the broker's REST contract, not - the generated request types, so the regeneration waits for the host change to - land and is its own refresh. +- The broker client speaks the REST contract verified against + `gateway_modules.go` at released host tag `v0.0.94`; the generated request + types are consumed through the tagged API refresh above. ## Unreleased — module audit declarations and gateway authority @@ -231,7 +243,7 @@ version and to signal the breaking removal below. - the three mints go to the gateway's module broker as JSON (`POST /modules/_work-context`, `/modules/_operation-context`, `/modules/_source-operation-context`) with `X-Codefly-Internal-Token` and - `X-Codefly-Module-Secret`; + `Authorization: Bearer `; - `ExchangeOperation` calls accounts' `authority` gRPC endpoint with the module Work Context in `x-codefly-work-context` and the internal token in `x-codefly-internal-token`, and still refreshes a rejected module Work @@ -241,8 +253,8 @@ version and to signal the breaking removal below. token, gateway, gateway base URL or authority address, and on a plaintext seam to anything but `localhost` or a loopback IP unless the consumer sets `AllowInsecureHTTP` from its own mesh-protection assertion; and with - `ErrInvalidCredentials` on an empty prefix or secret. The broker client never - follows a redirect. The `connect.ClientOption` variadic is gone. + `ErrInvalidCredentials` on an empty prefix or missing token source. The broker + client never follows a redirect. The `connect.ClientOption` variadic is gone. Refusals change shape with the transport: a broker answer other than 200 is a `*BrokerError` (path, status, reason), wrapped with `ErrInvalidCredentials` diff --git a/README.md b/README.md index ef001e7..6db918e 100644 --- a/README.md +++ b/README.md @@ -225,9 +225,10 @@ The SDK surfaces are: `LookupPruneSourceOperationReceipts` are included in the generated bindings only; this caller facade does not expose receipt retention operations. - **`moduleauthority/`** — the module-principal side of installed operation - authority. A long-running module supplies its Codefly-projected registration - credential once; the client mints and refreshes the module's short-lived Work - Context, attaches it to the generic SaaS exchange, and returns the exchanged + authority. A long-running module supplies a source for its pod's projected + ServiceAccount token; the client rereads it before every mint and refreshes + the module's short-lived Work Context, attaches it to the generic SaaS + exchange, and returns the exchanged child as an opaque `sdk-go` token. The signed-in person's retained parent and the exchanged child remain request-local. @@ -257,6 +258,8 @@ The SDK surfaces are: Prefix: modulePrefix, // Read before every mint: the kubelet rotates the projection. Token: moduleauthority.ProjectedTokenFile(projectedTokenPath), + // Required for a shared module; a tenant-pinned module may omit it. + OrganizationID: organizationID, }) if err != nil { return err // ErrInvalidSeams / ErrInvalidCredentials: fail closed at startup @@ -276,6 +279,33 @@ The SDK surfaces are: `AllowInsecureHTTP` asserts the hop is mesh-protected. SaaS owns the installed binding's audience, scopes, lifetime, verification, and audit record. + `projectedTokenPath` is the token projection mount path supplied by the + consumer's deployment; there is no default path. The host requires audience + `accounts` and a token lifetime of at most 600 seconds, and verifies the pod, + workload and approved build. The SDK rereads the source for every actual mint + (including a module Work Context refresh), trims whitespace, and rejects an + empty token or one larger than 64 KiB with `ErrInvalidCredentials` before + sending. `ProjectedTokenFile` also bounds the file read to 64 KiB. Every mint + sends exactly one `Authorization: Bearer ` header alongside the + internal perimeter token; the removed module-secret credential has no fallback. + + The REST bodies match [the v0.0.94 gateway](https://github.com/codefly-dev/module-saas-starter/blob/v0.0.94/module/services/auth-gateway/code/gateway_modules.go): + + | Mint path | JSON fields sent | + | --- | --- | + | `/modules/_work-context` | `prefix`, optional `organization_id` | + | `/modules/_operation-context` | `prefix`, `binding`, optional `organization_id` | + | `/modules/_source-operation-context` | `prefix`, `source_id` | + + Set `Credentials.OrganizationID` for a shared module and use a separate client + per organization, since each client caches its own module Work Context. A + tenant-pinned module may omit the selector or name its own tenant. The host + checks the module envelope and live installation; selecting an organization + grants no authority. Source mints ignore this selector and derive their + organization from the source's delegation. Deployment and identity requirements + are in [MODULE_INSTALLATION.md](https://github.com/codefly-dev/module-saas-starter/blob/v0.0.94/module/MODULE_INSTALLATION.md) + and [Establishing what a caller is running](https://github.com/codefly-dev/module-saas-starter/blob/v0.0.94/module/services/accounts/AGENTS.md#establishing-what-a-caller-is-running). + `ExchangeRequest` presents authority one of two ways, and exactly one — both, or neither, is `ErrInvalidExchange` before anything is sent. `Parent` is a live capability the caller holds; the child is attenuated against it and expires diff --git a/moduleauthority/broker.go b/moduleauthority/broker.go index 3d53eea..dc8c647 100644 --- a/moduleauthority/broker.go +++ b/moduleauthority/broker.go @@ -35,6 +35,26 @@ const ( // answers are a handful of short strings. const maxBrokerResponse = 64 << 10 +// REST request bodies match gateway_modules.go at module-saas-starter v0.0.94 +// (lines 185–188, 273–277 and 427–431). Credentials travel only in headers. +type workContextRequest struct { + Prefix string `json:"prefix"` + OrganizationID string `json:"organization_id,omitempty"` +} + +type operationContextRequest struct { + Prefix string `json:"prefix"` + Binding string `json:"binding"` + OrganizationID string `json:"organization_id,omitempty"` +} + +type sourceOperationContextRequest struct { + // This facade selects the gateway's source_id arm; it never sends the + // mutually exclusive delegation_id or an organization selector. + Prefix string `json:"prefix"` + SourceID string `json:"source_id"` +} + type broker struct { baseURL string client *http.Client @@ -63,7 +83,7 @@ func newBroker(seams Seams) (broker, error) { // post sends one broker request and decodes a 200 answer into out. Any other // status is a *BrokerError, classified without reading its body. Transport // and response read failures are *BrokerTransportError, without a cause. -func (b broker) post(ctx context.Context, path, projectedToken string, body map[string]string, out any) error { +func (b broker) post(ctx context.Context, path, projectedToken string, body any, out any) error { payload, err := json.Marshal(body) if err != nil { return err diff --git a/moduleauthority/broker_test.go b/moduleauthority/broker_test.go index 3e48f2c..a464003 100644 --- a/moduleauthority/broker_test.go +++ b/moduleauthority/broker_test.go @@ -237,7 +237,7 @@ func TestMintsRejectIncompleteInputWithoutNetwork(t *testing.T) { } func TestTheBrokerClientNeverFollowsARedirect(t *testing.T) { - // A redirect would replay the internal token and the module secret to an + // A redirect would replay the internal token and the projected token to an // address the transport rule never judged. h := newHost(t) target := newFakeGateway(t) diff --git a/moduleauthority/client.go b/moduleauthority/client.go index 0b4b006..798d736 100644 --- a/moduleauthority/client.go +++ b/moduleauthority/client.go @@ -41,9 +41,10 @@ const defaultRefreshSkew = 30 * time.Second var ( // ErrInvalidCredentials: the module's credentials are missing or its - // projected token could not be read, or the broker refused to prove the - // module (HTTP 401 — a token the host refused, another workload's token, a - // pod running a build the host does not approve, or a wrong internal token; + // projected token could not be read, is empty or exceeds 64 KiB, or the + // broker refused to prove the module (HTTP 401 — a token the host refused, + // another workload's token, a pod running a build the host does not approve, + // or a wrong internal token; // the host does not say which). ErrInvalidCredentials = errors.New("module authority: invalid module credentials") // ErrExecutionUnestablished: the host could not establish what this pod is @@ -95,7 +96,7 @@ type Seams struct { // hops to a non-loopback address are protected out of band (every // in-cluster hop carried by a mutually authenticated mesh). It must come // from the consumer's own configured assertion — never inferred. Without - // it, the internal token and the module secret are sent only over https + // it, the internal token and the projected token are sent only over https // (or TLS) or to a loopback address, and New refuses anything else. AllowInsecureHTTP bool } @@ -120,6 +121,14 @@ type Authority struct { type Credentials struct { Prefix string Token ProjectedTokenSource + // OrganizationID selects the organization for module Work Context and + // operation-context mints. Required for a shared module; a tenant-pinned + // module may omit it or name its own tenant. The host checks the module's + // envelope and live installation; this selector grants no authority. + // Use a separate Client for each organization: its cached module Work + // Context belongs to that organization. Source-context mints ignore this + // field because the source's delegation determines their organization. + OrganizationID string } // ProjectedTokenSource reads the projected ServiceAccount token as it is NOW. @@ -127,6 +136,8 @@ type Credentials struct { // the projection under a running process. Its method set is // sdk-go/workcontext's ProjectedTokenSource's, so that implementation satisfies // it as written. +// Empty tokens and tokens larger than 64 KiB after trimming whitespace are +// refused with ErrInvalidCredentials before any broker request is sent. type ProjectedTokenSource interface { ProjectedToken() (string, error) } @@ -134,6 +145,8 @@ type ProjectedTokenSource interface { // ProjectedTokenFile reads the token from the path its projected volume is // mounted at, trimmed. An empty file is an error, never an empty token: a // projection mid-rotation must not be presented as a credential. +// The consumer supplies the mount path; there is no default. The file read +// is limited to 64 KiB, including surrounding whitespace. // // It is the same contract as sdk-go/workcontext's ProjectedTokenFile, which this // module cannot import while it pins the pre-cutover sdk-go/workcontext for @@ -177,9 +190,13 @@ func (c Credentials) projectedToken() (string, error) { if err != nil { return "", fmt.Errorf("%w: %w", ErrInvalidCredentials, err) } - if strings.TrimSpace(token) == "" { + token = strings.TrimSpace(token) + if token == "" { return "", fmt.Errorf("%w: projected token is empty", ErrInvalidCredentials) } + if len(token) > maxProjectedToken { + return "", fmt.Errorf("%w: projected token exceeds %d bytes", ErrInvalidCredentials, maxProjectedToken) + } return token, nil } @@ -333,9 +350,10 @@ func (c *Client) MintModuleOperationContext(ctx context.Context, bindingID strin return OperationContext{}, err } var issued operationContextResponse - if err := c.broker.post(ctx, operationContextPath, projected, map[string]string{ - "prefix": c.credentials.Prefix, - "binding": bindingID, + if err := c.broker.post(ctx, operationContextPath, projected, operationContextRequest{ + Prefix: c.credentials.Prefix, + Binding: bindingID, + OrganizationID: c.credentials.OrganizationID, }, &issued); err != nil { return OperationContext{}, fmt.Errorf("module authority: mint operation context: %w", brokerRefusal(err)) } @@ -358,8 +376,9 @@ func (c *Client) moduleWorkContextLocked(ctx context.Context) (codefly.WorkConte return codefly.WorkContextToken{}, err } var issued workContextResponse - if err := c.broker.post(ctx, workContextPath, projected, map[string]string{ - "prefix": c.credentials.Prefix, + if err := c.broker.post(ctx, workContextPath, projected, workContextRequest{ + Prefix: c.credentials.Prefix, + OrganizationID: c.credentials.OrganizationID, }, &issued); err != nil { return codefly.WorkContextToken{}, fmt.Errorf("module authority: mint module Work Context: %w", brokerRefusal(err)) } diff --git a/moduleauthority/host_fake_test.go b/moduleauthority/host_fake_test.go index 124e8ff..4b726b9 100644 --- a/moduleauthority/host_fake_test.go +++ b/moduleauthority/host_fake_test.go @@ -26,14 +26,14 @@ import ( "github.com/codefly-dev/saas-sdk-go/gen/saas/accounts/v1/accountsv1connect" ) -// This file fakes the two host seams exactly as module-saas-starter serves +// This file fakes the two host seams as module-saas-starter v0.0.94 serves // them, so the client is tested against the host's contract rather than // against itself: // // - fakeGateway is the auth-gateway's module broker // (module/services/auth-gateway/code/gateway_modules.go): the three -// POST /modules/_* paths, their perimeter (internal token + module -// secret header), body validation, status mapping, plain-text refusals and +// POST /modules/_* paths, their perimeter (internal token + projected +// ServiceAccount bearer), body validation, status mapping, plain-text refusals and // JSON answers. Every other path answers 404 "endpoint not exposed", as the // real gateway does for every EXPOSURE_INTERNAL procedure — the old bug was // a Connect call to exactly such a path. @@ -64,8 +64,7 @@ const ( var moduleCredentials = Credentials{Prefix: testPrefix, Token: staticToken(testProjectedToken)} -// staticToken is a ProjectedTokenSource that answers one value; rotatingToken -// answers the next of several on each read, the way a kubelet rotation does. +// staticToken is a test-only ProjectedTokenSource that answers one value. type staticToken string func (s staticToken) ProjectedToken() (string, error) { return string(s), nil } @@ -103,19 +102,21 @@ type fakeGateway struct { headers []http.Header // Behaviour knobs, per path. - refuse map[string]refusal - expires time.Time - binding string // overrides the binding answered by the operation mint - sourceID string // overrides the source answered by the source mint + refuse map[string]refusal + expires time.Time + binding string // overrides the binding answered by the operation mint + sourceID string // overrides the source answered by the source mint + projectedToken string // token currently accepted by the simulated accounts verifier } func newFakeGateway(t *testing.T) *fakeGateway { t.Helper() g := &fakeGateway{ - valid: map[string]bool{}, - bodies: map[string][]map[string]string{}, - refuse: map[string]refusal{}, - expires: time.Now().Add(time.Hour), + valid: map[string]bool{}, + bodies: map[string][]map[string]string{}, + refuse: map[string]refusal{}, + expires: time.Now().Add(time.Hour), + projectedToken: testProjectedToken, } g.server = httptest.NewServer(http.HandlerFunc(g.serve)) t.Cleanup(g.server.Close) @@ -167,14 +168,24 @@ func (g *fakeGateway) serve(w http.ResponseWriter, r *http.Request) { plainError(w, http.StatusUnauthorized, "unauthorized") return } - // Exactly one bearer, as the real broker reads it; a module secret header - // is not a credential the host reads any more. + // The real gateway no longer reads the legacy header. Refuse even its + // presence here as a stricter regression guard against sending it again. + if len(r.Header.Values("X-Codefly-Module-Secret")) != 0 { + plainError(w, http.StatusUnauthorized, "unauthorized") + return + } + // Exactly one Bearer token, trimmed and bounded as gateway_modules.go + // lines 112–122 at v0.0.94 (the literal limit is independent of the SDK). authorization := r.Header.Values("Authorization") if len(authorization) != 1 || !strings.HasPrefix(authorization[0], "Bearer ") { plainError(w, http.StatusUnauthorized, "unauthorized") return } - projected := strings.TrimPrefix(authorization[0], "Bearer ") + projected := strings.TrimSpace(strings.TrimPrefix(authorization[0], "Bearer ")) + if projected == "" || len(projected) > 64*1024 { + plainError(w, http.StatusUnauthorized, "unauthorized") + return + } var payload map[string]string if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&payload); err != nil { plainError(w, http.StatusBadRequest, "invalid json") @@ -186,7 +197,7 @@ func (g *fakeGateway) serve(w http.ResponseWriter, r *http.Request) { return } // accounts' verdict on the token: the gateway maps it per path. - if payload["prefix"] != testPrefix || projected != testProjectedToken { + if payload["prefix"] != testPrefix || projected != g.projectedToken { plainError(w, http.StatusUnauthorized, "unauthorized") return } diff --git a/moduleauthority/projected_token_test.go b/moduleauthority/projected_token_test.go new file mode 100644 index 0000000..01a5f0c --- /dev/null +++ b/moduleauthority/projected_token_test.go @@ -0,0 +1,191 @@ +package moduleauthority + +import ( + "errors" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" +) + +func TestEveryMintRejectsInvalidProjectedTokensBeforeDispatch(t *testing.T) { + for _, mint := range brokerMints(t) { + for _, tc := range []struct { + name string + source ProjectedTokenSource + }{ + {"empty", staticToken("")}, + {"whitespace", staticToken(" \t\r\n")}, + {"oversized", staticToken(strings.Repeat("x", 64*1024+1))}, + {"unreadable", failingToken{errors.New("projection unavailable")}}, + } { + t.Run(mint.path+"/"+tc.name, func(t *testing.T) { + h := newHost(t) + client := h.client(t, Credentials{Prefix: testPrefix, Token: tc.source}) + if err := mint.call(client); !errors.Is(err, ErrInvalidCredentials) { + t.Fatalf("error = %v, want ErrInvalidCredentials", err) + } + if calls, _, _, _ := h.gateway.snapshot(); len(calls) != 0 { + t.Fatalf("invalid token reached the broker: %v", calls) + } + }) + } + } +} + +func TestEveryMintAcceptsTheHostTokenSizeBoundary(t *testing.T) { + for _, mint := range brokerMints(t) { + t.Run(mint.path, func(t *testing.T) { + h := newHost(t) + token := strings.Repeat("x", 64*1024) + h.gateway.projectedToken = token + client := h.client(t, Credentials{Prefix: testPrefix, Token: staticToken(" \t" + token + "\n")}) + if err := mint.call(client); err != nil { + t.Fatal(err) + } + _, _, _, headers := h.gateway.snapshot() + assertProjectedBearer(t, headers[0], token) + }) + } +} + +// Model a kubelet rotation with a replaced file, after client construction +// and between actual mints. Module Work Contexts inside the refresh window +// must reread the projection too; a cached Work Context itself needs no mint. +func TestEveryMintReadsTheRotatedProjectedFile(t *testing.T) { + for _, mint := range brokerMints(t) { + t.Run(mint.path, func(t *testing.T) { + h := newHost(t) + h.gateway.expires = time.Now().Add(20 * time.Second) + path := filepath.Join(t.TempDir(), "projected-token") + client := h.client(t, Credentials{Prefix: testPrefix, Token: ProjectedTokenFile(path)}) + for _, token := range []string{"first-projection", "rotated-projection"} { + if err := os.WriteFile(path+".next", []byte(token+"\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Rename(path+".next", path); err != nil { + t.Fatal(err) + } + h.gateway.mu.Lock() + h.gateway.projectedToken = token + h.gateway.mu.Unlock() + if err := mint.call(client); err != nil { + t.Fatal(err) + } + } + calls, _, _, headers := h.gateway.snapshot() + if len(calls) != 2 || calls[0] != mint.path || calls[1] != mint.path { + t.Fatalf("broker calls = %v, want two mints", calls) + } + assertProjectedBearer(t, headers[0], "first-projection") + assertProjectedBearer(t, headers[1], "rotated-projection") + }) + } +} + +func assertProjectedBearer(t *testing.T, header http.Header, token string) { + t.Helper() + if !reflect.DeepEqual(header.Values("Authorization"), []string{"Bearer " + token}) { + t.Fatal("mint must send exactly one Authorization containing the current projected bearer") + } + if len(header.Values("X-Codefly-Module-Secret")) != 0 { + t.Fatal("mint sent the removed module-secret header") + } +} + +func TestMintBodiesMatchTaggedGateway(t *testing.T) { + for _, org := range []string{"", testTenant} { + for _, mint := range brokerMints(t) { + t.Run(mint.path+"/org="+org, func(t *testing.T) { + h := newHost(t) + client := h.client(t, Credentials{Prefix: testPrefix, Token: staticToken(testProjectedToken), OrganizationID: org}) + if err := mint.call(client); err != nil { + t.Fatal(err) + } + want := map[string]string{"prefix": testPrefix} + switch mint.path { + case hostWorkContextPath: + if org != "" { + want["organization_id"] = org + } + case hostOperationContextPath: + want["binding"] = "binding" + if org != "" { + want["organization_id"] = org + } + case hostSourceOperationContextPath: + want["source_id"] = exampleSource + } + _, _, bodies, headers := h.gateway.snapshot() + if got := bodies[mint.path]; len(got) != 1 || !reflect.DeepEqual(got[0], want) { + t.Fatalf("body = %v, want exactly %v", got, want) + } + assertProjectedBearer(t, headers[0], testProjectedToken) + }) + } + } +} + +func TestHostFakeRefusesRemovedOrMalformedCredentials(t *testing.T) { + for _, mint := range brokerMints(t) { + for _, tc := range []struct { + name string + authorization []string + legacy bool + acceptedToken string + }{ + {name: "missing"}, + {name: "duplicate", authorization: []string{"Bearer " + testProjectedToken, "Bearer " + testProjectedToken}}, + {name: "non bearer", authorization: []string{"Basic " + testProjectedToken}}, + {name: "wrong case", authorization: []string{"bearer " + testProjectedToken}}, + {name: "empty", authorization: []string{"Bearer "}}, + {name: "whitespace", authorization: []string{"Bearer \t"}}, + {name: "oversized", authorization: []string{"Bearer " + strings.Repeat("x", 64*1024+1)}, acceptedToken: strings.Repeat("x", 64*1024+1)}, + {name: "secret only", legacy: true}, + {name: "dual header", authorization: []string{"Bearer " + testProjectedToken}, legacy: true, acceptedToken: testProjectedToken}, + } { + t.Run(mint.path+"/"+tc.name, func(t *testing.T) { + g := newFakeGateway(t) + g.projectedToken = tc.acceptedToken + request := httptest.NewRequest(http.MethodPost, mint.path, strings.NewReader(`{"prefix":"documents","binding":"binding","source_id":"`+exampleSource+`"}`)) + request.Header.Set("X-Codefly-Internal-Token", testInternalToken) + for _, value := range tc.authorization { + request.Header.Add("Authorization", value) + } + if tc.legacy { + request.Header.Set("X-Codefly-Module-Secret", "removed-credential") + } + response := httptest.NewRecorder() + g.serve(response, request) + if response.Code != http.StatusUnauthorized { + t.Fatalf("status = %d, want 401", response.Code) + } + _, _, bodies, _ := g.snapshot() + if len(bodies) != 0 { + t.Fatal("malformed credential passed gateway authentication and reached body decoding") + } + }) + } + } +} + +func TestProjectedTokenFileBoundsItsRead(t *testing.T) { + path := filepath.Join(t.TempDir(), "token") + for _, size := range []int{64 * 1024, 64*1024 + 1} { + if err := os.WriteFile(path, []byte(strings.Repeat("x", size)), 0o600); err != nil { + t.Fatal(err) + } + token, err := ProjectedTokenFile(path).ProjectedToken() + if size == 64*1024 { + if err != nil || len(token) != size { + t.Fatalf("bounded token: length %d, error %v", len(token), err) + } + } else if err == nil || token != "" { + t.Fatal("oversized file must not yield a token") + } + } +} diff --git a/moduleauthority/source_context.go b/moduleauthority/source_context.go index 2bf9284..eb675ed 100644 --- a/moduleauthority/source_context.go +++ b/moduleauthority/source_context.go @@ -74,9 +74,9 @@ func (c *Client) MintSourceOperationContext(ctx context.Context, sourceID string return SourceOperationContext{}, err } var issued sourceOperationContextResponse - if err := c.broker.post(ctx, sourceOperationContextPath, projected, map[string]string{ - "prefix": c.credentials.Prefix, - "source_id": sourceID, + if err := c.broker.post(ctx, sourceOperationContextPath, projected, sourceOperationContextRequest{ + Prefix: c.credentials.Prefix, + SourceID: sourceID, }, &issued); err != nil { if refusal := delegationRefusal(err); refusal != nil { return SourceOperationContext{}, fmt.Errorf("%w: %w", refusal, err) diff --git a/moduleauthority/transport.go b/moduleauthority/transport.go index b386ef2..4234475 100644 --- a/moduleauthority/transport.go +++ b/moduleauthority/transport.go @@ -7,7 +7,7 @@ import ( "strings" ) -// The transport rule, in one place: the internal token and the module secret +// The transport rule, in one place: the internal token and the projected token // travel only over TLS, or in plaintext to a loopback address, or in plaintext // to any address the consumer explicitly asserted protected // (Seams.AllowInsecureHTTP). Loopback is the literal name "localhost" or a @@ -30,7 +30,7 @@ func gatewayBaseURL(raw string, allowInsecureHTTP bool) (string, error) { case "https": case "http": if !isLoopback(parsed.Hostname()) && !allowInsecureHTTP { - return "", fmt.Errorf("%w: the gateway base URL %q is plaintext to a non-loopback host; the broker is sent the internal token and the module secret, so use https, or assert the hop is mesh-protected with AllowInsecureHTTP", ErrInvalidSeams, raw) + return "", fmt.Errorf("%w: the gateway base URL %q is plaintext to a non-loopback host; the broker is sent the internal token and the projected token, so use https, or assert the hop is mesh-protected with AllowInsecureHTTP", ErrInvalidSeams, raw) } default: return "", fmt.Errorf("%w: the gateway base URL %q must be http or https", ErrInvalidSeams, raw)