From 4cd51caaeb30e430a97f501a1fd95f6b41809fd1 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:46:36 -0400 Subject: [PATCH 01/45] fix: preserve delegation limits and revoke issued authority Preserve resource IDs across delegation state and RPCs, reject fixture matcher syntax, invalidate existing capabilities on revoke, and recheck concurrent issuance through core. Keep the declared hop HTTP listener available in gRPC-only compositions. Update README and shared conformance coverage. --- README.md | 35 ++++ libraries/conformance/go/conformance.go | 3 + .../issuer/delegation_revocation_test.go | 193 ++++++++++++++++++ .../code/pkg/business/issuer/issuer.go | 21 +- .../business/state/delegation_fixture_test.go | 43 ++++ .../gateway/code/pkg/business/state/state.go | 56 ++++- .../authority/delegation_resources_test.go | 94 +++++++++ .../gateway/code/plugins/authority/plugin.go | 6 +- modules/authority/services/hop/code/work.go | 11 +- 9 files changed, 445 insertions(+), 17 deletions(-) create mode 100644 modules/authority/services/gateway/code/pkg/business/issuer/delegation_revocation_test.go create mode 100644 modules/authority/services/gateway/code/pkg/business/state/delegation_fixture_test.go create mode 100644 modules/authority/services/gateway/code/plugins/authority/delegation_resources_test.go diff --git a/README.md b/README.md index f36cf3c6..7cdf1b85 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,11 @@ caller's Work Context, asks the interface's `Check` with the module's principal as the audience, removes any stamp the caller sent, writes its own stamp, and forwards the call as opaque frames. It knows no method of the module. +The hop always listens on its declared HTTP endpoint, including in a +gRPC-only composition. An empty HTTP routing table does not remove that +endpoint from Codefly's readiness checks. Requests still pass through the +authority decision and are refused when no corresponding HTTP upstream exists. + The module behind it carries one interceptor from `libraries/hop/go` and no other authority code. The stamp binds the digest of the Work Context the hop checked, so a module that needs the full claims decodes that exact token @@ -616,6 +621,36 @@ file fetch) report themselves unwired and idle, and ingest is off because no runtime is composed. The fixture's collection grants do not yet use the store's real namespace spelling. +## Delegated resource restrictions + +A delegation preserves the exact resource IDs supplied in its WorkScopes. +Omitted IDs grant the whole kind, while named IDs remain a ceiling even when +the grantor and operation binding hold broader authority. Delegate, +GetDelegation, Issue and RevokeDelegation preserve that ceiling; requesting an +ID outside it is refused. + +Development fixtures use `resource_ids` on delegation scopes. The +`resources` matchers used by roles (IDs, prefixes and patterns) are a separate +contract and are rejected on delegation scopes rather than silently ignored. +The generated gRPC client regression covers the complete delegation lifecycle; +the fixture regression checks exact IDs and matcher-syntax refusal. These are +hermetic gateway checks, not full consumer or SaaS-provider qualification. + +Revoking a delegation advances the organization's authorization revision in the +same state update. Previously issued capabilities, including descendants, then +fail the ordinary core verifier. This is deliberately conservative: outstanding +capabilities throughout that organization are invalidated, while other +organizations are unaffected. Ordinary authorized callers recover by requesting +fresh authority; the revoked delegation cannot issue again. Repeated, unknown +or unauthorized revocations do not advance the revision. + +Issuance rereads the delegation after signing, and parent exchanges recheck the +original parent after signing, to prevent concurrent revocation from being +refreshed into new authority. Regressions cover existing-token checks, resource +discovery, descendants, tenant isolation and deterministic issuance interleavings. +This does not qualify the separate cross-audience attenuation or durable custody +contracts. + ## Every saas module-authority feature, and where it lives now (2026-10-23) A module's saas client (`saas-sdk-go/moduleauthority`) is replaced by this diff --git a/libraries/conformance/go/conformance.go b/libraries/conformance/go/conformance.go index 42475f77..3c0070c4 100644 --- a/libraries/conformance/go/conformance.go +++ b/libraries/conformance/go/conformance.go @@ -671,6 +671,9 @@ func Run(t *testing.T, conn grpc.ClientConnInterface, descriptors []byte, f Fixt t.Fatalf("anyone else learns the delegation exists: %v", err) } c.must(t, as(ctx, grantor), "host.authority.v1.AuthorityService", "RevokeDelegation", map[string]any{"delegation_id": id}) + if _, err := c.call(ctx, "host.authority.v1.AuthorityService", "Check", map[string]any{"token": str(out, "token"), "audience": dc.Audience}); status.Code(err) != codes.PermissionDenied { + t.Fatalf("authority issued before delegation revocation still checks: %v", err) + } if _, err := issue(); status.Code(err) != codes.PermissionDenied { t.Fatalf("a revoked delegation issues: %v", err) } diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/delegation_revocation_test.go b/modules/authority/services/gateway/code/pkg/business/issuer/delegation_revocation_test.go new file mode 100644 index 00000000..1d7e07a7 --- /dev/null +++ b/modules/authority/services/gateway/code/pkg/business/issuer/delegation_revocation_test.go @@ -0,0 +1,193 @@ +package issuer_test + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "errors" + "fmt" + "gateway/pkg/business/issuer" + "testing" + "time" + + "gateway/pkg/business/testkit" + authorityv1 "gateway/pkg/gen/host/authority/v1" + "github.com/codefly-dev/core/workcontext" +) + +func TestDelegationRevocationInvalidatesExistingUsesAndPreservesOtherTenants(t *testing.T) { + k := testkit.New(t) + ctx := context.Background() + grantor := person(t, k, "fixture-remi") + d, err := k.Issuer.Delegate(grantor, "org-directory", + testkit.OrgDirectory(1).Bindings["documents"].InvokeScopes, "bounded work") + if err != nil { + t.Fatal(err) + } + delegated, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}}, + Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "org-directory")}, + Audience: "documents", OperationBindingId: "documents", + }) + if err != nil { + t.Fatal(err) + } + other, err := k.Issuer.Issue(person(t, k, "fixture-zoe"), &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", + }) + if err != nil { + t.Fatal(err) + } + descendant, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_ParentToken{ParentToken: delegated.GetToken()}, + Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "documents")}, + Audience: "model", OperationBindingId: "model", + }) + if err != nil { + t.Fatal(err) + } + uses := map[string]func() error{ + "existing descendant": func() error { _, err := k.Issuer.Check(ctx, descendant.GetToken(), "model", ""); return err }, + "check": func() error { _, err := k.Issuer.Check(ctx, delegated.GetToken(), "documents", ""); return err }, + "resources": func() error { + _, err := k.Issuer.ListResources(ctx, delegated.GetToken(), "documents", "documents", "read") + return err + }, + "child": func() error { + _, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_ParentToken{ParentToken: delegated.GetToken()}, + Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "documents")}, + Audience: "model", OperationBindingId: "model", + }) + return err + }, + } + for name, use := range uses { + if err := use(); err != nil { + t.Fatalf("%s before revocation: %v", name, err) + } + } + if _, err := k.Issuer.RevokeDelegation(grantor, d.ID); err != nil { + t.Fatal(err) + } + for name, use := range uses { + if err := use(); !errors.Is(err, workcontext.ErrRevoked) { + t.Errorf("%s after revocation: %v", name, err) + } + } + if _, err := k.Issuer.Check(ctx, other.GetToken(), "documents", ""); err != nil { + t.Fatalf("another tenant lost authority: %v", err) + } + fresh, err := k.Issuer.Issue(grantor, &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", + }) + if err != nil { + t.Fatal(err) + } + if _, err := k.Issuer.RevokeDelegation(grantor, d.ID); err != nil { + t.Fatal(err) + } + if _, err := k.Issuer.Check(ctx, fresh.GetToken(), "documents", ""); err != nil { + t.Fatalf("repeating an already-complete revocation invalidated fresh authority: %v", err) + } +} + +// Exercise revocation at every clock boundary of issuance. The clock remains +// fixed; only the interleaving changes. A refusal or a token rejected by the +// ordinary verifier is safe, but refreshed authority after revocation is not. +func TestRevocationDuringIssuanceCannotRefreshAuthority(t *testing.T) { + for _, child := range []bool{false, true} { + name := "delegation" + if child { + name = "parent" + } + t.Run(name, func(t *testing.T) { + run := func(t *testing.T, revokeAt int) int { + k := testkit.New(t) + _, key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + ticks, armed := 0, false + var delegationID string + grantor := person(t, k, "fixture-remi") + k.Issuer = issuer.New(k.State, testkit.Audience, "race-test", key, func() time.Time { + if armed { + ticks++ + if ticks == revokeAt { + if _, err := k.State.RevokeDelegation("remi", delegationID); err != nil { + t.Fatal(err) + } + } + } + return k.Now + }) + d, err := k.Issuer.Delegate(grantor, "org-directory", testkit.OrgDirectory(1).Bindings["documents"].InvokeScopes, "race test") + if err != nil { + t.Fatal(err) + } + delegationID = d.ID + req := &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}}, + Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "org-directory")}, + Audience: "documents", OperationBindingId: "documents", + } + if child { + parent, err := k.Issuer.Issue(context.Background(), req) + if err != nil { + t.Fatal(err) + } + req = &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_ParentToken{ParentToken: parent.GetToken()}, + Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, "documents")}, + Audience: "model", OperationBindingId: "model", + } + } + armed = true + out, issueErr := k.Issuer.Issue(context.Background(), req) + armed = false + if revokeAt == 0 { + if issueErr != nil { + t.Fatal(issueErr) + } + } else { + if ticks < revokeAt { + t.Fatalf("revocation point %d not reached (%d)", revokeAt, ticks) + } + if issueErr == nil { + if _, err := k.Issuer.Check(context.Background(), out.GetToken(), req.Audience, ""); !errors.Is(err, workcontext.ErrRevoked) { + t.Fatalf("issuance refreshed revoked authority at clock boundary %d: %v", revokeAt, err) + } + } + } + return ticks + } + count := run(t, 0) + for tick := 1; tick <= count; tick++ { + t.Run(fmt.Sprintf("clock-%d", tick), func(t *testing.T) { run(t, tick) }) + } + }) + } +} + +func TestFailedRevocationDoesNotInvalidateExistingAuthority(t *testing.T) { + k := testkit.New(t) + grantor := person(t, k, "fixture-remi") + d, err := k.Issuer.Delegate(grantor, "org-directory", testkit.OrgDirectory(1).Bindings["documents"].InvokeScopes, "failure isolation") + if err != nil { + t.Fatal(err) + } + before, err := k.State.AuthorizationRevision(context.Background(), d.Organization) + if err != nil { + t.Fatal(err) + } + for _, attempt := range []struct{ person, id string }{{"maya", d.ID}, {"remi", "unknown-delegation"}} { + if _, err := k.State.RevokeDelegation(attempt.person, attempt.id); err == nil { + t.Fatal("invalid revocation succeeded") + } + after, err := k.State.AuthorizationRevision(context.Background(), d.Organization) + if err != nil || after != before { + t.Fatalf("failed revocation changed revision: %d -> %d (%v)", before, after, err) + } + } +} diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go b/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go index 1faccb98..af25ecf0 100644 --- a/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go +++ b/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go @@ -424,6 +424,11 @@ func (i *Issuer) child(ctx context.Context, parent, actorCredential, bindingID, return "", nil, err } if n == len(hops) { + // Reconstruction must not refresh a parent across revocation. Check + // the original capability after the final child has been signed. + if err := i.verifier(claims.GetAudience()).Recheck(ctx, verified); err != nil { + return "", nil, err + } return token, signed, nil } } @@ -506,11 +511,25 @@ func (i *Issuer) forDelegation(ctx context.Context, id, actorCredential, binding if err != nil { return "", nil, fmt.Errorf("verify the capability a delegation's hop is derived from: %w", err) } - return i.authority.Child(ctx, current, workcontext.ChildInput{ + token, signed, err := i.authority.Child(ctx, current, workcontext.ChildInput{ PrincipalID: actor.Principal, PrincipalKind: "service", DelegationID: d.ID, GrantedScopes: scopes, Audience: audience.Principal, TTL: ttl, ReplayPolicy: workcontext.ReplayIdempotent, OperationBindingID: d.Organization + "/" + actor.Module + "/" + bindingID, }) + if err != nil { + return "", nil, err + } + // Start may have observed a newer revision than the initial delegation + // read. Recheck after signing so that revocation cannot be laundered into + // a fresh capability; a later revocation invalidates its signed revision. + d, err = i.state.DelegationByID(id) + if err != nil { + return "", nil, err + } + if d.Revoked { + return "", nil, fmt.Errorf("%w: delegation %s is revoked", ErrRefused, id) + } + return token, signed, nil } // Delegate records the signed-in person's grant to a module. diff --git a/modules/authority/services/gateway/code/pkg/business/state/delegation_fixture_test.go b/modules/authority/services/gateway/code/pkg/business/state/delegation_fixture_test.go new file mode 100644 index 00000000..26ce8df6 --- /dev/null +++ b/modules/authority/services/gateway/code/pkg/business/state/delegation_fixture_test.go @@ -0,0 +1,43 @@ +package state_test + +import ( + "context" + "slices" + "strings" + "testing" + + "gateway/pkg/business/testkit" +) + +func TestDelegationFixtureRetainsExactIDsAndRefusesRoleMatchers(t *testing.T) { + for _, selection := range []string{ + "resource_ids: [a]", + "resources: [{id: a}]", + "resources: [{prefix: a}]", + "resources: []", + "<<: {resources: [{id: a}]}", + } { + t.Run(selection, func(t *testing.T) { + k := testkit.New(t) + fixture := "delegations:\n - id: narrow\n organization: acme\n grantor: remi\n module: org-directory\n scopes:\n - kind: documents\n actions: [write]\n " + selection + "\n" + err := k.State.LoadFixture(context.Background(), []byte(fixture)) + if !strings.HasPrefix(selection, "resource_ids:") { + if err == nil || !strings.Contains(err.Error(), "resource_ids") { + t.Fatalf("unsupported matcher syntax must refuse rather than widen: %v", err) + } + return + } + if err != nil { + t.Fatal(err) + } + d, err := k.State.DelegationByID("narrow") + if err != nil { + t.Fatal(err) + } + scopes := d.WorkScopes() + if len(scopes) != 1 || !slices.Equal(scopes[0].GetResourceIds(), []string{"a"}) { + t.Fatalf("fixture restriction lost: %v", scopes) + } + }) + } +} diff --git a/modules/authority/services/gateway/code/pkg/business/state/state.go b/modules/authority/services/gateway/code/pkg/business/state/state.go index 2e2ce89e..66aeb5cc 100644 --- a/modules/authority/services/gateway/code/pkg/business/state/state.go +++ b/modules/authority/services/gateway/code/pkg/business/state/state.go @@ -80,24 +80,53 @@ type Binding struct { LookupScopes []*basev0.WorkScopeV1 } +// DelegationScope preserves the exact resource ceiling of a WorkScope. +// Role matchers are a separate contract; a delegation names IDs, not patterns. +type DelegationScope struct { + Kind string `yaml:"kind"` + Actions []string `yaml:"actions"` + ResourceIDs []string `yaml:"resource_ids,omitempty"` +} + +// Refuse role-matcher syntax instead of silently decoding a restricted grant +// as an unrestricted one. Fixture decoding otherwise permits unknown fields. +func (s *DelegationScope) UnmarshalYAML(node *yaml.Node) error { + type plain DelegationScope + var decoded struct { + plain `yaml:",inline"` + Resources yaml.Node `yaml:"resources"` + } + if err := node.Decode(&decoded); err != nil { + return err + } + if decoded.Resources.Kind != 0 { + return errors.New("delegation scopes use resource_ids, not role resources matchers") + } + *s = DelegationScope(decoded.plain) + return nil +} + // Delegation is a person's grant to a module to act for them beyond any // session, on scopes they hold. Every capability issued from it is the // grantor's, re-checked against what the grantor holds at the time. type Delegation struct { - ID string `yaml:"id"` - Organization string `yaml:"organization"` - Grantor string `yaml:"grantor"` - Module string `yaml:"module"` - Purpose string `yaml:"purpose"` - Scopes []Scope `yaml:"scopes"` - Revoked bool `yaml:"-"` + ID string `yaml:"id"` + Organization string `yaml:"organization"` + Grantor string `yaml:"grantor"` + Module string `yaml:"module"` + Purpose string `yaml:"purpose"` + Scopes []DelegationScope `yaml:"scopes"` + Revoked bool `yaml:"-"` } // WorkScopes is the delegation's scopes as core spells them. func (d Delegation) WorkScopes() []*basev0.WorkScopeV1 { var out []*basev0.WorkScopeV1 for _, sc := range d.Scopes { - out = append(out, &basev0.WorkScopeV1{ResourceKind: sc.Kind, Actions: append([]string(nil), sc.Actions...)}) + out = append(out, &basev0.WorkScopeV1{ + ResourceKind: sc.Kind, Actions: append([]string(nil), sc.Actions...), + ResourceIds: append([]string(nil), sc.ResourceIDs...), + }) } return Normalize(out) } @@ -643,7 +672,10 @@ func (s *State) Delegate(person, module string, scopes []*basev0.WorkScopeV1, pu } d := Delegation{ID: newDelegationID(), Organization: p.Organization, Grantor: person, Module: m.Module, Purpose: purpose} for _, sc := range Normalize(scopes) { - d.Scopes = append(d.Scopes, Scope{Kind: sc.GetResourceKind(), Actions: sc.GetActions()}) + d.Scopes = append(d.Scopes, DelegationScope{ + Kind: sc.GetResourceKind(), Actions: append([]string(nil), sc.GetActions()...), + ResourceIDs: append([]string(nil), sc.GetResourceIds()...), + }) } s.mu.Lock() s.delegations[d.ID] = d @@ -662,8 +694,14 @@ func (s *State) RevokeDelegation(person, id string) (Delegation, error) { if d.Grantor != person { return Delegation{}, fmt.Errorf("%w: only the grantor of %s may revoke it", ErrForbidden, id) } + if d.Revoked { + return d, nil + } d.Revoked = true s.delegations[id] = d + // Core checks this revision on every effect-time verification. Revoking + // the grant must also invalidate capabilities already issued from it. + s.revisions[d.Organization]++ return d, nil } diff --git a/modules/authority/services/gateway/code/plugins/authority/delegation_resources_test.go b/modules/authority/services/gateway/code/plugins/authority/delegation_resources_test.go new file mode 100644 index 00000000..b1ec880f --- /dev/null +++ b/modules/authority/services/gateway/code/plugins/authority/delegation_resources_test.go @@ -0,0 +1,94 @@ +package authority_test + +import ( + "context" + "net" + "slices" + "testing" + + "gateway/pkg/business/testkit" + authorityv1 "gateway/pkg/gen/host/authority/v1" + authorityplugin "gateway/plugins/authority" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" +) + +// A broad person and binding cannot expand a resource-limited delegation. +// Exercise the generated RPC client so response projection cannot lose IDs. +func TestDelegationKeepsResourceIDsAcrossItsLifecycle(t *testing.T) { + k := testkit.New(t) + server := grpc.NewServer() + (&authorityplugin.Plugin{Issuer: k.Issuer}).RegisterGRPC(server) + listener := bufconn.Listen(1 << 20) + go func() { _ = server.Serve(listener) }() + t.Cleanup(server.Stop) + conn, err := grpc.NewClient("passthrough:///bufconn", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = conn.Close() }) + client := authorityv1.NewAuthorityServiceClient(conn) + session, err := k.State.SignIn("fixture-remi") + if err != nil { + t.Fatal(err) + } + person := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorization", "Bearer "+session.ID)) + granted, err := client.Delegate(person, &authorityv1.DelegateRequest{ + Module: "org-directory", Purpose: "bounded work", + Scopes: []*authorityv1.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"write"}, ResourceIds: []string{"a"}}}, + }) + if err != nil { + t.Fatal(err) + } + check := func(scopes []*authorityv1.WorkScopeV1) { + t.Helper() + if len(scopes) != 1 || scopes[0].GetResourceKind() != "documents" || + !slices.Equal(scopes[0].GetActions(), []string{"write"}) || + !slices.Equal(scopes[0].GetResourceIds(), []string{"a"}) { + t.Errorf("delegation widened: %v", scopes) + } + } + check(granted.GetScopes()) + id := granted.GetDelegationId() + actor := &authorityv1.WorkloadCredential{Credential: k.Credential(t, "org-directory")} + read, err := client.GetDelegation(context.Background(), &authorityv1.GetDelegationRequest{DelegationId: id, Actor: actor}) + if err != nil { + t.Fatal(err) + } + check(read.GetScopes()) + request := &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: id}}, + Actor: actor, Audience: "documents", OperationBindingId: "documents", + } + issued, err := client.Issue(context.Background(), request) + if err != nil { + t.Fatal(err) + } + check(issued.GetContext().GetAuthorityScopes()) + checked, err := client.Check(context.Background(), &authorityv1.CheckRequest{Token: issued.GetToken(), Audience: "documents"}) + if err != nil { + t.Fatal(err) + } + check(checked.GetContext().GetAuthorityScopes()) + request.Scopes = []*authorityv1.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"write"}, ResourceIds: []string{"b"}}} + if _, err := client.Issue(context.Background(), request); status.Code(err) != codes.PermissionDenied { + t.Errorf("request outside delegated IDs must be refused: %v", err) + } + revoked, err := client.RevokeDelegation(person, &authorityv1.RevokeDelegationRequest{DelegationId: id}) + if err != nil { + t.Fatal(err) + } + check(revoked.GetDelegation().GetScopes()) + if revoked.GetDelegation().GetActive() { + t.Error("revoked delegation still active") + } + if _, err := client.Check(context.Background(), &authorityv1.CheckRequest{Token: issued.GetToken(), Audience: "documents"}); status.Code(err) != codes.PermissionDenied { + t.Errorf("previously issued authority must be refused after revocation: %v", err) + } +} diff --git a/modules/authority/services/gateway/code/plugins/authority/plugin.go b/modules/authority/services/gateway/code/plugins/authority/plugin.go index d8cef500..7fb909c8 100644 --- a/modules/authority/services/gateway/code/plugins/authority/plugin.go +++ b/modules/authority/services/gateway/code/plugins/authority/plugin.go @@ -135,8 +135,10 @@ func (p *Plugin) GetDelegation(ctx context.Context, req *authorityv1.GetDelegati func delegationOf(d state.Delegation) *authorityv1.Delegation { out := &authorityv1.Delegation{DelegationId: d.ID, OrganizationId: d.Organization, Grantor: d.Grantor, Module: d.Module, Purpose: d.Purpose, Active: !d.Revoked} - for _, sc := range d.Scopes { - out.Scopes = append(out.Scopes, &authorityv1.WorkScopeV1{ResourceKind: sc.Kind, Actions: sc.Actions}) + for _, sc := range d.WorkScopes() { + out.Scopes = append(out.Scopes, &authorityv1.WorkScopeV1{ + ResourceKind: sc.GetResourceKind(), Actions: sc.GetActions(), ResourceIds: sc.GetResourceIds(), + }) } return out } diff --git a/modules/authority/services/hop/code/work.go b/modules/authority/services/hop/code/work.go index 4539d1f4..9f1da14d 100644 --- a/modules/authority/services/hop/code/work.go +++ b/modules/authority/services/hop/code/work.go @@ -89,11 +89,12 @@ func standInFront(ctx context.Context, config *adapters.Configuration) (Clean, e } p := &proxy.Proxy{Decide: decision, Upstreams: upstreams, Proof: proof, Refused: refused} config.GRPCServerOptions = append(config.GRPCServerOptions, p.ServerOptions()...) - stopWeb := func() {} - if len(webs)+len(connects) > 0 { - if stopWeb, err = serveHTTP(ctx, &proxy.HTTP{Decide: decision, Upstreams: webs, Connect: connects, Proof: proof, Refused: refused}); err != nil { - return nil, err - } + // The service declares this endpoint even when every upstream uses gRPC. + // Keep it listening so the declared readiness contract holds; the proxy + // still refuses requests for which no authorized HTTP route exists. + stopWeb, err := serveHTTP(ctx, &proxy.HTTP{Decide: decision, Upstreams: webs, Connect: connects, Proof: proof, Refused: refused}) + if err != nil { + return nil, err } w.Info("standing in front of modules", wool.Field("modules", listed)) return func() { From ff19f25f229ebe6143dfafb107ce09d504236a0d Mon Sep 17 00:00:00 2001 From: Antoine Toussaint Date: Sat, 24 Oct 2026 20:09:42 -0400 Subject: [PATCH 02/45] hop: a permissive mode that never blocks a developer hop/ENFORCEMENT=warn: the hop still asks the authority, and whatever it answers the call goes through, stamped with who the token claims (a developer identity when there is none) and with the refusal the hop did not make as x-codefly-hop-advisory, logged once per call. The default, enforce, is unchanged. The authority's Check gains an advisory form (Advise) that answers the verified capability beside the refusal it would have made, for a hop that wants the authority's own words. Co-Authored-By: Claude Fable 5.1 --- .../contract/catalog.codefly.json | 2 +- .../authority-client/contract/contract.binpb | Bin 372770 -> 373459 bytes .../go/gen/host/authority/v1/authority.pb.go | 37 +++++++++-- .../gen/host/composition/v1/composition.pb.go | 2 +- libraries/hop/go/hop.go | 11 ++++ .../code/pkg/business/issuer/issuer.go | 20 +++++- .../pkg/gen/host/authority/v1/authority.pb.go | 37 +++++++++-- .../gateway/code/plugins/authority/plugin.go | 4 ++ .../host/authority/v1/authority.swagger.json | 8 +++ .../proto/host/authority/v1/authority.proto | 9 +++ .../hop/code/pkg/business/decide/decide.go | 60 ++++++++++++++++++ .../pkg/business/decide/permissive_test.go | 55 ++++++++++++++++ modules/authority/services/hop/code/work.go | 15 ++++- 13 files changed, 243 insertions(+), 17 deletions(-) create mode 100644 modules/authority/services/hop/code/pkg/business/decide/permissive_test.go diff --git a/libraries/authority-client/contract/catalog.codefly.json b/libraries/authority-client/contract/catalog.codefly.json index 7ad276fc..214e7ae5 100644 --- a/libraries/authority-client/contract/catalog.codefly.json +++ b/libraries/authority-client/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "api", "path": "contract.binpb", - "digest": "sha256:89baa9ae5c5f7940f17b303afdb58824e913427d3fb826c81b24f78e5378d599", + "digest": "sha256:f589836f5fb21d4e1ed9a83659debaacb4eda07b8231fb55d01c1ce7603b898d", "services": [ { "name": "GatewayService", diff --git a/libraries/authority-client/contract/contract.binpb b/libraries/authority-client/contract/contract.binpb index 5f888ae9eb951aed3e2e001c104730d6c0b92f9a..d4975f33a9d62704c34b30bdec5ae353f6741950 100644 GIT binary patch delta 1868 zcmZ8iU1$_n6rOW4cQZSiWH&cIja!Wul{UugwyAAuB2vT}gjR(hHh)?t*|9sg+1c*Q zX4$8-G>?5~q4d7=LE2YKUrO^}K|;WSAd-rN5W$8JY(XO^Aq`EX=iZsgLm$7p=ljk% z-*@lq_kXnCUvK|x7G}w8-K_pI9qcAQOwXO^ruSpui@m*cxRa$#!^%xNo?EVU=p4F? zp5JEIrp-U9^9Y!6(HTm zPvz~&YvS`8v}NwSi}VSZX*8*xAa#`KH-W^=e~76|tYB;U3E?NhLAd2z^Y~S_%>BI0 zOK!oLtnr*Z>3EJ?#==$7qE5%$7pFq`W-tv{&l#Ok2rtDj=%C;wK-D07J8YQ>v+gR@u zgC&P-ujCdzI~$JE!!7J3XOFs@eA1eUrjtrm(W}~JJD24rdEPBaDT`0KQzh4Pe7U7B z=O|Z-9xvJDDaZ4iY1`yoSo_B5bA#uHPMtn``rL&RRjXV)9QftuQMYQ=tOhiM|nftn(HaZK|k&XfdYG z14)`)7$HJL;>!`*otcN!D^U#!%|rUt5CX=&0K_ogR}^fzAl63c;LHNV_eV8IS%3ov zLr9QvTT-rr;t&Pf(q97~3a+Dc@_&K7j)nynrs`G-Ii9zs?I`vhna70XJ5xQu4Zw(I zFdBJL(K$?I_X39bMRzYWF7Fm{$GJUK@@uOo{BcWg>FMjt1`2Z z>32YDPiOcCcFh~IKdsoF&&5Zh^l<797%k>8bi9eh?t=Ea_-d5C+j$p^R;AQLH1y)0 z^x`eF0wE%?K1$!t+=F-!9Ufx0dw|i&V4Dc&OkV_In%# zu0nIrA*BXilayga!NJ$W%0+tNDE=e?!~<6lB5@5`gBOw_a{P5kl-C_0A~A584rbON z9;}FmNLhzH>9Do@Fg}))enr6_AB!I^)8nr_j(km#I(aI6{Tp!QW)^!2+B01FFW}<* zQQ9UBeoP;ZJ_Tc+5^W|LUV;sI2^vrop+bkEAMqJ)K%ygpxX}je30@=CVy_MP8b`1d zW3lI;y%Zx?=@=MT!g3QoZ>;;g2p-yQaMvWI>GzL&vz6rWv9+B0u{ifKsLNl37 zh&Mzv*k==x!JA>q@-5I!l@daxZ;3M&rnCi#NLHG?1*yP$rp(@!6zLp7rf-Yu7CoNb shQw|SQnsUfnEDsmk(6)bw*@iNcSL-Gew*5XM36on%G0@@Ca8DzKV#vOs{jB1 delta 1120 zcmX|=-%Aux6vyY>y=R=6UESI1ue5ay?V;AjS`}2Jhak+C9)!p(+f~sXgP?yPYX5== z?_<$R|3I{u7ziR8$g(KNB4RzbfwnA5W)>85?wy?;zvrIM_nbNB%uc1_&6kd+ImoeG zD97L7WRiV)<~t zafk7UFjv6%C_86yEU^MsyVL9-k1)d&d=*&Kx#VyqNy$gEnDkb`?F@24DyRE=LjnrE z2F!A9Y6M8hjai(GuYuhg}Zff-kTs=8xpwS1z?Vo)CjdxkY^dZGFX6cU<)b= z?Og#v4h4w!P!-0b!rTI5M^0xkmDmC+rjw$&m7;28kbFrBUX-u1c+D#Ytx%ARia{$; zrY2KTNJ=AAv?Tv#ao8(?EgH5!ND10I8j=yR4XnwztPw^^X6A69cN?q#VT5ceWQy8# zg}DpHXZdyxZzXoYifbcRw_8@Vl@Uq7%Q7;L16~>ImLP|W$`JE?UES`ULed(ccK2j# z2JZ~hlM-P2vmi;}9<=#K<1*Fsz5>++kR&C)&EurE54Qhd6eMIHy1E;sLxdbCWLzWk zcp$S2IC0@1n5N4@DypXW3+O{*!aM}yt6W*Yv-^i&^=PLUGicuxweJWyk<{v9@JQ!e zf$+IN(q~kl%fCv>r2-XomG{VB2=fSxQ|T;X`udUjBU;E%ScWix+;Rp~wklthuNKkss$fTg9I8`=sN0b0ENZ|Vy&A#DuO@#l(rRiD4s6wgYvB3= z7ka{Vg{X0n6ud5{mvCaR4&nbfgw%tj2&N`{qL7!M8G1dDAC_?0J%O;la!OLs{QN_F Ga{C{a$*wy9 diff --git a/libraries/authority-client/go/gen/host/authority/v1/authority.pb.go b/libraries/authority-client/go/gen/host/authority/v1/authority.pb.go index 3ff58762..f445c5af 100644 --- a/libraries/authority-client/go/gen/host/authority/v1/authority.pb.go +++ b/libraries/authority-client/go/gen/host/authority/v1/authority.pb.go @@ -802,7 +802,13 @@ type CheckRequest struct { // applies what the audience module's delivery declares for that method, and // refuses by name a call that does not hold it, a method that declares // nothing, and a method the module does not serve. - Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + // Advisory asks for the policy decision without its refusal: the capability + // is still verified and refused when it is not sound, but what the method + // declares is only reported, in CheckResponse.refusal, and the verified + // capability is answered. A hop in a composition that runs permissive + // (hop/ENFORCEMENT=warn) asks this way and logs the refusal it did not make. + Advisory bool `protobuf:"varint,4,opt,name=advisory,proto3" json:"advisory,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -858,6 +864,13 @@ func (x *CheckRequest) GetMethod() string { return "" } +func (x *CheckRequest) GetAdvisory() bool { + if x != nil { + return x.Advisory + } + return false +} + // CheckResponse is the verified capability. type CheckResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -865,8 +878,11 @@ type CheckResponse struct { Context *WorkContextV1 `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` // The owner's authorization revision as of this check. AuthorizationRevision uint64 `protobuf:"varint,2,opt,name=authorization_revision,json=authorizationRevision,proto3" json:"authorization_revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Refusal is what the method's policy would have refused, verbatim, when the + // check was advisory and the policy did not hold; empty when it held. + Refusal string `protobuf:"bytes,3,opt,name=refusal,proto3" json:"refusal,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CheckResponse) Reset() { @@ -913,6 +929,13 @@ func (x *CheckResponse) GetAuthorizationRevision() uint64 { return 0 } +func (x *CheckResponse) GetRefusal() string { + if x != nil { + return x.Refusal + } + return "" +} + // KeysRequest asks for the verification keys. type KeysRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1367,14 +1390,16 @@ const file_host_authority_v1_authority_proto_rawDesc = "" + "\rIssueResponse\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x12&\n" + "\x0fexpires_at_unix\x18\x02 \x01(\x03R\rexpiresAtUnix\x12:\n" + - "\acontext\x18\x03 \x01(\v2 .host.authority.v1.WorkContextV1R\acontext\"X\n" + + "\acontext\x18\x03 \x01(\v2 .host.authority.v1.WorkContextV1R\acontext\"t\n" + "\fCheckRequest\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n" + "\baudience\x18\x02 \x01(\tR\baudience\x12\x16\n" + - "\x06method\x18\x03 \x01(\tR\x06method\"\x82\x01\n" + + "\x06method\x18\x03 \x01(\tR\x06method\x12\x1a\n" + + "\badvisory\x18\x04 \x01(\bR\badvisory\"\x9c\x01\n" + "\rCheckResponse\x12:\n" + "\acontext\x18\x01 \x01(\v2 .host.authority.v1.WorkContextV1R\acontext\x125\n" + - "\x16authorization_revision\x18\x02 \x01(\x04R\x15authorizationRevision\"\r\n" + + "\x16authorization_revision\x18\x02 \x01(\x04R\x15authorizationRevision\x12\x18\n" + + "\arefusal\x18\x03 \x01(\tR\arefusal\"\r\n" + "\vKeysRequest\"X\n" + "\fKeysResponse\x12\x16\n" + "\x06issuer\x18\x01 \x01(\tR\x06issuer\x120\n" + diff --git a/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go b/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go index 156549b7..61ab7143 100644 --- a/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go +++ b/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go @@ -16,7 +16,7 @@ import ( sync "sync" unsafe "unsafe" - v1 "github.com/codefly-dev/authority/libraries/authority-client/go/gen/host/authority/v1" + v1 "github.com/codefly-dev/authority-client-go/gen/host/authority/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) diff --git a/libraries/hop/go/hop.go b/libraries/hop/go/hop.go index c3910ae8..0661913b 100644 --- a/libraries/hop/go/hop.go +++ b/libraries/hop/go/hop.go @@ -48,6 +48,10 @@ const ( // the digest of the forwarded token. A module reading that token's claims // reads exactly what was checked, and nothing a caller swapped in after. ContextKey = StampPrefix + "context" + // AdvisoryKey carries what the authority would have refused when the hop + // runs permissive (hop/ENFORCEMENT=warn): the call went through, this says + // why it would not have. Empty, or absent, when nothing was refused. + AdvisoryKey = StampPrefix + "advisory" ) // Identity is who a call is for, as the hop established it. @@ -69,6 +73,9 @@ type Identity struct { // that needs its full claims decodes them from it without verifying again: // the stamp's proof and digest are what vouch for it. Context string + // Advisory is what the authority would have refused, when the hop let the + // call through anyway (permissive mode); "" when the call was admitted. + Advisory string } // Has reports whether the identity may do action on kind. @@ -113,6 +120,9 @@ func Stamp(md metadata.MD, id Identity, proof string) { if id.Expires > 0 { md.Set(ExpiresKey, strconv.FormatInt(id.Expires, 10)) } + if id.Advisory != "" { + md.Set(AdvisoryKey, id.Advisory) + } if token := first(md, WorkContextKey); token != "" { md.Set(ContextKey, digest(token)) } @@ -139,6 +149,7 @@ func Read(md metadata.MD, proof string) (Identity, error) { } id.Actors = split(first(md, ActorsKey)) id.Scopes = split(first(md, ScopesKey)) + id.Advisory = first(md, AdvisoryKey) if bound := first(md, ContextKey); bound != "" { tokens := md.Get(WorkContextKey) if len(tokens) != 1 || subtle.ConstantTimeCompare([]byte(digest(tokens[0])), []byte(bound)) != 1 { diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go b/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go index af25ecf0..a144a7c7 100644 --- a/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go +++ b/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go @@ -134,6 +134,18 @@ func (i *Issuer) ListResources(ctx context.Context, token, audience, kind, actio // Check verifies a capability for the audience the caller is and, when method // is named, applies what the audience module's delivery declares for it. func (i *Issuer) Check(ctx context.Context, token, audience, method string) (*authorityv1.CheckResponse, error) { + return i.check(ctx, token, audience, method, false) +} + +// Advise is Check with the method's policy decided but not enforced: the +// capability is verified and refused when unsound; what the policy would have +// refused is answered in CheckResponse.Refusal beside the verified capability. +// A hop running permissive asks this way and logs the refusal it did not make. +func (i *Issuer) Advise(ctx context.Context, token, audience, method string) (*authorityv1.CheckResponse, error) { + return i.check(ctx, token, audience, method, true) +} + +func (i *Issuer) check(ctx context.Context, token, audience, method string, advisory bool) (*authorityv1.CheckResponse, error) { if audience == "" { return nil, fmt.Errorf("%w: a check names the audience the caller is", ErrRefused) } @@ -141,9 +153,13 @@ func (i *Issuer) Check(ctx context.Context, token, audience, method string) (*au if err != nil { return nil, err } + refusal := "" if method != "" { if err := i.state.AdmitMethod(audience, method, verified.EffectiveScopes()); err != nil { - return nil, err + if !advisory { + return nil, err + } + refusal = err.Error() } } wc, err := ToWire(verified.Context()) @@ -154,7 +170,7 @@ func (i *Issuer) Check(ctx context.Context, token, audience, method string) (*au if err != nil { return nil, err } - return &authorityv1.CheckResponse{Context: wc, AuthorizationRevision: revision}, nil + return &authorityv1.CheckResponse{Context: wc, AuthorizationRevision: revision, Refusal: refusal}, nil } // Issue signs a capability for one owner shape. diff --git a/modules/authority/services/gateway/code/pkg/gen/host/authority/v1/authority.pb.go b/modules/authority/services/gateway/code/pkg/gen/host/authority/v1/authority.pb.go index 6e488df4..33928cf9 100644 --- a/modules/authority/services/gateway/code/pkg/gen/host/authority/v1/authority.pb.go +++ b/modules/authority/services/gateway/code/pkg/gen/host/authority/v1/authority.pb.go @@ -802,7 +802,13 @@ type CheckRequest struct { // applies what the audience module's delivery declares for that method, and // refuses by name a call that does not hold it, a method that declares // nothing, and a method the module does not serve. - Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + // Advisory asks for the policy decision without its refusal: the capability + // is still verified and refused when it is not sound, but what the method + // declares is only reported, in CheckResponse.refusal, and the verified + // capability is answered. A hop in a composition that runs permissive + // (hop/ENFORCEMENT=warn) asks this way and logs the refusal it did not make. + Advisory bool `protobuf:"varint,4,opt,name=advisory,proto3" json:"advisory,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -858,6 +864,13 @@ func (x *CheckRequest) GetMethod() string { return "" } +func (x *CheckRequest) GetAdvisory() bool { + if x != nil { + return x.Advisory + } + return false +} + // CheckResponse is the verified capability. type CheckResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -865,8 +878,11 @@ type CheckResponse struct { Context *WorkContextV1 `protobuf:"bytes,1,opt,name=context,proto3" json:"context,omitempty"` // The owner's authorization revision as of this check. AuthorizationRevision uint64 `protobuf:"varint,2,opt,name=authorization_revision,json=authorizationRevision,proto3" json:"authorization_revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Refusal is what the method's policy would have refused, verbatim, when the + // check was advisory and the policy did not hold; empty when it held. + Refusal string `protobuf:"bytes,3,opt,name=refusal,proto3" json:"refusal,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CheckResponse) Reset() { @@ -913,6 +929,13 @@ func (x *CheckResponse) GetAuthorizationRevision() uint64 { return 0 } +func (x *CheckResponse) GetRefusal() string { + if x != nil { + return x.Refusal + } + return "" +} + // KeysRequest asks for the verification keys. type KeysRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1367,14 +1390,16 @@ const file_host_authority_v1_authority_proto_rawDesc = "" + "\rIssueResponse\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x12&\n" + "\x0fexpires_at_unix\x18\x02 \x01(\x03R\rexpiresAtUnix\x12:\n" + - "\acontext\x18\x03 \x01(\v2 .host.authority.v1.WorkContextV1R\acontext\"X\n" + + "\acontext\x18\x03 \x01(\v2 .host.authority.v1.WorkContextV1R\acontext\"t\n" + "\fCheckRequest\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n" + "\baudience\x18\x02 \x01(\tR\baudience\x12\x16\n" + - "\x06method\x18\x03 \x01(\tR\x06method\"\x82\x01\n" + + "\x06method\x18\x03 \x01(\tR\x06method\x12\x1a\n" + + "\badvisory\x18\x04 \x01(\bR\badvisory\"\x9c\x01\n" + "\rCheckResponse\x12:\n" + "\acontext\x18\x01 \x01(\v2 .host.authority.v1.WorkContextV1R\acontext\x125\n" + - "\x16authorization_revision\x18\x02 \x01(\x04R\x15authorizationRevision\"\r\n" + + "\x16authorization_revision\x18\x02 \x01(\x04R\x15authorizationRevision\x12\x18\n" + + "\arefusal\x18\x03 \x01(\tR\arefusal\"\r\n" + "\vKeysRequest\"X\n" + "\fKeysResponse\x12\x16\n" + "\x06issuer\x18\x01 \x01(\tR\x06issuer\x120\n" + diff --git a/modules/authority/services/gateway/code/plugins/authority/plugin.go b/modules/authority/services/gateway/code/plugins/authority/plugin.go index 7fb909c8..00ddddd1 100644 --- a/modules/authority/services/gateway/code/plugins/authority/plugin.go +++ b/modules/authority/services/gateway/code/plugins/authority/plugin.go @@ -145,6 +145,10 @@ func delegationOf(d state.Delegation) *authorityv1.Delegation { // Check verifies for the audience the caller is. func (p *Plugin) Check(ctx context.Context, req *authorityv1.CheckRequest) (*authorityv1.CheckResponse, error) { + if req.GetAdvisory() { + out, err := p.Issuer.Advise(ctx, req.GetToken(), req.GetAudience(), req.GetMethod()) + return out, Status(err) + } out, err := p.Issuer.Check(ctx, req.GetToken(), req.GetAudience(), req.GetMethod()) return out, Status(err) } diff --git a/modules/authority/services/gateway/openapi/host/authority/v1/authority.swagger.json b/modules/authority/services/gateway/openapi/host/authority/v1/authority.swagger.json index 16b928e8..12386052 100644 --- a/modules/authority/services/gateway/openapi/host/authority/v1/authority.swagger.json +++ b/modules/authority/services/gateway/openapi/host/authority/v1/authority.swagger.json @@ -129,6 +129,10 @@ "method": { "type": "string", "description": "The method the capability is presented to: \"/package.Service/Method\" for a\ngRPC call, \"\u003cVERB\u003e \u003cpath\u003e\" for an HTTP request (GET /v1/model/profiles),\nmatched against the routes the module declares. When set, the check also\napplies what the audience module's delivery declares for that method, and\nrefuses by name a call that does not hold it, a method that declares\nnothing, and a method the module does not serve." + }, + "advisory": { + "type": "boolean", + "description": "Advisory asks for the policy decision without its refusal: the capability\nis still verified and refused when it is not sound, but what the method\ndeclares is only reported, in CheckResponse.refusal, and the verified\ncapability is answered. A hop in a composition that runs permissive\n(hop/ENFORCEMENT=warn) asks this way and logs the refusal it did not make." } }, "description": "CheckRequest presents a capability for verification." @@ -144,6 +148,10 @@ "type": "string", "format": "uint64", "description": "The owner's authorization revision as of this check." + }, + "refusal": { + "type": "string", + "description": "Refusal is what the method's policy would have refused, verbatim, when the\ncheck was advisory and the policy did not hold; empty when it held." } }, "description": "CheckResponse is the verified capability." diff --git a/modules/authority/services/gateway/proto/host/authority/v1/authority.proto b/modules/authority/services/gateway/proto/host/authority/v1/authority.proto index fa60e5e6..aeb6a0f2 100644 --- a/modules/authority/services/gateway/proto/host/authority/v1/authority.proto +++ b/modules/authority/services/gateway/proto/host/authority/v1/authority.proto @@ -180,6 +180,12 @@ message CheckRequest { // refuses by name a call that does not hold it, a method that declares // nothing, and a method the module does not serve. string method = 3; + // Advisory asks for the policy decision without its refusal: the capability + // is still verified and refused when it is not sound, but what the method + // declares is only reported, in CheckResponse.refusal, and the verified + // capability is answered. A hop in a composition that runs permissive + // (hop/ENFORCEMENT=warn) asks this way and logs the refusal it did not make. + bool advisory = 4; } // CheckResponse is the verified capability. @@ -188,6 +194,9 @@ message CheckResponse { WorkContextV1 context = 1; // The owner's authorization revision as of this check. uint64 authorization_revision = 2; + // Refusal is what the method's policy would have refused, verbatim, when the + // check was advisory and the policy did not hold; empty when it held. + string refusal = 3; } // KeysRequest asks for the verification keys. diff --git a/modules/authority/services/hop/code/pkg/business/decide/decide.go b/modules/authority/services/hop/code/pkg/business/decide/decide.go index 0aaf6d6b..757a10ef 100644 --- a/modules/authority/services/hop/code/pkg/business/decide/decide.go +++ b/modules/authority/services/hop/code/pkg/business/decide/decide.go @@ -30,6 +30,14 @@ type Checker interface { type Decider struct { Authority Checker Modules map[string]bool + // Permissive is hop/ENFORCEMENT=warn: the authority is still asked, and + // whatever it answers the call goes through, stamped with who the token + // says it is (a developer identity when there is none) and with the + // refusal the hop did not make, which it also logs. The composition says + // so; a cell never does. + Permissive bool + // Warned is told each refusal the permissive hop let through. + Warned func(method string, err error) // AudienceOf reads the module a token is addressed to; nil decodes it with // core, unverified, which is all the hop needs before the authority checks. AudienceOf func(token string) (string, error) @@ -45,6 +53,58 @@ func coreAudience(token string) (string, error) { // Decide returns the module a call is for and who it acts for, or the refusal. func (d Decider) Decide(ctx context.Context, method string, md metadata.MD) (string, hop.Identity, error) { + module, id, err := d.decide(ctx, method, md) + if err == nil || !d.Permissive { + return module, id, err + } + if d.Warned != nil { + d.Warned(method, err) + } + return d.permit(method, md, err) +} + +// permit is the permissive hop's answer to a refusal: the module the call +// names (or the first one it fronts), the identity the token claims unverified +// (or a developer's), and the refusal carried as the stamp's advisory. +func (d Decider) permit(method string, md metadata.MD, refusal error) (string, hop.Identity, error) { + id := hop.Identity{Owner: "developer", OwnerKind: "human", Tenant: "local", Advisory: refusal.Error()} + module := "" + if tokens := md.Get(hop.WorkContextKey); len(tokens) == 1 { + if decoded, err := workcontext.Decode(tokens[0]); err == nil { + wc := decoded.Context() + id = hop.Identity{Owner: wc.GetOwnerPrincipalId(), OwnerKind: wc.GetOwnerPrincipalKind(), Tenant: wc.GetTenantId(), + Task: wc.GetTaskId(), Expires: wc.GetExpiresAtUnix(), Advisory: refusal.Error()} + for _, scope := range wc.GetAuthorityScopes() { + for _, action := range scope.GetActions() { + id.Scopes = append(id.Scopes, scope.GetResourceKind()+":"+action) + } + } + for _, actor := range wc.GetActorChain() { + id.Actors = append(id.Actors, actor.GetPrincipalId()) + } + if d.Modules[wc.GetAudience()] { + module = wc.GetAudience() + } + } + } + if module == "" { + names := make([]string, 0, len(d.Modules)) + for name := range d.Modules { + names = append(names, name) + } + slices.Sort(names) + if len(names) == 0 { + return "", hop.Identity{}, status.Errorf(codes.Internal, "the hop fronts no module") + } + module = names[0] + } + if id.Owner == "" || id.Tenant == "" { + id.Owner, id.OwnerKind, id.Tenant = "developer", "human", "local" + } + return module, id, nil +} + +func (d Decider) decide(ctx context.Context, method string, md metadata.MD) (string, hop.Identity, error) { tokens := md.Get(hop.WorkContextKey) switch len(tokens) { case 0: diff --git a/modules/authority/services/hop/code/pkg/business/decide/permissive_test.go b/modules/authority/services/hop/code/pkg/business/decide/permissive_test.go new file mode 100644 index 00000000..dda50552 --- /dev/null +++ b/modules/authority/services/hop/code/pkg/business/decide/permissive_test.go @@ -0,0 +1,55 @@ +package decide + +import ( + "context" + "errors" + "testing" + + authorityv1 "github.com/codefly-dev/authority/libraries/authority-client/go/gen/host/authority/v1" + "github.com/codefly-dev/authority/libraries/hop/go" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type refusing struct{} + +func (refusing) Check(context.Context, *authorityv1.CheckRequest, ...grpc.CallOption) (*authorityv1.CheckResponse, error) { + return nil, status.Error(codes.PermissionDenied, "robin requires robin.intents:read_config for GET /v1/robin/intents/version, which the call does not hold") +} + +// Permissive, the hop lets a refused call through: the module is the token's +// audience, the identity what the token claims, the refusal rides as the +// stamp's advisory and is told to Warned. Enforcing, the refusal stands. +func TestAPermissiveHopLetsARefusedCallThroughWithTheRefusalAsAdvisory(t *testing.T) { + var warned string + d := Decider{Authority: refusing{}, Modules: map[string]bool{"robin": true}, Permissive: true, + Warned: func(method string, err error) { warned = method + ": " + err.Error() }, + AudienceOf: func(string) (string, error) { return "robin", nil }} + md := metadata.Pairs(hop.WorkContextKey, "not-a-core-token") + module, id, err := d.Decide(context.Background(), "GET /v1/robin/intents/version", md) + if err != nil || module != "robin" { + t.Fatalf("permissive: %v %v", module, err) + } + if id.Owner != "developer" || id.Tenant != "local" || id.Advisory == "" { + t.Fatalf("an undecodable token is stamped as a developer with the advisory: %+v", id) + } + if warned == "" { + t.Fatal("the refusal was not told") + } + stamp := metadata.MD{} + hop.Stamp(stamp, id, "proof") + read, err := hop.Read(stamp, "proof") + if err != nil || read.Advisory != id.Advisory { + t.Fatalf("the advisory rides the stamp: %+v %v", read, err) + } + + strict := Decider{Authority: refusing{}, Modules: map[string]bool{"robin": true}, AudienceOf: func(string) (string, error) { return "robin", nil }} + if _, _, err := strict.Decide(context.Background(), "GET /v1/robin/intents/version", md); err == nil || status.Code(err) != codes.PermissionDenied { + t.Fatalf("enforcing, the refusal stands: %v", err) + } + if !errors.Is(err, err) { + t.Fatal("unreachable") + } +} diff --git a/modules/authority/services/hop/code/work.go b/modules/authority/services/hop/code/work.go index 9f1da14d..db8c942f 100644 --- a/modules/authority/services/hop/code/work.go +++ b/modules/authority/services/hop/code/work.go @@ -83,10 +83,23 @@ func standInFront(ctx context.Context, config *adapters.Configuration) (Clean, e } modules[name] = true } - decision := decide.Decider{Authority: authorityv1.NewAuthorityServiceClient(authority), Modules: modules}.Decide refused := func(method string, err error) { w.Warn("refused", wool.Field("method", method), wool.Field("reason", err.Error())) } + // hop/ENFORCEMENT: enforce (the default) refuses what the authority + // refuses; warn lets every call through stamped and logs what would have + // been refused — a composition's choice for a developer's run, never a + // cell's. + enforcement, _ := codefly.For(ctx).WorkspaceConfiguration("hop", "ENFORCEMENT") + permissive := strings.EqualFold(strings.TrimSpace(enforcement), "warn") + decider := decide.Decider{Authority: authorityv1.NewAuthorityServiceClient(authority), Modules: modules, Permissive: permissive, + Warned: func(method string, err error) { + w.Warn("PERMISSIVE: let through a call the authority refused", wool.Field("method", method), wool.Field("reason", err.Error())) + }} + decision := decider.Decide + if permissive { + w.Warn("hop/ENFORCEMENT=warn: the hop lets every call through and logs what it would have refused") + } p := &proxy.Proxy{Decide: decision, Upstreams: upstreams, Proof: proof, Refused: refused} config.GRPCServerOptions = append(config.GRPCServerOptions, p.ServerOptions()...) // The service declares this endpoint even when every upstream uses gRPC. From d20c272cf63eb114fa14f898761e2ac0810c09fc Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:17:45 -0400 Subject: [PATCH 03/45] fix: preserve advisory identity and avoid guessing hop destinations --- .../code/plugins/authority/advisory_test.go | 80 +++++++++++++++++++ modules/authority/services/hop/README.md | 34 ++++++-- .../hop/code/pkg/business/decide/decide.go | 32 +++++--- .../pkg/business/decide/permissive_test.go | 70 +++++++++++++++- .../infra/proxy/permissive_routing_test.go | 41 ++++++++++ 5 files changed, 239 insertions(+), 18 deletions(-) create mode 100644 modules/authority/services/gateway/code/plugins/authority/advisory_test.go create mode 100644 modules/authority/services/hop/code/pkg/infra/proxy/permissive_routing_test.go diff --git a/modules/authority/services/gateway/code/plugins/authority/advisory_test.go b/modules/authority/services/gateway/code/plugins/authority/advisory_test.go new file mode 100644 index 00000000..9a854670 --- /dev/null +++ b/modules/authority/services/gateway/code/plugins/authority/advisory_test.go @@ -0,0 +1,80 @@ +package authority_test + +import ( + "context" + "net" + "strings" + "testing" + + "gateway/pkg/business/state" + "gateway/pkg/business/testkit" + authorityv1 "gateway/pkg/gen/host/authority/v1" + authorityplugin "gateway/plugins/authority" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/proto" +) + +// Probe the actual generated client, plugin and issuer together. The existing +// testkit owns the fixture identity/signing; this test adds no second issuer. +func TestGeneratedCheckReportsAdvisoryWithoutChangingIdentity(t *testing.T) { + k := testkit.New(t) + manifest := testkit.Documents(2) + manifest.Methods = map[string]state.MethodPolicy{ + "/documents.v1.Documents/Read": {Requires: []*basev0.WorkScopeV1{testkit.Scope("documents", "read")}}, + "/documents.v1.Documents/Write": {Requires: []*basev0.WorkScopeV1{testkit.Scope("documents", "write")}}, + } + if _, err := k.State.ApplyManifest(manifest); err != nil { + t.Fatal(err) + } + server := grpc.NewServer() + (&authorityplugin.Plugin{Issuer: k.Issuer}).RegisterGRPC(server) + listener := bufconn.Listen(1 << 20) + go func() { _ = server.Serve(listener) }() + t.Cleanup(server.Stop) + connection, err := grpc.NewClient("passthrough:///bufconn", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = connection.Close() }) + client := authorityv1.NewAuthorityServiceClient(connection) + session, err := k.State.SignIn("fixture-maya") + if err != nil { + t.Fatal(err) + } + person := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorization", "Bearer "+session.ID)) + issued, err := client.Issue(person, &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents"}) + if err != nil { + t.Fatal(err) + } + for _, method := range []string{"/documents.v1.Documents/Write", "/documents.v1.Documents/Undeclared"} { + t.Run(method, func(t *testing.T) { + request := &authorityv1.CheckRequest{Token: issued.GetToken(), Audience: "documents", Method: method} + if _, err := client.Check(context.Background(), request); err == nil { + t.Fatal("strict check did not refuse") + } + request.Advisory = true + response, err := client.Check(context.Background(), request) + if err != nil { + t.Fatal(err) + } + if response.GetRefusal() == "" { + t.Fatal("warning missing") + } + if !proto.Equal(response.GetContext(), issued.GetContext()) { + t.Fatal("advisory changed issued identity/scopes") + } + }) + } + allowed, err := client.Check(context.Background(), &authorityv1.CheckRequest{ + Token: issued.GetToken(), Audience: "documents", Method: "/documents.v1.Documents/Read", Advisory: true}) + if err != nil || strings.TrimSpace(allowed.GetRefusal()) != "" { + t.Fatalf("allowed read: %v, %v", allowed, err) + } +} diff --git a/modules/authority/services/hop/README.md b/modules/authority/services/hop/README.md index 7e75d1a8..3a9a5955 100644 --- a/modules/authority/services/hop/README.md +++ b/modules/authority/services/hop/README.md @@ -1,8 +1,30 @@ # hop -The door in front of a module: it lets through only a call the authority -checked and that holds what its method declares, stamped with who it is for. -The decision is `pkg/business/decide`, the module's declarations are read by -`pkg/infra/policy`, the local proxy face is `pkg/infra/proxy`, and the stamp and -the declaration a module uses are `libraries/hop`. The repository README's -section on the hop says how to run it and what each refusal answers. +The proxy in front of a module. It asks the authority through the generated +Check client, stamps the returned identity, and forwards the real call. Both +gRPC and HTTP/Connect use `pkg/business/decide` and `pkg/infra/proxy`. + +The composition selects `hop/ENFORCEMENT` through SDK-delivered configuration: +`enforce` (the default) returns authority refusals; `warn` logs them and allows +permissive developer forwarding. For method-policy failures, warn mode requests +an advisory Check and uses the authority's returned context, preserving its +owner, tenant, operation binding, task and effective scopes. The refusal travels +in the stamp's advisory field and is logged once. + +For other authority failures, the existing developer fallback uses the readable +token's identity, or `developer`/`local` if none is readable. A usable token +audience selects the upstream. With no audience, only a sole configured upstream +can be selected; a multi-module hop reports a routing diagnostic rather than +sending the request to an unrelated module. No module response is fabricated. + +Warning mode relaxes hop admission. Issuance and downstream module checks still +run: modules that require a live Work Context, scope or resource grant can still +refuse the forwarded call. The synthetic fallback is not a complete developer +session or a replacement for an installation. Upstream connection and application +failures remain visible. Full solution unblocking needs those owner paths to be +exercised; a successful hop unit test alone does not prove it. + +Validation: run `go test -race ./...` in this service's `code/`, the gateway's +`code/`, and `libraries/hop/go`. Gateway's generated-client advisory test checks +the real issuer; proxy's routing regression checks that a Records request never +reaches Documents when its destination is unresolved. diff --git a/modules/authority/services/hop/code/pkg/business/decide/decide.go b/modules/authority/services/hop/code/pkg/business/decide/decide.go index 757a10ef..e2936afe 100644 --- a/modules/authority/services/hop/code/pkg/business/decide/decide.go +++ b/modules/authority/services/hop/code/pkg/business/decide/decide.go @@ -57,14 +57,16 @@ func (d Decider) Decide(ctx context.Context, method string, md metadata.MD) (str if err == nil || !d.Permissive { return module, id, err } - if d.Warned != nil { - d.Warned(method, err) + refusal := err + module, id, err = d.permit(method, md, refusal) + if err == nil && d.Warned != nil { + d.Warned(method, refusal) } - return d.permit(method, md, err) + return module, id, err } // permit is the permissive hop's answer to a refusal: the module the call -// names (or the first one it fronts), the identity the token claims unverified +// names (or its sole upstream), the identity the token claims unverified // (or a developer's), and the refusal carried as the stamp's advisory. func (d Decider) permit(method string, md metadata.MD, refusal error) (string, hop.Identity, error) { id := hop.Identity{Owner: "developer", OwnerKind: "human", Tenant: "local", Advisory: refusal.Error()} @@ -73,7 +75,7 @@ func (d Decider) permit(method string, md metadata.MD, refusal error) (string, h if decoded, err := workcontext.Decode(tokens[0]); err == nil { wc := decoded.Context() id = hop.Identity{Owner: wc.GetOwnerPrincipalId(), OwnerKind: wc.GetOwnerPrincipalKind(), Tenant: wc.GetTenantId(), - Task: wc.GetTaskId(), Expires: wc.GetExpiresAtUnix(), Advisory: refusal.Error()} + Task: wc.GetTaskId(), Expires: wc.GetExpiresAtUnix(), Binding: wc.GetOperationBinding().GetBindingId(), Advisory: refusal.Error()} for _, scope := range wc.GetAuthorityScopes() { for _, action := range scope.GetActions() { id.Scopes = append(id.Scopes, scope.GetResourceKind()+":"+action) @@ -89,13 +91,18 @@ func (d Decider) permit(method string, md metadata.MD, refusal error) (string, h } if module == "" { names := make([]string, 0, len(d.Modules)) - for name := range d.Modules { - names = append(names, name) + for name, enabled := range d.Modules { + if enabled { + names = append(names, name) + } } slices.Sort(names) if len(names) == 0 { return "", hop.Identity{}, status.Errorf(codes.Internal, "the hop fronts no module") } + if len(names) > 1 { + return "", hop.Identity{}, status.Errorf(codes.FailedPrecondition, "the permissive hop cannot route %s without a module audience; it fronts %v", method, names) + } module = names[0] } if id.Owner == "" || id.Tenant == "" { @@ -124,7 +131,7 @@ func (d Decider) decide(ctx context.Context, method string, md metadata.MD) (str if !d.Modules[module] { return "", hop.Identity{}, status.Errorf(codes.PermissionDenied, "the call to %s is addressed to %q, a module this hop does not stand in front of", method, module) } - checked, err := d.Authority.Check(ctx, &authorityv1.CheckRequest{Token: tokens[0], Audience: module, Method: method}) + checked, err := d.Authority.Check(ctx, &authorityv1.CheckRequest{Token: tokens[0], Audience: module, Method: method, Advisory: d.Permissive}) if err != nil { refusal := status.Convert(err) if refusal.Code() == codes.Unavailable || refusal.Code() == codes.DeadlineExceeded { @@ -132,7 +139,14 @@ func (d Decider) decide(ctx context.Context, method string, md metadata.MD) (str } return "", hop.Identity{}, status.Errorf(refusal.Code(), "the authority refused the call to %s: %s", module, refusal.Message()) } - return module, IdentityOf(checked.GetContext()), nil + id := IdentityOf(checked.GetContext()) + if d.Permissive && checked.GetRefusal() != "" { + id.Advisory = checked.GetRefusal() + if d.Warned != nil { + d.Warned(method, status.Error(codes.PermissionDenied, id.Advisory)) + } + } + return module, id, nil } // IdentityOf reads who a checked Work Context is for. Its scopes are the diff --git a/modules/authority/services/hop/code/pkg/business/decide/permissive_test.go b/modules/authority/services/hop/code/pkg/business/decide/permissive_test.go index dda50552..e74952cc 100644 --- a/modules/authority/services/hop/code/pkg/business/decide/permissive_test.go +++ b/modules/authority/services/hop/code/pkg/business/decide/permissive_test.go @@ -2,7 +2,7 @@ package decide import ( "context" - "errors" + "strings" "testing" authorityv1 "github.com/codefly-dev/authority/libraries/authority-client/go/gen/host/authority/v1" @@ -11,6 +11,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) type refusing struct{} @@ -49,7 +50,70 @@ func TestAPermissiveHopLetsARefusedCallThroughWithTheRefusalAsAdvisory(t *testin if _, _, err := strict.Decide(context.Background(), "GET /v1/robin/intents/version", md); err == nil || status.Code(err) != codes.PermissionDenied { t.Fatalf("enforcing, the refusal stands: %v", err) } - if !errors.Is(err, err) { - t.Fatal("unreachable") +} + +// The gateway already provides verified identity with an advisory. The hop +// must use that answer, including the binding and attenuated actor scopes. +type advisoryChecker struct{} + +func (advisoryChecker) Check(_ context.Context, req *authorityv1.CheckRequest, _ ...grpc.CallOption) (*authorityv1.CheckResponse, error) { + if !req.GetAdvisory() { + return nil, status.Error(codes.PermissionDenied, "write scope missing") + } + return &authorityv1.CheckResponse{Refusal: "write scope missing", Context: &authorityv1.WorkContextV1{ + OwnerPrincipalId: "maya", OwnerPrincipalKind: proto.String("human"), TenantId: "acme", TaskId: "task-1", ExpiresAtUnix: 42, + OperationBinding: &authorityv1.WorkOperationBindingV1{BindingId: "acme/robin/documents"}, + AuthorityScopes: []*authorityv1.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"read", "write"}}}, + ActorChain: []*authorityv1.WorkActorV1{{PrincipalId: "robin", GrantedScopes: []*authorityv1.WorkScopeV1{{ResourceKind: "documents", Actions: []string{"read"}}}}}, + }}, nil +} + +func TestPermissivePolicyUsesTheAuthorityAdvisoryIdentity(t *testing.T) { + warnings := 0 + d := Decider{Authority: advisoryChecker{}, Modules: map[string]bool{"documents": true}, Permissive: true, + AudienceOf: func(string) (string, error) { return "documents", nil }, + Warned: func(method string, err error) { + warnings++ + if method != "POST /documents/write" || status.Convert(err).Message() != "write scope missing" { + t.Errorf("warning = %q, %v", method, err) + } + }, + } + module, id, err := d.Decide(context.Background(), "POST /documents/write", metadata.Pairs(hop.WorkContextKey, "test-context")) + if err != nil || module != "documents" || warnings != 1 { + t.Fatalf("decision = %q, %v; warnings = %d", module, err, warnings) + } + if id.Owner != "maya" || id.Tenant != "acme" || id.Binding != "acme/robin/documents" || id.Task != "task-1" || id.Expires != 42 { + t.Fatalf("lost authority identity: %+v", id) + } + if id.Advisory != "write scope missing" || len(id.Actors) != 1 || id.Actors[0] != "robin" || len(id.Scopes) != 1 || id.Scopes[0] != "documents:read" { + t.Fatalf("lost advisory or effective scopes: %+v", id) + } +} + +func TestPermissiveFallbackNeedsAnUnambiguousDestination(t *testing.T) { + for _, tc := range []struct { + name string + modules map[string]bool + code codes.Code + diagnostic string + module string + warnings int + }{ + {name: "none", modules: map[string]bool{}, code: codes.Internal, diagnostic: "fronts no module"}, + {name: "sole upstream", modules: map[string]bool{"records": true, "documents": false}, code: codes.OK, module: "records", warnings: 1}, + {name: "unresolved audience", modules: map[string]bool{"records": true, "documents": true}, code: codes.FailedPrecondition, diagnostic: "without a module audience"}, + } { + t.Run(tc.name, func(t *testing.T) { + warnings := 0 + d := Decider{Permissive: true, Modules: tc.modules, Warned: func(string, error) { warnings++ }} + module, _, err := d.Decide(context.Background(), "/records.v1.RecordsService/ReadRecord", nil) + if module != tc.module || status.Code(err) != tc.code || warnings != tc.warnings { + t.Fatalf("module=%q error=%v warnings=%d; want %q %v %d", module, err, warnings, tc.module, tc.code, tc.warnings) + } + if tc.diagnostic != "" && !strings.Contains(status.Convert(err).Message(), tc.diagnostic) { + t.Fatalf("missing routing diagnostic %q: %v", tc.diagnostic, err) + } + }) } } diff --git a/modules/authority/services/hop/code/pkg/infra/proxy/permissive_routing_test.go b/modules/authority/services/hop/code/pkg/infra/proxy/permissive_routing_test.go new file mode 100644 index 00000000..468f9621 --- /dev/null +++ b/modules/authority/services/hop/code/pkg/infra/proxy/permissive_routing_test.go @@ -0,0 +1,41 @@ +package proxy_test + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync/atomic" + "testing" + + "hop/pkg/business/decide" + "hop/pkg/infra/proxy" +) + +// This is a hermetic transport probe, not a replacement module or playground. +// A routing failure may be reported, but a Records request must never go to +// Documents merely because its name sorts first in the hop's module list. +func TestWarnModeDoesNotSendRecordsToDocuments(t *testing.T) { + var documentsCalls atomic.Int32 + documents := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + documentsCalls.Add(1) + http.Error(w, "documents does not serve RecordsService", http.StatusNotFound) + })) + defer documents.Close() + records := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"record":{}}`)) + })) + defer records.Close() + docsURL, _ := url.Parse(documents.URL) + recordsURL, _ := url.Parse(records.URL) + decision := decide.Decider{Permissive: true, Modules: map[string]bool{"documents": true, "records": true}} + face := proxy.HTTP{Proof: "test-run-proof", Decide: decision.Decide, + Connect: map[string]*url.URL{"documents": docsURL, "records": recordsURL}} + req := httptest.NewRequest(http.MethodPost, "/records.v1.RecordsService/ReadRecord", strings.NewReader(`{}`)) + req.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + face.ServeHTTP(response, req) + if documentsCalls.Load() != 0 { + t.Fatalf("Records request reached Documents %d time(s): HTTP %d, body %q", documentsCalls.Load(), response.Code, response.Body.String()) + } +} From dc41de4126aea7453008dd9de499458e0c750397 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:09:02 -0400 Subject: [PATCH 04/45] feat: derive local developer permissions and diagnose delivered policies --- README.md | 5 + libraries/delivery/go/policy.go | 43 ++++++ libraries/delivery/go/policy_test.go | 23 +++ modules/authority/services/gateway/README.md | 22 +++ .../services/gateway/code/fixtures/local.yaml | 5 +- .../code/pkg/business/issuer/issuer_test.go | 8 +- .../code/pkg/business/state/permissions.go | 134 ++++++++++++++++++ .../pkg/business/state/permissions_test.go | 77 ++++++++++ .../gateway/code/pkg/business/state/state.go | 24 ++++ .../code/plugins/composition/plugin.go | 4 + 10 files changed, 338 insertions(+), 7 deletions(-) create mode 100644 libraries/delivery/go/policy.go create mode 100644 libraries/delivery/go/policy_test.go create mode 100644 modules/authority/services/gateway/code/pkg/business/state/permissions.go create mode 100644 modules/authority/services/gateway/code/pkg/business/state/permissions_test.go diff --git a/README.md b/README.md index 7cdf1b85..dd32d23b 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,11 @@ own fixture. When the fixture signs, the suite composes its solution as a carrie with the authority's document carrying its roles; the sketch remains for a fixture that does not sign. +The development gateway also logs a delivery-time role × method report and +generates its local developer role from concrete delivered permissions. See +[local permission diagnostics](modules/authority/services/gateway/README.md#permission-diagnostics-during-local-development) +for the distinction between static coverage and runtime authorization. + ## Delegate chains: a module inherits who it acts for Owner's rule of 2026-10-23, generic to every module that acts for someone (robin diff --git a/libraries/delivery/go/policy.go b/libraries/delivery/go/policy.go new file mode 100644 index 00000000..fc2b3726 --- /dev/null +++ b/libraries/delivery/go/policy.go @@ -0,0 +1,43 @@ +package delivery + +import ( + "fmt" + "regexp" +) + +var permissionName = regexp.MustCompile(`^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$`) + +// ValidatePermission checks the vocabulary of one declared permission. Wildcard +// fixture grants are not authored method requirements. +func ValidatePermission(kind, action string) error { + if !permissionName.MatchString(kind) || !permissionName.MatchString(action) { + return fmt.Errorf("invalid permission %q:%q: expected lowercase kind and action", kind, action) + } + return nil +} + +// ValidateMethodPolicy is shared by offline preflight and delivered state. +// Local adoption may retain absent policy for named diagnostics; deployment +// requires an explicit policy. Contradictory or malformed policy is never valid. +func ValidateMethodPolicy(method string, anyChecked bool, requires map[string][]string, requireDeclaration bool) error { + if method == "" { + return fmt.Errorf("method policy has no method") + } + if anyChecked && len(requires) > 0 { + return fmt.Errorf("%s declares both requirements and any_checked_caller", method) + } + if !anyChecked && len(requires) == 0 && requireDeclaration { + return fmt.Errorf("%s declares no permission policy", method) + } + for kind, actions := range requires { + if len(actions) == 0 { + return fmt.Errorf("%s: %s declares no actions", method, kind) + } + for _, action := range actions { + if err := ValidatePermission(kind, action); err != nil { + return fmt.Errorf("%s: %w", method, err) + } + } + } + return nil +} diff --git a/libraries/delivery/go/policy_test.go b/libraries/delivery/go/policy_test.go new file mode 100644 index 00000000..6acd0db3 --- /dev/null +++ b/libraries/delivery/go/policy_test.go @@ -0,0 +1,23 @@ +package delivery + +import "testing" + +func TestPolicyValidityDoesNotDependOnRuntimeWarningMode(t *testing.T) { + for _, local := range []bool{false, true} { + if err := ValidateMethodPolicy("/api/Read", true, map[string][]string{"entry": {"read"}}, !local); err == nil { + t.Fatal("contradictory policy accepted") + } + if err := ValidateMethodPolicy("/api/Read", false, map[string][]string{"entry": {"*"}}, !local); err == nil { + t.Fatal("wildcard requirement accepted") + } + if err := ValidateMethodPolicy("/api/Read", false, map[string][]string{"entry": {"read"}}, !local); err != nil { + t.Fatal(err) + } + } + if err := ValidateMethodPolicy("/api/Missing", false, nil, true); err == nil { + t.Fatal("missing deployment policy accepted") + } + if err := ValidateMethodPolicy("/api/Missing", false, nil, false); err != nil { + t.Fatal("local missing policy must remain diagnosable") + } +} diff --git a/modules/authority/services/gateway/README.md b/modules/authority/services/gateway/README.md index eef04a15..28658da9 100644 --- a/modules/authority/services/gateway/README.md +++ b/modules/authority/services/gateway/README.md @@ -36,3 +36,25 @@ AUTHORITY_STORE_TEST_DATABASE_URL='postgres://postgres:…@127.0.0.1:5432/postgr Generated code (`pkg/gen`, `*_gen.go`, `main.go`) comes from `codefly generate` and the agent; `work.go` and everything under `pkg/business`, `pkg/infra` and `plugins` is written here. + +## Permission diagnostics during local development + +The local fixture's `developer` role uses `generated: true` and declares no +scopes. The gateway replaces its scopes after each delivery with the explicit +union of currently delivered module kinds/actions. Method requirements contribute +to that catalog; explicitly declared non-method ceilings remain supported during +migration. A new delivery can remove an action as well as add one. The generated +role does not assign itself to anyone and does not change business roles or +solution installation roles. This is fixture behavior in the development gateway. + +Every successful composition delivery logs a sorted role × module-generation × +method report. It names uncovered requirements, unrecognized fixture actions and +kinds unresolved in the currently delivered composition. These are diagnostics, +not a demand that every role may call every method. Static coverage still needs +valid identity, active installation, consent where required and resource checks. +An absent method policy remains a named local refusal; contradictory or malformed +policy is rejected when delivered. Offline deployment validation uses the same +policy validator in the delivery library, requiring explicit method declarations. + +`go test -race ./...` in `code/` includes replacement/removal and fixture coverage +regressions. This does not qualify the production host or live browser journeys. diff --git a/modules/authority/services/gateway/code/fixtures/local.yaml b/modules/authority/services/gateway/code/fixtures/local.yaml index 622bc351..ef0c264c 100644 --- a/modules/authority/services/gateway/code/fixtures/local.yaml +++ b/modules/authority/services/gateway/code/fixtures/local.yaml @@ -74,12 +74,11 @@ roles: name: admin scopes: - {kind: documents, actions: [read, write]} - # The local developer: every action on every kind each composed module serves, + # Generated from the current delivered catalog, recomputed on each delivery, # in Acme. A playground signs its person in as dev. - organization: acme name: developer - scopes: - - {kind: "*", actions: ["*"]} + generated: true people: - id: maya email: maya@acme.test diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go b/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go index e5854012..b47f0722 100644 --- a/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go +++ b/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go @@ -461,8 +461,8 @@ func TestABindingNamingIssueMayLeaveItsAudienceToTheBinding(t *testing.T) { // The local developer's role names no module: it holds every action on every // kind the audience serves. At a module whose manifest names kinds and not -// actions, an explicit ask is granted as asked and an empty ask is refused by -// name, since "everything" would put no concrete action in the token. +// actions, both explicit and empty asks are refused until concrete permission +// declarations arrive. The developer role never invents actions from a wildcard. func TestTheDeveloperRoleHoldsWhatEachComposedModuleServes(t *testing.T) { k := testkit.New(t) held, err := k.State.PersonScopes("dev", "records") @@ -476,8 +476,8 @@ func TestTheDeveloperRoleHoldsWhatEachComposedModuleServes(t *testing.T) { dev := person(t, k, "fixture-dev") out, err := k.Issuer.Issue(dev, &authorityv1.IssueRequest{ As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "probe", Scopes: scopes(testkit.Scope("probe.items", "run"))}) - if err != nil || out.GetContext().GetAuthorityScopes()[0].GetActions()[0] != "run" { - t.Fatalf("an explicit ask at a kinds-only module: %v %v", out.GetContext().GetAuthorityScopes(), err) + if !errors.Is(err, issuer.ErrRefused) || out != nil { + t.Fatalf("undeclared action was invented: %v %v", out, err) } if _, err := k.Issuer.Issue(dev, &authorityv1.IssueRequest{ As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "probe"}); !errors.Is(err, issuer.ErrRefused) { diff --git a/modules/authority/services/gateway/code/pkg/business/state/permissions.go b/modules/authority/services/gateway/code/pkg/business/state/permissions.go new file mode 100644 index 00000000..41eb55e7 --- /dev/null +++ b/modules/authority/services/gateway/code/pkg/business/state/permissions.go @@ -0,0 +1,134 @@ +package state + +import ( + "fmt" + "slices" + "sort" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" +) + +// refreshDeveloperRoles replaces, never accumulates, the local fixture's +// generated union. It does not assign roles to people or change business roles. +// Caller holds mu. Explicit non-method ceilings remain supported. +func (s *State) refreshDeveloperRoles() { + union := s.permissionCatalog() + for key, role := range s.roles { + if !role.Generated { + continue + } + role.Scopes = nil + for _, sc := range union { + role.Scopes = append(role.Scopes, Scope{Kind: sc.GetResourceKind(), Actions: append([]string(nil), sc.GetActions()...)}) + } + s.roles[key] = role + } +} + +// permissionCatalog contains concrete declarations only. Caller holds mu. +func (s *State) permissionCatalog() []*basev0.WorkScopeV1 { + var union []*basev0.WorkScopeV1 + for _, m := range s.modules { + if m.Kind != "solution" { + for _, sc := range m.ResourceKinds { + if sc.GetResourceKind() == AnyKind { + continue + } + var actions []string + for _, action := range sc.GetActions() { + if action != AnyAction { + actions = append(actions, action) + } + } + if len(actions) > 0 { + union = append(union, &basev0.WorkScopeV1{ResourceKind: sc.GetResourceKind(), Actions: actions}) + } + } + } + } + union = Normalize(union) + return union +} + +// PermissionReport describes a snapshot of delivered generations. Coverage +// means static eligibility only: identity, installations, consent and resource +// visibility are still evaluated at use time. Undelivered kinds are unresolved. +func (s *State) PermissionReport() []string { + s.mu.RLock() + defer s.mu.RUnlock() + var lines []string + catalog := s.permissionCatalog() + kinds := map[string]bool{} + for _, m := range s.modules { + if m.Kind == "solution" { + continue + } + for _, sc := range m.ResourceKinds { + kinds[sc.GetResourceKind()] = true + } + } + for key, role := range s.roles { + for _, sc := range role.Scopes { + if sc.Kind == AnyKind { + continue + } + for _, action := range sc.Actions { + if action == AnyAction { + continue + } + if !kinds[sc.Kind] { + lines = append(lines, fmt.Sprintf("WARN role %s: %s:%s unresolved in current delivery", key, sc.Kind, action)) + } else if !holdsAction(catalog, sc.Kind, action) { + lines = append(lines, fmt.Sprintf("WARN role %s: %s:%s is not declared by current delivery", key, sc.Kind, action)) + } + } + } + } + for _, m := range s.modules { + if len(m.Methods) == 0 { + lines = append(lines, fmt.Sprintf("WARN %s@%d: no delivered method policies; declared ceilings alone do not admit a call", m.Module, m.Generation)) + } + for _, sc := range m.ResourceKinds { + if sc.GetResourceKind() == AnyKind || slices.Contains(sc.GetActions(), AnyAction) { + lines = append(lines, fmt.Sprintf("WARN %s@%d %s: wildcard ceiling is unresolved; generated developer uses explicit actions only", m.Module, m.Generation, sc.GetResourceKind())) + } + } + roles := map[string][]*basev0.WorkScopeV1{} + if m.Kind == "solution" { + for name, scopes := range m.Roles { + roles["installation/"+name] = scopes + } + } else { + for key, role := range s.roles { + var scopes []*basev0.WorkScopeV1 + for _, sc := range role.Scopes { + if sc.Kind == AnyKind { + for _, kind := range m.ResourceKinds { + scopes = append(scopes, &basev0.WorkScopeV1{ResourceKind: kind.GetResourceKind(), Actions: sc.Actions}) + } + } else { + scopes = append(scopes, &basev0.WorkScopeV1{ResourceKind: sc.Kind, Actions: sc.Actions}) + } + } + roles[key] = Intersect(Normalize(scopes), m.ResourceKinds) + } + } + for method := range m.Methods { + covered := false + for role, scopes := range roles { + result := "covered statically; identity, installation and resource checks still apply" + if err := admitMethod(m, m.Principal, method, scopes); err != nil { + result = err.Error() + } else { + covered = true + } + lines = append(lines, fmt.Sprintf("role %s × %s@%d × %s: %s", role, m.Module, m.Generation, method, result)) + } + if !covered { + lines = append(lines, fmt.Sprintf("WARN %s@%d %s: no single declared role covers this method", m.Module, m.Generation, method)) + } + } + } + sort.Strings(lines) + return slices.Compact(lines) +} diff --git a/modules/authority/services/gateway/code/pkg/business/state/permissions_test.go b/modules/authority/services/gateway/code/pkg/business/state/permissions_test.go new file mode 100644 index 00000000..80d019f9 --- /dev/null +++ b/modules/authority/services/gateway/code/pkg/business/state/permissions_test.go @@ -0,0 +1,77 @@ +package state_test + +import ( + "gateway/pkg/business/state" + "gateway/pkg/business/testkit" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "slices" + "strings" + "testing" +) + +func TestGeneratedDeveloperTracksReplacementAndReportsBusinessGaps(t *testing.T) { + k := testkit.New(t) + m := state.Manifest{Module: "ledger", Principal: "ledger", Kind: "module", Generation: 1, + Methods: map[string]state.MethodPolicy{ + "/ledger/Write": {Requires: []*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}}, + "/ledger/Version": {AnyCheckedCaller: true}, + "/ledger/Missing": {}, + }} + before := k.State.RoleScopes("acme", "viewer") + if _, err := k.State.ApplyManifest(m); err != nil { + t.Fatal(err) + } + held, err := k.State.PersonScopes("dev", "ledger") + if err != nil || !state.Attenuates([]*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}, held) { + t.Fatalf("developer: %v %v", held, err) + } + report := strings.Join(k.State.PermissionReport(), "\n") + for _, want := range []string{"acme/developer × ledger@1 × /ledger/Write: covered statically", "acme/viewer × ledger@1 × /ledger/Write: state: forbidden", "/ledger/Missing: no single declared role", "resource checks still apply"} { + if !strings.Contains(report, want) { + t.Errorf("missing %q in report", want) + } + } + m.Generation = 2 + m.Methods = map[string]state.MethodPolicy{"/ledger/Read": {Requires: []*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "read")}}} + if _, err := k.State.ApplyManifest(m); err != nil { + t.Fatal(err) + } + held, err = k.State.PersonScopes("dev", "ledger") + if err != nil || state.Attenuates([]*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "write")}, held) { + t.Fatalf("removed write retained: %v %v", held, err) + } + if !state.Attenuates([]*basev0.WorkScopeV1{testkit.Scope("ledger.entry", "read")}, held) { + t.Fatal("new read missing") + } + after := k.State.RoleScopes("acme", "viewer") + if !state.Attenuates(before, after) || !state.Attenuates(after, before) { + t.Fatal("business role changed") + } + for _, sc := range k.State.RoleScopes("acme", "developer") { + if sc.GetResourceKind() == "*" { + t.Fatal("generated wildcard") + } + } +} + +func TestGeneratedDeveloperNeverCopiesWildcardCarrierCeilings(t *testing.T) { + k := testkit.New(t) + m := state.Manifest{Module: "unresolved", Principal: "unresolved", Kind: "module", Generation: 1, + ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope("unresolved.entry", "*")}, + Methods: map[string]state.MethodPolicy{"/unresolved/Read": {Requires: []*basev0.WorkScopeV1{testkit.Scope("unresolved.entry", "read")}}}} + if _, err := k.State.ApplyManifest(m); err != nil { + t.Fatal(err) + } + held := k.State.RoleScopes("acme", "developer") + for _, scope := range held { + if scope.GetResourceKind() == "*" || slices.Contains(scope.GetActions(), "*") { + t.Fatalf("wildcard leaked: %v", scope) + } + } + if !state.Attenuates([]*basev0.WorkScopeV1{testkit.Scope("unresolved.entry", "read")}, held) { + t.Fatal("explicit method permission lost") + } + if !strings.Contains(strings.Join(k.State.PermissionReport(), "\n"), "wildcard ceiling is unresolved") { + t.Fatal("missing wildcard diagnostic") + } +} diff --git a/modules/authority/services/gateway/code/pkg/business/state/state.go b/modules/authority/services/gateway/code/pkg/business/state/state.go index 66aeb5cc..854a4b9e 100644 --- a/modules/authority/services/gateway/code/pkg/business/state/state.go +++ b/modules/authority/services/gateway/code/pkg/business/state/state.go @@ -10,6 +10,7 @@ import ( "encoding/hex" "errors" "fmt" + delivery "github.com/codefly-dev/authority/libraries/delivery/go" resources "github.com/codefly-dev/authority/libraries/resources/go" "slices" "sort" @@ -67,6 +68,7 @@ type Person struct { type Role struct { Organization string `yaml:"organization"` Name string `yaml:"name"` + Generated bool `yaml:"generated,omitempty"` Scopes []Scope `yaml:"scopes"` } @@ -186,6 +188,11 @@ func (s *State) AdmitMethod(audience, method string, held []*basev0.WorkScopeV1) if err != nil { return err } + return admitMethod(m, audience, method, held) +} + +func admitMethod(m Manifest, audience, method string, held []*basev0.WorkScopeV1) error { + var err error if m.Methods == nil { return fmt.Errorf("%w: %s's delivery declares no method policy, so %s is refused", ErrForbidden, audience, method) } @@ -486,20 +493,27 @@ func (s *State) LoadFixture(ctx context.Context, data []byte) error { s.ensureModuleInstallations(m) } for _, r := range f.Roles { + if r.Generated && (r.Name != "developer" || len(r.Scopes) != 0) { + s.mu.Unlock() + return fmt.Errorf("fixture: generated role must be developer with no authored scopes") + } for _, sc := range r.Scopes { grants := make([]resources.Grant, 0, len(sc.Resources)) for _, m := range sc.Resources { grants = append(grants, m.Grant()) } if _, err := resources.Compile(grants); err != nil { + s.mu.Unlock() return fmt.Errorf("role %s/%s scope %s: %w", r.Organization, r.Name, sc.Kind, err) } } s.roles[r.Organization+"/"+r.Name] = r } + s.refreshDeveloperRoles() var fixtureModules []Manifest for _, d := range f.Delegations { if d.ID == "" || d.Grantor == "" || d.Module == "" || d.Organization == "" { + s.mu.Unlock() return fmt.Errorf("delegation %q names no id, grantor, module or organization", d.ID) } s.delegations[d.ID] = d @@ -771,6 +785,15 @@ func (s *State) ResourceGrants(person, audience, kind, action string) ([]resourc // module installation at every organisation, so a module is usable without a // click. A lower or equal generation is refused. func (s *State) ApplyManifest(m Manifest) ([]string, error) { + for method, policy := range m.Methods { + requires := map[string][]string{} + for _, scope := range policy.Requires { + requires[scope.GetResourceKind()] = append(requires[scope.GetResourceKind()], scope.GetActions()...) + } + if err := delivery.ValidateMethodPolicy(method, policy.AnyCheckedCaller, requires, false); err != nil { + return nil, fmt.Errorf("%w: %s: %w", ErrForbidden, m.Module, err) + } + } if err := validateRoles(m); err != nil { return nil, err } @@ -806,6 +829,7 @@ func (s *State) ApplyManifest(m Manifest) ([]string, error) { } m.ResourceKinds = Normalize(append(m.ResourceKinds, servedByMethods(m.Methods)...)) s.modules[m.Module] = m + s.refreshDeveloperRoles() s.principals[m.Principal] = m.Module ids := s.ensureModuleInstallations(m) pending := s.takePendingInstalls(m.Module) diff --git a/modules/authority/services/gateway/code/plugins/composition/plugin.go b/modules/authority/services/gateway/code/plugins/composition/plugin.go index 263edf24..98925317 100644 --- a/modules/authority/services/gateway/code/plugins/composition/plugin.go +++ b/modules/authority/services/gateway/code/plugins/composition/plugin.go @@ -6,6 +6,7 @@ import ( "crypto" "gateway/pkg/framework" "gateway/pkg/wiring" + "log" "strings" delivery "github.com/codefly-dev/authority/libraries/delivery/go" @@ -85,6 +86,9 @@ func (p *Plugin) ApplyManifest(ctx context.Context, req *compositionv1.ApplyMani if err != nil { return nil, authorityplugin.Status(err) } + for _, line := range p.State.PermissionReport() { + log.Printf("permissions: %s", line) + } return &compositionv1.ApplyManifestResponse{Principal: m.Principal, Generation: m.Generation, InstallationIds: ids}, nil } From e7afe12736f5d6b0df22698de4e8f6de57d952ff Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:44:15 -0400 Subject: [PATCH 05/45] fix: canonicalize issued teams and complete local collection grants --- modules/authority/services/gateway/README.md | 7 ++ .../services/gateway/code/fixtures/local.yaml | 1 + .../code/pkg/business/issuer/issuer.go | 6 +- .../code/pkg/business/issuer/issuer_test.go | 78 +++++++++++++++++++ .../code/pkg/business/testkit/testkit.go | 2 + 5 files changed, 93 insertions(+), 1 deletion(-) diff --git a/modules/authority/services/gateway/README.md b/modules/authority/services/gateway/README.md index 28658da9..6987f257 100644 --- a/modules/authority/services/gateway/README.md +++ b/modules/authority/services/gateway/README.md @@ -58,3 +58,10 @@ policy validator in the delivery library, requiring explicit method declarations `go test -race ./...` in `code/` includes replacement/removal and fixture coverage regressions. This does not qualify the production host or live browser journeys. + +Person issuance sorts and deduplicates a copy of the session's team attribution +before signing. Fixture order therefore does not affect a module's ability to +consume the context; the original session remains unchanged. The local Globex +administrator explicitly holds `documents.collection:read` alongside document +read/write; the role name itself implies no additional grants. Issuer regressions +and the shared fixture matrix cover this grant and a person who lacks it. diff --git a/modules/authority/services/gateway/code/fixtures/local.yaml b/modules/authority/services/gateway/code/fixtures/local.yaml index ef0c264c..49287339 100644 --- a/modules/authority/services/gateway/code/fixtures/local.yaml +++ b/modules/authority/services/gateway/code/fixtures/local.yaml @@ -74,6 +74,7 @@ roles: name: admin scopes: - {kind: documents, actions: [read, write]} + - {kind: documents.collection, actions: [read]} # Generated from the current delivered catalog, recomputed on each delivery, # in Acme. A playground signs its person in as dev. - organization: acme diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go b/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go index a144a7c7..17b9fa2a 100644 --- a/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go +++ b/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go @@ -262,11 +262,15 @@ func (i *Issuer) forPerson(ctx context.Context, sessionID string, audience state if err != nil { return "", nil, err } + // Attribution is a set on the wire; never sort the session's shared slice. + teams := slices.Clone(sess.Teams) + slices.Sort(teams) + teams = slices.Compact(teams) return i.authority.Start(ctx, workcontext.StartInput{ TenantID: sess.Organization, OrganizationID: sess.Organization, OwnerPrincipalID: sess.Person, OwnerPrincipalKind: "human", TaskID: uuid.NewString(), SessionID: sess.ID, - Audience: audience.Principal, AuthorityScopes: scopes, AttributionTeamIDs: sess.Teams, + Audience: audience.Principal, AuthorityScopes: scopes, AttributionTeamIDs: teams, InstallationID: state.InstallationID(sess.Organization, audience.Module), TTL: ttl, }) } diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go b/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go index b47f0722..b3011ca2 100644 --- a/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go +++ b/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go @@ -5,6 +5,7 @@ import ( "crypto/ed25519" "encoding/base64" "errors" + "slices" "strings" "testing" "time" @@ -489,3 +490,80 @@ func TestTheDeveloperRoleHoldsWhatEachComposedModuleServes(t *testing.T) { t.Fatal("a viewer holds nothing at a module her roles do not name") } } + +// Team attribution is a set on the signed wire. Fixture order must not make a +// valid person unusable by modules that require canonical attribution. +func TestPersonIssuanceCanonicalizesTeamsWithoutChangingTheSession(t *testing.T) { + for _, duplicate := range []bool{false, true} { + name := "fixture-order" + if duplicate { + name = "duplicate-membership" + } + t.Run(name, func(t *testing.T) { + k := testkit.New(t) + if duplicate { + err := k.State.LoadFixture(context.Background(), []byte(`people: + - id: alex + fixture_token: fixture-alex + organization: acme + roles: [admin] + teams: [finance, engineering, finance] +`)) + if err != nil { + t.Fatal(err) + } + } + session, err := k.State.SignIn("fixture-alex") + if err != nil { + t.Fatal(err) + } + before := slices.Clone(session.Teams) + ctx := issuer.WithSession(context.Background(), session.ID) + out, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", + }) + if err != nil { + t.Fatal(err) + } + checked, err := k.Issuer.Check(ctx, out.GetToken(), "documents", "") + if err != nil { + t.Fatal(err) + } + want := []string{"engineering", "finance"} + if got := checked.GetContext().GetAttributionTeamIds(); !slices.Equal(got, want) { + t.Fatalf("signed attribution teams = %v, want %v", got, want) + } + after, err := k.State.Session(session.ID) + if err != nil { + t.Fatal(err) + } + if !slices.Equal(after.Teams, before) { + t.Fatalf("issuance changed session teams from %v to %v", before, after.Teams) + } + }) + } +} + +func TestGlobexCollectionReadIsAnExplicitFixtureGrant(t *testing.T) { + k := testkit.New(t) + for _, token := range []string{"fixture-zoe", "fixture-omar"} { + t.Run(token, func(t *testing.T) { + out, err := k.Issuer.Issue(person(t, k, token), &authorityv1.IssueRequest{ + As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", + Scopes: scopes(testkit.Scope("documents.collection", "read")), + }) + if token == "fixture-omar" { + if !errors.Is(err, issuer.ErrRefused) { + t.Fatalf("ungranted collection read: %v", err) + } + return + } + if err != nil { + t.Fatal(err) + } + if out.GetContext().GetOrganizationId() != "globex" { + t.Fatal("grant escaped its organization") + } + }) + } +} diff --git a/modules/authority/services/gateway/code/pkg/business/testkit/testkit.go b/modules/authority/services/gateway/code/pkg/business/testkit/testkit.go index e5c1ffd4..cbf18a35 100644 --- a/modules/authority/services/gateway/code/pkg/business/testkit/testkit.go +++ b/modules/authority/services/gateway/code/pkg/business/testkit/testkit.go @@ -241,6 +241,8 @@ func Matrix() []conformance.Expectation { // Globex: its own installation of the wiki, and a documents role of its own. allow("fixture-zoe", "wiki", "wiki.page", "publish"), allow("fixture-omar", "wiki", "wiki.page", "write"), deny("fixture-omar", "wiki", "wiki.page", "publish"), allow("fixture-zoe", "documents", "documents", "write"), + allow("fixture-zoe", "documents", "documents.collection", "read"), + deny("fixture-omar", "documents", "documents.collection", "read"), deny("fixture-omar", "documents", "documents", "read"), } } From b55560873daa82ae7150eb72bc83fff760c87cd9 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sat, 24 Oct 2026 21:57:51 -0400 Subject: [PATCH 06/45] fix: preserve hop advisory diagnostics in Python --- libraries/hop/go/fixture_test.go | 2 +- libraries/hop/python/README.md | 5 +++++ libraries/hop/python/src/codefly_hop/__init__.py | 4 ++++ libraries/hop/python/tests/test_read.py | 14 ++++++++++++++ libraries/hop/testdata/stamped.json | 1 + 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/libraries/hop/go/fixture_test.go b/libraries/hop/go/fixture_test.go index b917fdb1..d18a4e41 100644 --- a/libraries/hop/go/fixture_test.go +++ b/libraries/hop/go/fixture_test.go @@ -17,7 +17,7 @@ var updateFixture = flag.Bool("update-fixture", false, "rewrite ../testdata/stam // (../python) reads back in its own tests. func TestTheStampFixtureIsWhatTheHopWrites(t *testing.T) { md := metadata.Pairs(WorkContextKey, "eyJ3YyI6ImZpeHR1cmUifQ.c2lnbmF0dXJl") - Stamp(md, Identity{Owner: "remi", OwnerKind: "human", Tenant: "acme", Actors: []string{"robin"}, Scopes: []string{"modelservice.profiles:read", "modelservice.profiles:invoke"}, Binding: "acme/robin/model", Task: "task-7", Expires: 1792818000}, "fixture-proof") + Stamp(md, Identity{Owner: "remi", OwnerKind: "human", Tenant: "acme", Actors: []string{"robin"}, Scopes: []string{"modelservice.profiles:read", "modelservice.profiles:invoke"}, Binding: "acme/robin/model", Task: "task-7", Expires: 1792818000, Advisory: "documents.collection:read is not held"}, "fixture-proof") headers := map[string]string{} for key, values := range md { headers[key] = values[0] diff --git a/libraries/hop/python/README.md b/libraries/hop/python/README.md index 544d5b8b..4d2caa60 100644 --- a/libraries/hop/python/README.md +++ b/libraries/hop/python/README.md @@ -11,6 +11,11 @@ identity = read(request.headers, proof) # proof: the run's hop/PROOF identity.tenant, identity.owner, identity.has("modelservice.profiles", "read") ``` +`identity.advisory` preserves the hop's reason for admitting a call in warning +mode; it is empty when no reason was stamped. It is diagnostic information: +reading it grants no scopes and leaves proof and Work Context binding checks +unchanged. Consumers decide how to report it alongside their own domain checks. + A module depends on it by its git coordinates at a pinned commit: `codefly-hop @ git+https://github.com/codefly-dev/authority@#subdirectory=libraries/hop/python`. diff --git a/libraries/hop/python/src/codefly_hop/__init__.py b/libraries/hop/python/src/codefly_hop/__init__.py index c119b584..f66c9960 100644 --- a/libraries/hop/python/src/codefly_hop/__init__.py +++ b/libraries/hop/python/src/codefly_hop/__init__.py @@ -30,6 +30,7 @@ TASK_KEY = STAMP_PREFIX + "task" EXPIRES_KEY = STAMP_PREFIX + "expires" CONTEXT_KEY = STAMP_PREFIX + "context" +ADVISORY_KEY = STAMP_PREFIX + "advisory" class NoStamp(Exception): @@ -58,6 +59,8 @@ class Identity: expires: int = 0 # The Work Context the authority checked, as forwarded. context: str = field(default="", repr=False) + # The refusal the hop allowed in warning mode. + advisory: str = "" def has(self, kind: str, action: str) -> bool: """Whether the call may do action on kind.""" @@ -97,6 +100,7 @@ def read(headers: Mapping[str, str] | Iterable[tuple[str, str]], proof: str) -> task=_first(values, TASK_KEY), expires=expires, context=context, + advisory=_first(values, ADVISORY_KEY), ) diff --git a/libraries/hop/python/tests/test_read.py b/libraries/hop/python/tests/test_read.py index 57b0765f..54ca3545 100644 --- a/libraries/hop/python/tests/test_read.py +++ b/libraries/hop/python/tests/test_read.py @@ -4,6 +4,7 @@ import pytest from codefly_hop import ( + ADVISORY_KEY, CONTEXT_KEY, EXPIRES_KEY, PROOF_KEY, @@ -33,6 +34,7 @@ def test_the_stamp_the_hop_writes_reads_back_as_its_identity(): assert identity.binding == "acme/robin/model" assert (identity.task, identity.expires) == ("task-7", 1792818000) assert identity.context == FIXTURE["headers"][WORK_CONTEXT_KEY] + assert identity.advisory == "documents.collection:read is not held" assert identity.has("modelservice.profiles", "invoke") assert not identity.has("modelservice.profiles", "admin") @@ -60,3 +62,15 @@ def test_header_names_are_case_insensitive_and_repeats_are_kept(): def test_a_call_without_this_runs_stamp_acts_for_no_one(headers, proof): with pytest.raises(NoStamp): read(headers, proof) + + +def test_advisory_is_preserved_without_changing_scope_or_stamp_checks(): + reason = "documents.collection:read is not held" + headers = stamped(**{ADVISORY_KEY: reason}) + identity = read(headers, FIXTURE["proof"]) + assert identity.advisory == reason + assert not identity.has("documents.collection", "read") + assert identity.context == FIXTURE["headers"][WORK_CONTEXT_KEY] + assert read(stamped(**{ADVISORY_KEY: None}), FIXTURE["proof"]).advisory == "" + with pytest.raises(NoStamp): + read(headers, "wrong-proof") diff --git a/libraries/hop/testdata/stamped.json b/libraries/hop/testdata/stamped.json index d6bb99e6..3a410fea 100644 --- a/libraries/hop/testdata/stamped.json +++ b/libraries/hop/testdata/stamped.json @@ -1,6 +1,7 @@ { "headers": { "x-codefly-hop-actors": "robin", + "x-codefly-hop-advisory": "documents.collection:read is not held", "x-codefly-hop-binding": "acme/robin/model", "x-codefly-hop-context": "cde194a7f3368961e48f1937c63fc67e058b2717d9c0581ac0eb08874ca8b714", "x-codefly-hop-expires": "1792818000", From e1b061fe2f443e0d41a12596f9742ec27f802ce7 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:15:11 -0400 Subject: [PATCH 07/45] feat: expose bound context resource scopes to Python consumers --- README.md | 6 +- libraries/authority-client/python/README.md | 54 ++++ .../python/authority_client/work_context.py | 93 ++++++ .../python/tests/test_work_context.py | 116 +++++++ .../authority-client/testdata/claims.json | 302 ++++++++++++++++++ .../pkg/business/issuer/python_claims_test.go | 122 +++++++ 6 files changed, 691 insertions(+), 2 deletions(-) create mode 100644 libraries/authority-client/python/README.md create mode 100644 libraries/authority-client/python/authority_client/work_context.py create mode 100644 libraries/authority-client/python/tests/test_work_context.py create mode 100644 libraries/authority-client/testdata/claims.json create mode 100644 modules/authority/services/gateway/code/pkg/business/issuer/python_claims_test.go diff --git a/README.md b/README.md index dd32d23b..46e2ae6d 100644 --- a/README.md +++ b/README.md @@ -304,8 +304,10 @@ consumer's organisation in an import path). GitHub redirects the old location. `libraries/authority-client/go` is the generated Go bindings library (`codefly generate client --no-facade`), importable as `github.com/codefly-dev/authority/libraries/authority-client/go`. The Python -library did not generate: the client generator lacks the protovalidate dependency -the Work Context messages import (reported on codefly-dev/cli#1107). +library publishes generated bindings under `libraries/authority-client/python`. +Its [bound-context reader](libraries/authority-client/python/README.md) projects +effective resource scopes without another verification call. Consumers read the +hop stamp first and retain their domain resource and installation checks. ## Rules of change (owner, 2026-10-22) diff --git a/libraries/authority-client/python/README.md b/libraries/authority-client/python/README.md new file mode 100644 index 00000000..b39b9a2b --- /dev/null +++ b/libraries/authority-client/python/README.md @@ -0,0 +1,54 @@ +# Authority Python client + +The Codefly-generated authority bindings are accompanied by a small, non-verifying +Work Context reader. A module first reads the hop stamp, then projects its bound +context through the generated protobuf type: + +```python +from codefly_hop import read +from authority_client.work_context import read_claims + +identity = read(request.headers, proof) +claims = read_claims(identity.context) +# Compare claims.audience to this module's delivered audience. +allowed = claims.allows(resource_kind, action, installed_resource_id) +``` + +The reader preserves Core's effective scopes: the final actor's grants, or the +owner's grants when there is no actor. An empty final grant stays empty. Each +scope is evaluated independently, so an action on one resource is not combined +with an unrelated resource from another scope. An omitted resource ID asks for +whole-kind authority; a resource-limited grant cannot satisfy it. A whole-kind +grant can satisfy a named-resource request. Actions are literal, following the +existing Core containment rule and Python SDK; `admin` implies no other action, +and raw token `*` actions are not expanded here. + +`read_claims` raises `UnreadableContext` for an unreadable envelope, unknown wire +fields, noncanonical payload or absent audience. It does **not** replace Core's +structural validation, authenticate a signature, check time/revocation or consume +a replay nonce. In warning mode even a stamped context may have been refused by +the authority. The advisory is diagnostic and grants nothing. Keep the module's +installed-definition, audience, expiry, ownership and visibility checks. Resolve +resource IDs from the domain object; never use a kind-wide fallback when a bound +context is missing or unreadable. + +This package directly reuses its generated `WorkContextV1` binding. Its canonical +wire and exact-resource checks follow the existing Python SDK's algorithms; +they are maintained here during migration, not imported from its private helpers. +SDK changes therefore require an explicit parity review until it adopts this seam. +It does not depend on a running SaaS service or introduce another verifier. + +Pin consumers by repository commit, together with the hop package: +`authority-client @ git+https://github.com/codefly-dev/authority@#subdirectory=libraries/authority-client/python`. + +Run the Python tests from this directory: + +```sh +uv run --no-project --with pytest --with-editable . --with-editable ../../hop/python pytest tests +``` + +The shared `../testdata/claims.json` contains Core-minted tokens and effective +scope/query expectations. The gateway's `TestPythonClaimProjectionFixtures` +verifies them against Core with a fixed test clock; use its +`-update-python-claims` flag to regenerate. These public conformance tokens are +test data, never credentials for a running installation. diff --git a/libraries/authority-client/python/authority_client/work_context.py b/libraries/authority-client/python/authority_client/work_context.py new file mode 100644 index 00000000..f01c80a9 --- /dev/null +++ b/libraries/authority-client/python/authority_client/work_context.py @@ -0,0 +1,93 @@ +"""Non-verifying projection of the Work Context already bound by a hop stamp. + +Call ``codefly_hop.read`` first, then pass ``identity.context`` to ``read_claims``. +This is a reader, not Core's structural validator or a verifier: it checks neither +signature, liveness, revocation nor delegation. In warning mode the hop may have +forwarded a refused context; its advisory remains diagnostic, never a grant. +""" + +from __future__ import annotations + +import base64 +from dataclasses import dataclass + +from google.protobuf.message import DecodeError + +from ._gen.host.authority.v1.work_context_pb2 import WorkContextV1 + +# The existing Core and Python SDK envelope bound, before decoding allocates. +MAX_TOKEN_BYTES = 32 * 1024 + + +class UnreadableContext(ValueError): + """The forwarded token cannot be projected through the published wire type.""" + + +@dataclass(frozen=True) +class Scope: + resource_kind: str + actions: tuple[str, ...] + resource_ids: tuple[str, ...] + + +@dataclass(frozen=True) +class Claims: + audience: str + # Core's effective scopes: the last actor's grants, or the owner's if no actor. + scopes: tuple[Scope, ...] + + def allows(self, kind: str, action: str, resource_id: str = "") -> bool: + """One exact action on one resource; empty ID asks for the whole kind. + + Each grant is considered independently. Actions are literal, as in Core's + scope containment and the Python SDK; no admin implication or wildcard + expansion happens here. A kind-wide grant includes any named resource. + """ + if not kind or not action: + return False + return any( + scope.resource_kind == kind + and action in scope.actions + and (not scope.resource_ids or bool(resource_id) and resource_id in scope.resource_ids) + for scope in self.scopes + ) + + +def read_claims(token: str) -> Claims: + """Read an already hop-bound context without another verification/replay call. + + Uses the published generated protobuf type. The canonical-envelope/payload + checks follow the existing Python SDK's algorithm, maintained here during + migration rather than imported from its private helpers. Unknown fields are + refused rather than silently losing a restriction. Consumers still check audience and resolve the + exact domain resource; this projection alone is never proof of authority. + """ + if not token or not token.isascii() or len(token) > MAX_TOKEN_BYTES or token.count(".") != 1: + raise UnreadableContext("expected a bounded . context") + payload_segment, signature_segment = token.split(".") + payload = _decode_segment(payload_segment) + if len(_decode_segment(signature_segment)) != 64: + raise UnreadableContext("expected a 64-byte signature") + context = WorkContextV1() + try: + context.ParseFromString(payload) + except DecodeError as error: + raise UnreadableContext("payload is not a WorkContextV1") from error + context.DiscardUnknownFields() + if context.SerializeToString(deterministic=True) != payload or not context.audience: + raise UnreadableContext("payload is noncanonical, has unknown fields or names no audience") + scopes = context.actor_chain[-1].granted_scopes if context.actor_chain else context.authority_scopes + return Claims( + audience=context.audience, + scopes=tuple(Scope(s.resource_kind, tuple(s.actions), tuple(s.resource_ids)) for s in scopes), + ) + + +def _decode_segment(segment: str) -> bytes: + try: + decoded = base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4)) + except ValueError as error: + raise UnreadableContext("context segment is not base64url") from error + if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != segment: + raise UnreadableContext("context segment is not canonical base64url") + return decoded diff --git a/libraries/authority-client/python/tests/test_work_context.py b/libraries/authority-client/python/tests/test_work_context.py new file mode 100644 index 00000000..fb9059e1 --- /dev/null +++ b/libraries/authority-client/python/tests/test_work_context.py @@ -0,0 +1,116 @@ +import base64 +import hashlib +import json +from dataclasses import FrozenInstanceError +from pathlib import Path + +import pytest + +from authority_client._gen.host.authority.v1.work_context_pb2 import WorkContextV1 +from authority_client.work_context import MAX_TOKEN_BYTES, Claims, Scope, UnreadableContext, read_claims +from codefly_hop import CONTEXT_KEY, WORK_CONTEXT_KEY, NoStamp, read + +FIXTURES = json.loads((Path(__file__).parents[2] / "testdata/claims.json").read_text()) +STAMP = json.loads((Path(__file__).parents[3] / "hop/testdata/stamped.json").read_text()) + + +def encoded(payload): + return base64.urlsafe_b64encode(payload).rstrip(b"=").decode() + + +def token_for(context): + # Projection-only unit tests. These are not authenticated capabilities. + return encoded(context.SerializeToString(deterministic=True)) + "." + encoded(bytes(64)) + + +@pytest.mark.parametrize("fixture", FIXTURES, ids=lambda f: f["name"]) +def test_core_minted_and_verified_contexts_match_the_owner_projection(fixture): + claims = read_claims(fixture["token"]) + assert claims.audience == fixture["audience"] + assert claims.scopes == tuple( + Scope(s["resource_kind"], tuple(s.get("actions", [])), tuple(s.get("resource_ids", []))) + for s in fixture["scopes"] + ) + for probe in fixture["probes"]: + assert claims.allows(probe["kind"], probe["action"], probe["resource_id"]) == probe["allowed"] + + +def test_resource_ids_and_actions_are_never_combined_across_grants(): + context = WorkContextV1(audience="model") + context.authority_scopes.add(resource_kind="profiles", actions=["read"], resource_ids=["a"]) + context.authority_scopes.add(resource_kind="profiles", actions=["invoke"], resource_ids=["b"]) + claims = read_claims(token_for(context)) + assert claims.allows("profiles", "read", "a") + assert claims.allows("profiles", "invoke", "b") + assert not claims.allows("profiles", "invoke", "a") + assert not claims.allows("profiles", "read", "b") + assert not claims.allows("profiles", "read") + assert not claims.allows("other", "read", "a") + with pytest.raises(FrozenInstanceError): + claims.audience = "other" + + +def test_last_actor_grants_are_used_even_when_empty(): + context = WorkContextV1(audience="model") + context.authority_scopes.add(resource_kind="profiles", actions=["read", "invoke"]) + first = context.actor_chain.add(principal_id="first") + first.granted_scopes.add(resource_kind="profiles", actions=["read"], resource_ids=["a"]) + claims = read_claims(token_for(context)) + assert claims.allows("profiles", "read", "a") + assert not claims.allows("profiles", "invoke", "a") + assert not claims.allows("profiles", "read", "b") + context.actor_chain.add(principal_id="last") + assert read_claims(token_for(context)).scopes == () + + +def test_kind_wide_grants_include_resources_without_action_implications(): + claims = Claims("model", (Scope("profiles", ("read", "admin", "*"), ()),)) + assert claims.allows("profiles", "read") + assert claims.allows("profiles", "read", "a") + assert not claims.allows("profiles", "invoke", "a") + assert not claims.allows("profiles", "") + assert not claims.allows("", "read") + + +@pytest.mark.parametrize("token", ["", "abc", "a.b.c", ".", "a.b", "a." + encoded(bytes(64)), + "é.x", "\ud800.x", "a" * (MAX_TOKEN_BYTES + 1), encoded(b"{}") + "." + encoded(bytes(64)), + token_for(WorkContextV1()), token_for(WorkContextV1(audience="model")) + "=", +]) +def test_unreadable_envelopes_are_named(token): + with pytest.raises(UnreadableContext): + read_claims(token) + + +def test_unknown_nested_fields_and_duplicate_wire_fields_are_refused(): + context = WorkContextV1(audience="model") + scope = context.authority_scopes.add(resource_kind="profiles", actions=["read"]) + scope.ParseFromString(scope.SerializeToString() + b"\x78\x01") + with pytest.raises(UnreadableContext): + read_claims(token_for(context)) + payload = WorkContextV1(audience="model").SerializeToString() + with pytest.raises(UnreadableContext): + read_claims(encoded(payload + payload) + "." + encoded(bytes(64))) + + +def test_resource_projection_uses_the_bound_token_and_never_the_flat_stamp(): + fixture = next(f for f in FIXTURES if f["name"] == "delegated") + headers = dict(STAMP["headers"]) + headers[WORK_CONTEXT_KEY] = fixture["token"] + headers[CONTEXT_KEY] = hashlib.sha256(fixture["token"].encode()).hexdigest() + identity = read(headers, STAMP["proof"]) + claims = read_claims(identity.context) + assert claims == read_claims(fixture["token"]) + assert not claims.allows("documents.collection", "read") # advisory is not a grant + other_token = next(f["token"] for f in FIXTURES if f["token"] != fixture["token"]) + for changed in [dict(headers, **{WORK_CONTEXT_KEY: other_token}), + list(headers.items()) + [(WORK_CONTEXT_KEY, fixture["token"])]]: + with pytest.raises(NoStamp): + read(changed, STAMP["proof"]) + headers.pop(CONTEXT_KEY) + with pytest.raises(UnreadableContext): + read_claims(read(headers, STAMP["proof"]).context) + + +def test_projection_does_not_claim_to_verify_signature(): + token = FIXTURES[0]["token"] + assert read_claims(token) == read_claims(token.split(".")[0] + "." + encoded(bytes(64))) diff --git a/libraries/authority-client/testdata/claims.json b/libraries/authority-client/testdata/claims.json new file mode 100644 index 00000000..5ddc748b --- /dev/null +++ b/libraries/authority-client/testdata/claims.json @@ -0,0 +1,302 @@ +[ + { + "name": "delegated", + "token": "Chdjb2RlZmx5LndvcmstY29udGV4dC92MRIHRWQyNTUxORoNY29uZm9ybWFuY2UtMSIeaHR0cHM6Ly9hdXRob3JpdHkuY29kZWZseS50ZXN0Khhjb2RlZmx5LnRlc3QvY29uZm9ybWFuY2UwoMX11gY4oMX11gZAsOH11gZKJGZmNTI5NTQ0LTFjMzItNDM1MS1hOGEwLWI4ODA1MDQzMGYzNFAHWgppZGVtcG90ZW50YhJ0ZW5hbnQtY29uZm9ybWFuY2VqD3ByaW5jaXBhbC1vd25lcnIQdGFzay1jb25mb3JtYW5jZXokZDczMGEwNjYtYWFjYy00ZDE4LTk2ZGItODdhNTU1MmQ1NWZkggEkZjdkMjNmYWQtZTI1MS00M2MzLWFmMzItM2M1M2U5Y2MwZWMyigEOCgZyZWNvcmQSBHJlYWSSAXAKD3ByaW5jaXBhbC1hY3RvchIFYWdlbnQaFmRlbGVnYXRpb24tY29uZm9ybWFuY2UiIgoGcmVjb3JkEgRyZWFkGhJyZWNvcmQtY29uZm9ybWFuY2UqGGNvZGVmbHkudGVzdC9hY3RvcjoxLjAuMDgFsgEFaHVtYW7KARhvcmdhbml6YXRpb24tY29uZm9ybWFuY2XSAR4IAhIYaW5zdGFsbGF0aW9uLWNvbmZvcm1hbmNlGAM.xHzsawwSVUPXqyBR6EfEk5vB5GePrMJAhDnZfiuG24EIHCYevVpUAW-PXs_otAQ3vSWIhB-UoW9LOLa3pnpqBA", + "audience": "codefly.test/conformance", + "scopes": [ + { + "resource_kind": "record", + "actions": [ + "read" + ], + "resource_ids": [ + "record-conformance" + ] + } + ], + "probes": [ + { + "kind": "record", + "action": "read", + "resource_id": "", + "allowed": false + }, + { + "kind": "record", + "action": "read", + "resource_id": "other-resource", + "allowed": false + }, + { + "kind": "record", + "action": "read", + "resource_id": "record-conformance", + "allowed": true + }, + { + "kind": "record", + "action": "read", + "resource_id": "record-conformance", + "allowed": true + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "other-resource", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "record-conformance", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "record-conformance", + "allowed": false + } + ] + }, + { + "name": "delegated-operation", + "token": "Chdjb2RlZmx5LndvcmstY29udGV4dC92MRIHRWQyNTUxORoNY29uZm9ybWFuY2UtMSIeaHR0cHM6Ly9hdXRob3JpdHkuY29kZWZseS50ZXN0Khhjb2RlZmx5LnRlc3QvY29uZm9ybWFuY2UwoMX11gY4oMX11gZAsOH11gZKJGY0Y2M3MDc2LThlYTItNDI3Ny1iYjBlLTkwZDE0ZjYzZjAxZFAHWgppZGVtcG90ZW50YhJ0ZW5hbnQtY29uZm9ybWFuY2VqD3ByaW5jaXBhbC1vd25lcnIQdGFzay1jb25mb3JtYW5jZXokZmIwOTg1ZmMtMjcxNi00NWE0LTlhZDUtMTg1Y2UwZDA4MGIyggEkZjdkMjNmYWQtZTI1MS00M2MzLWFmMzItM2M1M2U5Y2MwZWMyigEOCgZyZWNvcmQSBHJlYWSSAXAKD3ByaW5jaXBhbC1hY3RvchIFYWdlbnQaFmRlbGVnYXRpb24tY29uZm9ybWFuY2UiIgoGcmVjb3JkEgRyZWFkGhJyZWNvcmQtY29uZm9ybWFuY2UqGGNvZGVmbHkudGVzdC9hY3RvcjoxLjAuMDgFsgEFaHVtYW7KARhvcmdhbml6YXRpb24tY29uZm9ybWFuY2XSAR4IAhIYaW5zdGFsbGF0aW9uLWNvbmZvcm1hbmNlGAPaAR8KGWJpbmRpbmctY29uZm9ybWFuY2UtYWN0b3IQBBgB.igwjaNWjv_k76P78HjVpZ0WGIVLTzC31Kjd8HGgb5l_If6jTF6H32sfynx00zewIoykxvRJuRLHXxFQk7ZUVDw", + "audience": "codefly.test/conformance", + "scopes": [ + { + "resource_kind": "record", + "actions": [ + "read" + ], + "resource_ids": [ + "record-conformance" + ] + } + ], + "probes": [ + { + "kind": "record", + "action": "read", + "resource_id": "", + "allowed": false + }, + { + "kind": "record", + "action": "read", + "resource_id": "other-resource", + "allowed": false + }, + { + "kind": "record", + "action": "read", + "resource_id": "record-conformance", + "allowed": true + }, + { + "kind": "record", + "action": "read", + "resource_id": "record-conformance", + "allowed": true + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "other-resource", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "record-conformance", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "record-conformance", + "allowed": false + } + ] + }, + { + "name": "grant", + "token": "Chdjb2RlZmx5LndvcmstY29udGV4dC92MRIHRWQyNTUxORoNY29uZm9ybWFuY2UtMSIeaHR0cHM6Ly9hdXRob3JpdHkuY29kZWZseS50ZXN0Khhjb2RlZmx5LnRlc3QvY29uZm9ybWFuY2UwoMX11gY4oMX11gZA3MX11gZKJGIzNzgwZDEwLWFlYzAtNGJlYS05YTdiLTk4NzU1ODAxYzc0MFAHWgpzaW5nbGUtdXNlYhJ0ZW5hbnQtY29uZm9ybWFuY2VqD3ByaW5jaXBhbC1vd25lcnIQdGFzay1jb25mb3JtYW5jZXokYWEwYzdmMTQtY2ViYy00MDY1LThmYTEtYmNlMzMyZjE0YmY1ggEkZDczMGEwNjYtYWFjYy00ZDE4LTk2ZGItODdhNTU1MmQ1NWZkigEOCgZyZWNvcmQSBHJlYWSSAXAKD3ByaW5jaXBhbC1hY3RvchIFYWdlbnQaFmRlbGVnYXRpb24tY29uZm9ybWFuY2UiIgoGcmVjb3JkEgRyZWFkGhJyZWNvcmQtY29uZm9ybWFuY2UqGGNvZGVmbHkudGVzdC9hY3RvcjoxLjAuMDgFkgFuCg9wcmluY2lwYWwtYWN0b3ISBWFnZW50GhFncmFudC1jb25mb3JtYW5jZSIlCgZyZWNvcmQSB2FwcHJvdmUaEnJlY29yZC1jb25mb3JtYW5jZSoYY29kZWZseS50ZXN0L2FjdG9yOjEuMC4wOAWyAQVodW1hbsIBgQEKEWdyYW50LWNvbmZvcm1hbmNlEhsKEnByaW5jaXBhbC1hcHByb3ZlchIFaHVtYW4aJQoGcmVjb3JkEgdhcHByb3ZlGhJyZWNvcmQtY29uZm9ybWFuY2UiFHJlc291cmNlLWNvbmZvcm1hbmNlKhJzaGEyNTY6Y29uZm9ybWFuY2XKARhvcmdhbml6YXRpb24tY29uZm9ybWFuY2XSAR4IAhIYaW5zdGFsbGF0aW9uLWNvbmZvcm1hbmNlGAM.8J0ouWQivBDUs32wk8jv_mHqE7DXmEbcoXjcgknZOgNKFh3Ap2UkTgswvMWkLfXrSsgEkCZlMxAtIoe20rwACA", + "audience": "codefly.test/conformance", + "scopes": [ + { + "resource_kind": "record", + "actions": [ + "approve" + ], + "resource_ids": [ + "record-conformance" + ] + } + ], + "probes": [ + { + "kind": "record", + "action": "approve", + "resource_id": "", + "allowed": false + }, + { + "kind": "record", + "action": "approve", + "resource_id": "other-resource", + "allowed": false + }, + { + "kind": "record", + "action": "approve", + "resource_id": "record-conformance", + "allowed": true + }, + { + "kind": "record", + "action": "approve", + "resource_id": "record-conformance", + "allowed": true + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "other-resource", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "record-conformance", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "record-conformance", + "allowed": false + } + ] + }, + { + "name": "operation", + "token": "Chdjb2RlZmx5LndvcmstY29udGV4dC92MRIHRWQyNTUxORoNY29uZm9ybWFuY2UtMSIeaHR0cHM6Ly9hdXRob3JpdHkuY29kZWZseS50ZXN0Khhjb2RlZmx5LnRlc3QvY29uZm9ybWFuY2UwoMX11gY4oMX11gZAsOH11gZKJGRiYzBmYzgyLWQ0YzQtNGQ1NC1iN2RmLThjZTRhODZkYjlhMlAHWgppZGVtcG90ZW50YhJ0ZW5hbnQtY29uZm9ybWFuY2VqD3ByaW5jaXBhbC1vd25lcnIQdGFzay1jb25mb3JtYW5jZXokNDYzY2FkNjgtOGQxYi00ZmJkLWFkOTctZmUzZjI5NjdkMGFhigEOCgZyZWNvcmQSBHJlYWSyAQVodW1hbsoBGG9yZ2FuaXphdGlvbi1jb25mb3JtYW5jZdIBHggCEhhpbnN0YWxsYXRpb24tY29uZm9ybWFuY2UYA9oBGQoTYmluZGluZy1jb25mb3JtYW5jZRAEGAE.HaWwuxTDaXO0JA1gjwrHJ764CGsfNCRpmn1PuMYByH70X2lm_nbrxSnlDXe80IYR7hoq4NFGjq-xon08KTZiBQ", + "audience": "codefly.test/conformance", + "scopes": [ + { + "resource_kind": "record", + "actions": [ + "read" + ] + } + ], + "probes": [ + { + "kind": "record", + "action": "read", + "resource_id": "", + "allowed": true + }, + { + "kind": "record", + "action": "read", + "resource_id": "other-resource", + "allowed": true + }, + { + "kind": "record", + "action": "read", + "resource_id": "record-conformance", + "allowed": true + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "other-resource", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "record-conformance", + "allowed": false + } + ] + }, + { + "name": "session", + "token": "Chdjb2RlZmx5LndvcmstY29udGV4dC92MRIHRWQyNTUxORoNY29uZm9ybWFuY2UtMSIeaHR0cHM6Ly9hdXRob3JpdHkuY29kZWZseS50ZXN0Khhjb2RlZmx5LnRlc3QvY29uZm9ybWFuY2UwoMX11gY4oMX11gZAsOH11gZKJGEyMGFlZmY3LTg0MGMtNDY5My04Y2EzLTA0YTU0MDRiZDkxZVAHWgppZGVtcG90ZW50YhJ0ZW5hbnQtY29uZm9ybWFuY2VqD3ByaW5jaXBhbC1vd25lcnIQdGFzay1jb25mb3JtYW5jZXokZjdkMjNmYWQtZTI1MS00M2MzLWFmMzItM2M1M2U5Y2MwZWMyigEOCgZyZWNvcmQSBHJlYWSyAQVodW1hbsoBGG9yZ2FuaXphdGlvbi1jb25mb3JtYW5jZdIBHggCEhhpbnN0YWxsYXRpb24tY29uZm9ybWFuY2UYAw.QfE84kjvNVYJlmI60DaVgZZQQJebFu7uXjdzOkZh6MgVcpeZNM6bkdn9vCBNxWNtkdVp6N7wBrWHEuuHWradDQ", + "audience": "codefly.test/conformance", + "scopes": [ + { + "resource_kind": "record", + "actions": [ + "read" + ] + } + ], + "probes": [ + { + "kind": "record", + "action": "read", + "resource_id": "", + "allowed": true + }, + { + "kind": "record", + "action": "read", + "resource_id": "other-resource", + "allowed": true + }, + { + "kind": "record", + "action": "read", + "resource_id": "record-conformance", + "allowed": true + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "other-resource", + "allowed": false + }, + { + "kind": "record", + "action": "undeclared", + "resource_id": "record-conformance", + "allowed": false + } + ] + } +] diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/python_claims_test.go b/modules/authority/services/gateway/code/pkg/business/issuer/python_claims_test.go new file mode 100644 index 00000000..49fe3c1a --- /dev/null +++ b/modules/authority/services/gateway/code/pkg/business/issuer/python_claims_test.go @@ -0,0 +1,122 @@ +package issuer_test + +import ( + "context" + "encoding/json" + "flag" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/workcontext" +) + +var updatePythonClaims = flag.Bool("update-python-claims", false, "regenerate Python claim projection fixtures through Core") + +type claimProbe struct { + Kind string `json:"kind"` + Action string `json:"action"` + ResourceID string `json:"resource_id"` + Allowed bool `json:"allowed"` +} + +type claimProjection struct { + Name string `json:"name"` + Token string `json:"token"` + Audience string `json:"audience"` + Scopes []*basev0.WorkScopeV1 `json:"scopes"` + Probes []claimProbe `json:"probes"` +} + +// Python projects tokens minted and verified by Core. The clock is deliberately +// fixed: this tests wire/scope interpretation, not current token validity. +func TestPythonClaimProjectionFixtures(t *testing.T) { + now := time.Unix(1792893600, 0) + path := filepath.Join("..", "..", "..", "..", "..", "..", "..", "..", "libraries", "authority-client", "testdata", "claims.json") + if *updatePythonClaims { + fixtures, err := workcontext.Fixtures(now) + if err != nil { + t.Fatal(err) + } + var projections []claimProjection + for _, fixture := range fixtures { + if fixture.Outcome != workcontext.OutcomeAccepted { + continue + } + projections = append(projections, coreProjection(t, now, fixture.Name, fixture.Token)) + } + data, err := json.MarshalIndent(projections, "", " ") + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, append(data, '\n'), 0o644); err != nil { + t.Fatal(err) + } + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var projections []claimProjection + if err := json.Unmarshal(data, &projections); err != nil { + t.Fatal(err) + } + if len(projections) < 5 { + t.Fatal("missing Core token forms") + } + for _, projection := range projections { + t.Run(projection.Name, func(t *testing.T) { + got := coreProjection(t, now, projection.Name, projection.Token) + // JSON round-trip removes protobuf runtime internals from comparison. + wantJSON, _ := json.Marshal(projection) + gotJSON, _ := json.Marshal(got) + if !reflect.DeepEqual(wantJSON, gotJSON) { + t.Fatal("Core projection differs from shared fixture") + } + }) + } +} + +func coreProjection(t *testing.T, now time.Time, name, token string) claimProjection { + t.Helper() + replay := workcontext.NewMemoryReplayStore() + replay.Now = func() time.Time { return now } + verifier := workcontext.Verifier{ + TrustTheConformanceFixtureKey: true, + Issuer: workcontext.FixtureIssuer, Audience: workcontext.FixtureAudience, + Keys: workcontext.FixtureKeys(), Revisions: workcontext.FixtureRevisions(), + Replay: replay, Grants: workcontext.FixtureGrants(now), Seals: workcontext.FixtureSeals(), + Now: func() time.Time { return now }, + } + verified, err := verifier.Verify(context.Background(), token) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + scopes := verified.EffectiveScopes() + projection := claimProjection{Name: name, Token: token, Audience: workcontext.FixtureAudience, Scopes: scopes} + for _, scope := range scopes { + for _, action := range append(append([]string{}, scope.GetActions()...), "undeclared") { + for _, id := range append(append([]string{"", "other-resource"}, scope.GetResourceIds()...), "record-conformance") { + ask := &basev0.WorkScopeV1{ResourceKind: scope.GetResourceKind(), Actions: []string{action}} + if id != "" { + ask.ResourceIds = []string{id} + } + allowed := false + for _, candidate := range scopes { + if workcontext.ScopeContained(ask, []*basev0.WorkScopeV1{candidate}) { + allowed = true + break + } + } + projection.Probes = append(projection.Probes, claimProbe{scope.GetResourceKind(), action, id, allowed}) + } + } + } + return projection +} From 0e84c15f5b02b971ed920d6bad7102556ac9b74a Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:08:17 -0400 Subject: [PATCH 08/45] fix: bind installation roles and issued permissions to live revisions --- README.md | 29 ++ libraries/conformance/go/conformance.go | 25 +- .../issuer/installation_roles_test.go | 155 +++++++++ .../issuer/issuance_consistency_test.go | 293 ++++++++++++++++++ .../code/pkg/business/issuer/issuer.go | 66 +++- .../code/pkg/business/issuer/issuer_test.go | 12 +- .../code/pkg/business/issuer/scenario_test.go | 6 +- .../gateway/code/pkg/business/state/state.go | 70 +++++ .../code/plugins/registry_test/live_test.go | 2 +- .../plugins/registry_test/minigateway_test.go | 2 +- 10 files changed, 627 insertions(+), 33 deletions(-) create mode 100644 modules/authority/services/gateway/code/pkg/business/issuer/installation_roles_test.go create mode 100644 modules/authority/services/gateway/code/pkg/business/issuer/issuance_consistency_test.go diff --git a/README.md b/README.md index 46e2ae6d..0d816eba 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,35 @@ no roles, so they travel in the authority's own document delivered with it within the solution's scope ceilings in the same document. This repository holds the contract and its evaluation, never a product's roles. +An installation acting for itself resolves its selected role in that installed +solution's manifest, using the same role source as installation validation. +An organisation role with the same label supplies no authority to it. The +requested audience still intersects that role with what it serves; a binding +alone does not grant downstream module access. Downstream standing grants and +host parity remain separate qualification work. + +Delivering a narrower or removed selected role, or narrowing the installer's +role, advances active installation seals and the affected organisations' +authorization revisions. Existing roots and descendants are then refused by +Core's ordinary live verification. Role expansion alone leaves existing narrow +installation roots valid; module actors still obey their generation checks. +New issuance resolves the current declaration. + +Issuance binds those role reads to the authorization revision Core signs. If +delivery changes it between resolution and minting, issuance refuses with +`permissions changed during issuance`; it does not retry with the old scopes. +Workload, delegated and child binding resolution likewise must match the signed +binding's module generation. Narrowing, removing or retargeting a binding also +advances the active installations' seals and organization revisions: person +roots can carry its scopes for a later hop without carrying the module's epoch. +Organization revision invalidation is coarse: unrelated existing capabilities in +each affected organization also need fresh issuance. Current grants remain usable. +A change after version capture leaves an older capability that ordinary +verification refuses. Deterministic issuer tests cover +role narrowing/removal and binding narrowing/removal before resolution, during +minting and after issuance. These checks do not choose a child-scope policy or +qualify the separate accounts implementation. + The suite proves it as a matrix: the fixture states, for each person at each audience, which kind and action is allowed and which refused, and the suite asks for exactly that scope and expects the answer. The authority gateway's fixture carries diff --git a/libraries/conformance/go/conformance.go b/libraries/conformance/go/conformance.go index 3c0070c4..5298b466 100644 --- a/libraries/conformance/go/conformance.go +++ b/libraries/conformance/go/conformance.go @@ -44,6 +44,7 @@ type Fixture struct { BindingAudience string // the audience Binding is addressed to; a hop exercising Binding is addressed here Elsewhere string // a composed audience the viewer's context at Audience is not addressed to InstalledSolution string // an active installation the viewer is entitled to + InstalledAudience string // the installed solution principal; distinct from the organisation-role module Audience // A delivery, as Codefly signs it. When SignManifest is set the suite applies // manifests as signed carriers: core's module manifest for HostCoordinate and @@ -411,21 +412,33 @@ func Run(t *testing.T, conn grpc.ClientConnInterface, descriptors []byte, f Fixt } }) t.Run("an installation acts for itself", func(t *testing.T) { + if f.InstalledAudience == "" { + t.Fatal("fixture must name the installed solution audience") + } admin := c.must(t, ctx, "host.session.v1.SessionService", "SignIn", map[string]any{"fixture_token": f.AdminToken}) list := c.must(t, as(ctx, admin), "host.installation.v1.InstallationService", "List", map[string]any{"organization_id": f.Organization}) - var wiki protoreflect.Message + var installation protoreflect.Message for _, i := range listMsgs(list, "installations") { if str(i, "id") == f.InstalledSolution { - wiki = i + installation = i } } - if wiki == nil || enumName(wiki, "status") != "STATUS_ACTIVE" || str(wiki, "agent_principal_id") == "" { - t.Fatalf("installed solution = %v", wiki) + if installation == nil || enumName(installation, "status") != "STATUS_ACTIVE" || str(installation, "agent_principal_id") == "" { + t.Fatalf("installed solution = %v", installation) } - out := c.must(t, ctx, "host.authority.v1.AuthorityService", "Issue", map[string]any{"installation": map[string]any{"installation_id": str(wiki, "id")}, "audience": f.Audience}) - if str(msg(out, "context"), "owner_principal_id") != str(wiki, "agent_principal_id") { + out := c.must(t, ctx, "host.authority.v1.AuthorityService", "Issue", map[string]any{"installation": map[string]any{"installation_id": str(installation, "id")}, "audience": f.InstalledAudience}) + if str(msg(out, "context"), "owner_principal_id") != str(installation, "agent_principal_id") { t.Fatalf("installation issue: %v", out) } + c.must(t, ctx, "host.authority.v1.AuthorityService", "Check", map[string]any{"token": str(out, "token"), "audience": f.InstalledAudience}) + // The independent organisation role catalogue cannot grant this installation + // authority at an unrelated module merely because a role label matches. + if f.InstalledAudience != f.Audience { + _, err := c.call(ctx, "host.authority.v1.AuthorityService", "Issue", map[string]any{"installation": map[string]any{"installation_id": str(installation, "id")}, "audience": f.Audience}) + if status.Code(err) != codes.PermissionDenied { + t.Fatalf("installation acquired unrelated organisation-role authority: %v", err) + } + } }) t.Run("a child hop attenuates and names its actor and its binding", func(t *testing.T) { // The hop rule: a module hops only on a token addressed to it, and the hop diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/installation_roles_test.go b/modules/authority/services/gateway/code/pkg/business/issuer/installation_roles_test.go new file mode 100644 index 00000000..b7d0107f --- /dev/null +++ b/modules/authority/services/gateway/code/pkg/business/issuer/installation_roles_test.go @@ -0,0 +1,155 @@ +package issuer_test + +import ( + "context" + "errors" + "testing" + + "gateway/pkg/business/issuer" + "gateway/pkg/business/state" + "gateway/pkg/business/testkit" + authorityv1 "gateway/pkg/gen/host/authority/v1" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/codefly-dev/core/workcontext" +) + +func installationManifest(name, role string, generation uint64, actions ...string) state.Manifest { + kind := name + ".item" + return state.Manifest{Module: name, Principal: name, Kind: "solution", Generation: generation, + ResourceKinds: []*basev0.WorkScopeV1{testkit.Scope(kind, "read", "write")}, + Roles: map[string][]*basev0.WorkScopeV1{state.InstallerRole: {testkit.Scope(kind, "read", "write")}, role: {testkit.Scope(kind, actions...)}}, + Bindings: map[string]state.Binding{"self": {ID: "self", Audience: name, InvokeScopes: []*basev0.WorkScopeV1{testkit.Scope(kind, "read", "write")}}}} +} + +func TestInstallationRoleComesFromItsSolutionNotTheOrganization(t *testing.T) { + for _, role := range []string{"viewer", "task_operator"} { + t.Run(role, func(t *testing.T) { + k := testkit.New(t) + ctx := context.Background() + for _, target := range []struct{ name, action string }{{"surface-a", "read"}, {"surface-b", "write"}} { + if _, err := k.State.ApplyManifest(installationManifest(target.name, role, 1, target.action)); err != nil { + t.Fatal(err) + } + inst, err := k.State.Install(ctx, "acme", target.name, role, "alex") + if err != nil { + t.Fatal(err) + } + request := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: inst.ID}}, Audience: target.name} + out, err := k.Issuer.Issue(ctx, request) + if err != nil { + t.Fatalf("declared solution role cannot issue: %v", err) + } + held := out.GetContext().GetAuthorityScopes() + if len(held) != 1 || held[0].GetResourceKind() != target.name+".item" || len(held[0].GetActions()) != 1 || held[0].GetActions()[0] != target.action { + t.Fatalf("wrong role source: %v", held) + } + if _, err := k.Issuer.Check(ctx, out.GetToken(), target.name, ""); err != nil { + t.Fatal(err) + } + request.Audience = "documents" + if _, err := k.Issuer.Issue(ctx, request); !errors.Is(err, issuer.ErrRefused) { + t.Fatalf("organisation role supplied unrelated Documents authority: %v", err) + } + } + }) + } +} + +func TestSolutionRoleNarrowingRevokesExistingRootsAndChildren(t *testing.T) { + for _, remove := range []bool{false, true} { + name := "narrow" + if remove { + name = "remove" + } + t.Run(name, func(t *testing.T) { + k := testkit.New(t) + ctx := context.Background() + manifest := installationManifest("conformance-solution", "task_operator", 1, "read", "write") + if _, err := k.State.ApplyManifest(manifest); err != nil { + t.Fatal(err) + } + inst, err := k.State.Install(ctx, "acme", manifest.Module, "task_operator", "alex") + if err != nil { + t.Fatal(err) + } + request := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: inst.ID}}, Audience: manifest.Principal, Scopes: scopes(testkit.Scope("conformance-solution.item", "write"))} + root, err := k.Issuer.Issue(ctx, request) + if err != nil { + t.Fatal(err) + } + child, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_ParentToken{ParentToken: root.GetToken()}, Actor: &authorityv1.WorkloadCredential{Credential: k.Credential(t, manifest.Module)}, Audience: manifest.Principal, OperationBindingId: "self", Scopes: request.Scopes}) + if err != nil { + t.Fatal(err) + } + for _, token := range []string{root.GetToken(), child.GetToken()} { + if _, err := k.Issuer.Check(ctx, token, manifest.Principal, ""); err != nil { + t.Fatalf("before narrowing: %v", err) + } + } + other, err := k.Issuer.Issue(person(t, k, "fixture-zoe"), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents"}) + if err != nil { + t.Fatal(err) + } + next := installationManifest("conformance-solution", "task_operator", 2, "read") + if remove { + delete(next.Roles, "task_operator") + } + if _, err := k.State.ApplyManifest(next); err != nil { + t.Fatal(err) + } + for _, token := range []string{root.GetToken(), child.GetToken()} { + if _, err := k.Issuer.Check(ctx, token, manifest.Principal, ""); !errors.Is(err, workcontext.ErrRevoked) { + t.Fatalf("old role capability after narrowing: %v", err) + } + } + if _, err := k.Issuer.Issue(ctx, request); err == nil { + t.Fatal("fresh issuance recovered removed write") + } + request.Scopes = scopes(testkit.Scope("conformance-solution.item", "read")) + if _, err := k.Issuer.Issue(ctx, request); (err == nil) == remove { + t.Fatalf("fresh read after role change remove=%v: %v", remove, err) + } + if _, err := k.Issuer.Check(ctx, other.GetToken(), "documents", ""); err != nil { + t.Fatalf("unrelated organization revoked: %v", err) + } + }) + } +} + +func TestInstallerNarrowingRevokesTheOwnerButRoleExpansionKeepsExistingAuthority(t *testing.T) { + k := testkit.New(t) + ctx := context.Background() + m := installationManifest("conformance-solution", "viewer", 1, "read") + if _, err := k.State.ApplyManifest(m); err != nil { + t.Fatal(err) + } + inst, err := k.State.Install(ctx, "acme", m.Module, "viewer", "alex") + if err != nil { + t.Fatal(err) + } + agent, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: inst.ID}}, Audience: m.Principal}) + if err != nil { + t.Fatal(err) + } + owner, err := k.Issuer.Issue(person(t, k, "fixture-alex"), &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: m.Principal, Scopes: scopes(testkit.Scope(m.Module+".item", "write"))}) + if err != nil { + t.Fatal(err) + } + // Expanding the installed viewer role leaves already-issued narrow grants valid. + next := installationManifest(m.Module, "viewer", 2, "read", "write") + if _, err := k.State.ApplyManifest(next); err != nil { + t.Fatal(err) + } + if _, err := k.Issuer.Check(ctx, agent.GetToken(), m.Principal, ""); err != nil { + t.Fatalf("expansion invalidated old scope: %v", err) + } + // The owner's installer role is independent of that selected viewer role. + next = installationManifest(m.Module, "viewer", 3, "read", "write") + next.Roles[state.InstallerRole] = []*basev0.WorkScopeV1{testkit.Scope(m.Module+".item", "read")} + if _, err := k.State.ApplyManifest(next); err != nil { + t.Fatal(err) + } + if _, err := k.Issuer.Check(ctx, owner.GetToken(), m.Principal, ""); !errors.Is(err, workcontext.ErrRevoked) { + t.Fatalf("installer lost write but old token still works: %v", err) + } +} diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/issuance_consistency_test.go b/modules/authority/services/gateway/code/pkg/business/issuer/issuance_consistency_test.go new file mode 100644 index 00000000..c974347e --- /dev/null +++ b/modules/authority/services/gateway/code/pkg/business/issuer/issuance_consistency_test.go @@ -0,0 +1,293 @@ +package issuer_test + +import ( + "context" + "crypto/rand" + "testing" + + "gateway/pkg/business/state" + "gateway/pkg/business/testkit" + authorityv1 "gateway/pkg/gen/host/authority/v1" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + "github.com/google/uuid" +) + +// UUID creation is an existing injection point around Core's live version reads. +// These tests are deliberately serial: uuid's reader is process-wide. +type issuanceChangeReader struct { + remaining int + apply func() +} + +func (r *issuanceChangeReader) Read(p []byte) (int, error) { + r.remaining-- + if r.remaining == 0 { + r.apply() + } + return rand.Read(p) +} + +func interleaveIssuance(t *testing.T, stage string, apply func(), issue func() (*authorityv1.IssueResponse, error)) (*authorityv1.IssueResponse, error) { + t.Helper() + applied := false + change := func() { apply(); applied = true } + switch stage { + case "before_resolution": + change() + case "before_mint", "during_mint": + n := 1 + if stage == "during_mint" { + n = 2 + } + uuid.SetRand(&issuanceChangeReader{remaining: n, apply: change}) + defer uuid.SetRand(nil) + } + out, err := issue() + uuid.SetRand(nil) + if stage == "after_issue" { + if err != nil { + t.Fatalf("ordinary issuance: %v", err) + } + change() + } + if !applied { + t.Fatal("interleaving not reached") + } + return out, err +} + +func TestRoleNarrowingBetweenResolutionAndMint(t *testing.T) { + for _, owner := range []string{"installation", "installation_removed", "person", "delegation", "child"} { + for _, stage := range []string{"before_resolution", "before_mint", "during_mint", "after_issue"} { + t.Run(owner+"/"+stage, func(t *testing.T) { + k := testkit.New(t) + ctx := context.Background() + old := installationManifest("conformance-solution", "task_operator", 1, "read", "write") + if _, err := k.State.ApplyManifest(old); err != nil { + t.Fatal(err) + } + inst, err := k.State.Install(ctx, "acme", old.Module, "task_operator", "alex") + if err != nil { + t.Fatal(err) + } + req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: inst.ID}}, Audience: old.Principal, Scopes: scopes(testkit.Scope(old.Module+".item", "write"))} + next := installationManifest(old.Module, "task_operator", 2, "read") + if owner == "person" { + ctx = person(t, k, "fixture-alex") + req.As = &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}} + } + if owner == "person" || owner == "delegation" { + next.Roles[state.InstallerRole] = []*basev0.WorkScopeV1{testkit.Scope(old.Module+".item", "read")} + } + if owner == "installation_removed" { + delete(next.Roles, "task_operator") + } + if owner == "delegation" { + ctx = person(t, k, "fixture-alex") + d, err := k.Issuer.Delegate(ctx, old.Module, []*basev0.WorkScopeV1{testkit.Scope(old.Module+".item", "write")}, "consistency regression") + if err != nil { + t.Fatal(err) + } + req.As = &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}} + req.Actor = &authorityv1.WorkloadCredential{Credential: k.Credential(t, old.Module)} + req.OperationBindingId = "self" + } + if owner == "child" { + parent, err := k.Issuer.Issue(ctx, req) + if err != nil { + t.Fatal(err) + } + req.As = &authorityv1.IssueRequest_ParentToken{ParentToken: parent.Token} + req.Actor = &authorityv1.WorkloadCredential{Credential: k.Credential(t, old.Module)} + req.OperationBindingId = "self" + } + // Prove this owner can issue the exact requested scope before scheduling a change. + before, err := k.Issuer.Issue(ctx, req) + if err != nil { + t.Fatal(err) + } + if _, err := k.Issuer.Check(ctx, before.Token, old.Principal, ""); err != nil { + t.Fatal(err) + } + out, err := interleaveIssuance(t, stage, func() { + if _, err := k.State.ApplyManifest(next); err != nil { + t.Fatal(err) + } + }, func() (*authorityv1.IssueResponse, error) { return k.Issuer.Issue(ctx, req) }) + if err != nil { + t.Logf("safely refused: %v", err) + return + } + if _, err := k.Issuer.Check(ctx, out.Token, old.Principal, ""); err != nil { + t.Logf("safely rejected: %v", err) + return + } + t.Fatalf("stale write accepted: signed revision=%d installation revision=%d", out.Context.GetAuthorizationRevision(), out.Context.GetSeal().GetInstallationRevision()) + }) + } + } +} + +func TestBindingNarrowingBetweenResolutionAndMint(t *testing.T) { + for _, owner := range []string{"workload", "delegation", "child", "person"} { + for _, changeName := range []string{"narrow", "remove", "retarget", "resource"} { + for _, stage := range []string{"before_resolution", "before_mint", "during_mint", "after_issue"} { + t.Run(owner+"/"+changeName+"/"+stage, func(t *testing.T) { + k := testkit.New(t) + ctx := context.Background() + credential := k.Credential(t, "documents") + req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Workload{Workload: &authorityv1.WorkloadCredential{Credential: credential}}, OrganizationId: "acme", Audience: "model", OperationBindingId: "model", Scopes: scopes(testkit.Scope("modelservice.profiles", "invoke"))} + if owner == "person" { + ctx = person(t, k, "fixture-dev") + req.As = &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}} + req.Audience = "documents" + } + if owner == "delegation" { + ctx = person(t, k, "fixture-dev") + d, err := k.Issuer.Delegate(ctx, "documents", []*basev0.WorkScopeV1{testkit.Scope("modelservice.profiles", "invoke")}, "binding consistency regression") + if err != nil { + t.Fatal(err) + } + req.As = &authorityv1.IssueRequest_Delegation{Delegation: &authorityv1.DelegationRef{DelegationId: d.ID}} + req.Actor = &authorityv1.WorkloadCredential{Credential: credential} + } + if owner == "child" { + ctx = person(t, k, "fixture-dev") + parent, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", Scopes: scopes(testkit.Scope("documents", "read"))}) + if err != nil { + t.Fatal(err) + } + req.As = &authorityv1.IssueRequest_ParentToken{ParentToken: parent.Token} + req.Actor = &authorityv1.WorkloadCredential{Credential: credential} + } + before, err := k.Issuer.Issue(ctx, req) + if err != nil { + t.Fatal(err) + } + if _, err := k.Issuer.Check(ctx, before.Token, req.Audience, ""); err != nil { + t.Fatal(err) + } + next := testkit.Documents(2) + binding := next.Bindings["model"] + switch changeName { + case "narrow": + binding.HeadlessScopes = []*basev0.WorkScopeV1{testkit.Scope("modelservice.profiles", "read")} + binding.InvokeScopes = binding.HeadlessScopes + case "retarget": + binding.Audience = "runtime" + case "resource": + scoped := testkit.Scope("modelservice.profiles", "invoke", "read") + scoped.ResourceIds = []string{"profile-one"} + binding.HeadlessScopes = []*basev0.WorkScopeV1{scoped} + binding.InvokeScopes = binding.HeadlessScopes + } + next.Bindings["model"] = binding + if changeName == "remove" { + delete(next.Bindings, "model") + } + out, err := interleaveIssuance(t, stage, func() { + if _, err := k.State.ApplyManifest(next); err != nil { + t.Fatal(err) + } + }, func() (*authorityv1.IssueResponse, error) { return k.Issuer.Issue(ctx, req) }) + if err != nil { + t.Logf("safely refused: %v", err) + return + } + if _, err := k.Issuer.Check(ctx, out.Token, req.Audience, ""); err != nil { + t.Logf("safely rejected: %v", err) + return + } + t.Fatal("old invoke accepted after binding narrowing/removal") + }) + } + } + } +} + +func TestPersonBindingReadRemainsUsableAfterNarrowingAndExpansion(t *testing.T) { + k := testkit.New(t) + ctx := person(t, k, "fixture-dev") + req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", OperationBindingId: "model", Scopes: scopes(testkit.Scope("modelservice.profiles", "read"))} + next := testkit.Documents(2) + binding := next.Bindings["model"] + binding.InvokeScopes = []*basev0.WorkScopeV1{testkit.Scope("modelservice.profiles", "read")} + binding.HeadlessScopes = binding.InvokeScopes + next.Bindings["model"] = binding + if _, err := k.State.ApplyManifest(next); err != nil { + t.Fatal(err) + } + read, err := k.Issuer.Issue(ctx, req) + if err != nil { + t.Fatalf("fresh read after narrowing: %v", err) + } + if _, err := k.Issuer.Check(ctx, read.Token, req.Audience, ""); err != nil { + t.Fatal(err) + } + if _, err := k.State.ApplyManifest(testkit.Documents(3)); err != nil { + t.Fatal(err) + } + if _, err := k.Issuer.Check(ctx, read.Token, req.Audience, ""); err != nil { + t.Fatalf("unchanged person read revoked by expansion: %v", err) + } + req.Scopes = scopes(testkit.Scope("modelservice.profiles", "invoke")) + invoke, err := k.Issuer.Issue(ctx, req) + if err != nil { + t.Fatalf("fresh invoke after expansion: %v", err) + } + if _, err := k.Issuer.Check(ctx, invoke.Token, req.Audience, ""); err != nil { + t.Fatal(err) + } +} + +// Issue reads its audience before sessionOf calls Value; capture a delivery +// there to exercise the early audience snapshot independently of mint UUIDs. +type deliveryOnSession struct { + context.Context + apply func() +} + +func (c *deliveryOnSession) Value(key any) any { + value := c.Context.Value(key) + if value != nil && c.apply != nil { + apply := c.apply + c.apply = nil + apply() + } + return value +} +func TestBindingChangeBeforePersonRevisionSnapshot(t *testing.T) { + k := testkit.New(t) + ctx := person(t, k, "fixture-dev") + req := &authorityv1.IssueRequest{As: &authorityv1.IssueRequest_Person{Person: &authorityv1.PersonSession{}}, Audience: "documents", OperationBindingId: "model", Scopes: scopes(testkit.Scope("modelservice.profiles", "invoke"))} + original, err := k.Issuer.Issue(ctx, req) + if err != nil { + t.Fatal(err) + } + if _, err := k.Issuer.Check(ctx, original.Token, "documents", ""); err != nil { + t.Fatal(err) + } + next := testkit.Documents(2) + b := next.Bindings["model"] + b.InvokeScopes = []*basev0.WorkScopeV1{testkit.Scope("modelservice.profiles", "read")} + b.HeadlessScopes = b.InvokeScopes + next.Bindings["model"] = b + interleaved := &deliveryOnSession{Context: ctx, apply: func() { + if _, err := k.State.ApplyManifest(next); err != nil { + t.Fatal(err) + } + }} + out, err := k.Issuer.Issue(interleaved, req) + if interleaved.apply != nil { + t.Fatal("early interleaving not reached") + } + if err != nil { + t.Logf("refused stale audience: %v", err) + return + } + if _, err := k.Issuer.Check(ctx, out.Token, "documents", ""); err != nil { + t.Logf("rejected stale audience: %v", err) + return + } + t.Fatalf("stale early audience accepted with authorization revision %d", out.Context.GetAuthorizationRevision()) +} diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go b/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go index 17b9fa2a..ee503602 100644 --- a/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go +++ b/modules/authority/services/gateway/code/pkg/business/issuer/issuer.go @@ -236,6 +236,16 @@ func (i *Issuer) forPerson(ctx context.Context, sessionID string, audience state if err != nil { return "", nil, err } + revision, err := i.state.AuthorizationRevision(ctx, sess.Organization) + if err != nil { + return "", nil, err + } + // Issue's audience lookup precedes this revision snapshot. Resolve any + // binding again below it so a concurrent delivery cannot supply old scopes. + audience, err = i.state.ModuleByPrincipal(audience.Principal) + if err != nil { + return "", nil, err + } held, err := i.state.PersonScopes(sess.Person, audience.Principal) if err != nil { return "", nil, err @@ -266,7 +276,7 @@ func (i *Issuer) forPerson(ctx context.Context, sessionID string, audience state teams := slices.Clone(sess.Teams) slices.Sort(teams) teams = slices.Compact(teams) - return i.authority.Start(ctx, workcontext.StartInput{ + return i.startAtRevision(ctx, revision, workcontext.StartInput{ TenantID: sess.Organization, OrganizationID: sess.Organization, OwnerPrincipalID: sess.Person, OwnerPrincipalKind: "human", TaskID: uuid.NewString(), SessionID: sess.ID, @@ -301,30 +311,33 @@ func (i *Issuer) forWorkload(ctx context.Context, credential, org, bindingID, op if err != nil { return "", nil, err } - return i.authority.Start(ctx, workcontext.StartInput{ + token, signed, err := i.authority.Start(ctx, workcontext.StartInput{ TenantID: org, OrganizationID: org, OwnerPrincipalID: module.Principal, OwnerPrincipalKind: "service", TaskID: uuid.NewString(), Audience: audience.Principal, AuthorityScopes: scopes, InstallationID: state.InstallationID(org, module.Module), OperationBindingID: fullID, TTL: ttl, }) + if err != nil { + return "", nil, err + } + if signed.GetSeal().GetPrincipalEpoch() != module.Generation || signed.GetOperationBinding().GetRevision() != module.Generation { + return "", nil, fmt.Errorf("%w: workload binding changed during issuance", ErrRefused) + } + return token, signed, nil } // forInstallation: owner = actor = the installation's agent; scopes = asked ∩ // the installation's role ∩ the audience's ceiling. func (i *Issuer) forInstallation(ctx context.Context, id string, audience state.Manifest, asked []*basev0.WorkScopeV1, ttl time.Duration) (string, *basev0.WorkContextV1, error) { - inst, ok := i.state.Installation(id) - if !ok { - return "", nil, fmt.Errorf("%w: installation %q", state.ErrUnknown, id) - } - if inst.Status != state.Active { - return "", nil, fmt.Errorf("%w: installation %s is %s", workcontext.ErrNoSeal, id, inst.Status) + inst, held, revision, err := i.state.InstallationScopes(id, audience.Principal) + if err != nil { + return "", nil, err } - held := state.Intersect(i.state.RoleScopes(inst.Organization, inst.Role), audience.ResourceKinds) scopes, err := grant(asked, held) if err != nil { return "", nil, err } - return i.authority.Start(ctx, workcontext.StartInput{ + return i.startAtRevision(ctx, revision, workcontext.StartInput{ TenantID: inst.Organization, OrganizationID: inst.Organization, OwnerPrincipalID: inst.AgentPrincipal(), OwnerPrincipalKind: "agent", OwnerAgentID: inst.Target, TaskID: uuid.NewString(), Audience: audience.Principal, AuthorityScopes: scopes, @@ -332,6 +345,20 @@ func (i *Issuer) forInstallation(ctx context.Context, id string, audience state. }) } +// startAtRevision binds the permission reads to the revision Core stamps. +// A concurrent grant change must not turn old scopes into fresh authority. +// A change after Core reads the revision is rejected by ordinary verification. +func (i *Issuer) startAtRevision(ctx context.Context, revision uint64, in workcontext.StartInput) (string, *basev0.WorkContextV1, error) { + token, signed, err := i.authority.Start(ctx, in) + if err != nil { + return "", nil, err + } + if signed.GetAuthorizationRevision() != revision { + return "", nil, fmt.Errorf("%w: permissions changed during issuance", ErrRefused) + } + return token, signed, nil +} + // child: one more hop, inherited from the owner. The parent is verified for its // own audience and must be addressed to the module acting; the actor proves itself // and names the binding it exercises, addressed to the hop's audience. The child @@ -444,6 +471,9 @@ func (i *Issuer) child(ctx context.Context, parent, actorCredential, bindingID, return "", nil, err } if n == len(hops) { + if signed.GetOperationBinding().GetRevision() != actor.Generation { + return "", nil, fmt.Errorf("%w: actor binding changed during issuance", ErrRefused) + } // Reconstruction must not refresh a parent across revocation. Check // the original capability after the final child has been signed. if err := i.verifier(claims.GetAudience()).Recheck(ctx, verified); err != nil { @@ -485,6 +515,10 @@ func (i *Issuer) forDelegation(ctx context.Context, id, actorCredential, binding if d.Revoked { return "", nil, fmt.Errorf("%w: delegation %s is revoked", ErrRefused, id) } + revision, err := i.state.AuthorizationRevision(ctx, d.Organization) + if err != nil { + return "", nil, err + } actor, err := i.state.VerifyWorkload(ctx, actorCredential) if err != nil { return "", nil, fmt.Errorf("an issue from a delegation names the module acting: %w", err) @@ -518,7 +552,7 @@ func (i *Issuer) forDelegation(ctx context.Context, id, actorCredential, binding if err != nil { return "", nil, err } - token, _, err := i.authority.Start(ctx, workcontext.StartInput{ + token, _, err := i.startAtRevision(ctx, revision, workcontext.StartInput{ TenantID: d.Organization, OrganizationID: d.Organization, OwnerPrincipalID: d.Grantor, OwnerPrincipalKind: "human", TaskID: uuid.NewString(), Audience: audience.Principal, AuthorityScopes: scopes, @@ -549,6 +583,9 @@ func (i *Issuer) forDelegation(ctx context.Context, id, actorCredential, binding if d.Revoked { return "", nil, fmt.Errorf("%w: delegation %s is revoked", ErrRefused, id) } + if signed.GetOperationBinding().GetRevision() != actor.Generation { + return "", nil, fmt.Errorf("%w: actor binding changed during issuance", ErrRefused) + } return token, signed, nil } @@ -608,11 +645,8 @@ func (i *Issuer) ownerHolds(claims *basev0.WorkContextV1, audience state.Manifes case "human": return i.state.PersonScopes(owner, audience.Principal) case "agent": - inst, ok := i.state.Installation(strings.TrimSuffix(owner, "/agent")) - if !ok || inst.Status != state.Active { - return nil, fmt.Errorf("%w: the installation that owns this context is not active", workcontext.ErrNoSeal) - } - return state.Intersect(i.state.RoleScopes(inst.Organization, inst.Role), audience.ResourceKinds), nil + _, scopes, _, err := i.state.InstallationScopes(strings.TrimSuffix(owner, "/agent"), audience.Principal) + return scopes, err } // A module's own context inherits nothing more at the next audience: its // child carries what it was already delegated. diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go b/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go index b3011ca2..1688863b 100644 --- a/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go +++ b/modules/authority/services/gateway/code/pkg/business/issuer/issuer_test.go @@ -228,18 +228,18 @@ func TestAnUninstallRefusesAnOutstandingTokenOnTheNextCheck(t *testing.T) { ctx := context.Background() wiki, _ := k.State.Installation(state.InstallationID("acme", "wiki")) out, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: wiki.ID}}, Audience: "documents", + As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: wiki.ID}}, Audience: "wiki", }) if err != nil { t.Fatal(err) } - if _, err := k.Issuer.Check(ctx, out.GetToken(), "documents", ""); err != nil { + if _, err := k.Issuer.Check(ctx, out.GetToken(), "wiki", ""); err != nil { t.Fatalf("before: %v", err) } if err := k.State.Revoke(wiki.ID); err != nil { t.Fatal(err) } - _, err = k.Issuer.Check(ctx, out.GetToken(), "documents", "") + _, err = k.Issuer.Check(ctx, out.GetToken(), "wiki", "") if err == nil || !(errors.Is(err, workcontext.ErrRevoked) || errors.Is(err, workcontext.ErrNoSeal)) { t.Fatalf("after the uninstall: %v", err) } @@ -249,7 +249,7 @@ func TestAnInstallationActsForItselfWithinItsRoleAndTheAudiencesCeiling(t *testi k := testkit.New(t) ctx := context.Background() out, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}}, Audience: "documents", + As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}}, Audience: "wiki", }) if err != nil { t.Fatal(err) @@ -258,8 +258,8 @@ func TestAnInstallationActsForItselfWithinItsRoleAndTheAudiencesCeiling(t *testi if wc.GetOwnerPrincipalId() != state.InstallationID("acme", "wiki")+"/agent" || wc.GetOwnerPrincipalKind() != "agent" || wc.GetOwnerAgentId() != "wiki" { t.Fatalf("owner = %s/%s/%s", wc.GetOwnerPrincipalId(), wc.GetOwnerPrincipalKind(), wc.GetOwnerAgentId()) } - if strings.Join(wc.GetAuthorityScopes()[0].GetActions(), ",") != "read" { - t.Fatalf("viewer role at documents = %v", wc.GetAuthorityScopes()) + if wc.GetAuthorityScopes()[0].GetResourceKind() != "wiki.page" || strings.Join(wc.GetAuthorityScopes()[0].GetActions(), ",") != "read" { + t.Fatalf("viewer role at its solution = %v", wc.GetAuthorityScopes()) } if _, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "nothing")}}, Audience: "documents", diff --git a/modules/authority/services/gateway/code/pkg/business/issuer/scenario_test.go b/modules/authority/services/gateway/code/pkg/business/issuer/scenario_test.go index 22e0a316..70609d58 100644 --- a/modules/authority/services/gateway/code/pkg/business/issuer/scenario_test.go +++ b/modules/authority/services/gateway/code/pkg/business/issuer/scenario_test.go @@ -140,13 +140,13 @@ func TestTheScenarioOneHopAtATime(t *testing.T) { t.Run("6 overnight, the wiki reconciles, as itself", func(t *testing.T) { out, err := k.Issuer.Issue(ctx, &authorityv1.IssueRequest{ - As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}}, Audience: "documents", + As: &authorityv1.IssueRequest_Installation{Installation: &authorityv1.InstallationRef{InstallationId: state.InstallationID("acme", "wiki")}}, Audience: "wiki", }) if err != nil { t.Fatal(err) } wc := out.GetContext() - // owner = actor = the wiki at Acme; scopes = what Acme consented to at install (viewer) ∩ documents' ceiling. + // owner = actor = the wiki at Acme; scopes = what Acme consented to at install (viewer) ∩ the solution's own ceiling. No downstream grant is implied. if wc.GetOwnerPrincipalId() != state.InstallationID("acme", "wiki")+"/agent" || wc.GetOwnerAgentId() != "wiki" || wc.GetSeal().GetInstallationId() != state.InstallationID("acme", "wiki") || strings.Join(wc.GetAuthorityScopes()[0].GetActions(), ",") != "read" { t.Fatalf("step 6 token = %v", wc) @@ -155,7 +155,7 @@ func TestTheScenarioOneHopAtATime(t *testing.T) { if err := k.State.Revoke(state.InstallationID("acme", "wiki")); err != nil { t.Fatal(err) } - if _, err := k.Issuer.Check(ctx, out.GetToken(), "documents", ""); err == nil { + if _, err := k.Issuer.Check(ctx, out.GetToken(), "wiki", ""); err == nil { t.Fatal("an uninstalled wiki still acted") } }) diff --git a/modules/authority/services/gateway/code/pkg/business/state/state.go b/modules/authority/services/gateway/code/pkg/business/state/state.go index 854a4b9e..c5c30cbe 100644 --- a/modules/authority/services/gateway/code/pkg/business/state/state.go +++ b/modules/authority/services/gateway/code/pkg/business/state/state.go @@ -828,6 +828,7 @@ func (s *State) ApplyManifest(m Manifest) ([]string, error) { } } m.ResourceKinds = Normalize(append(m.ResourceKinds, servedByMethods(m.Methods)...)) + s.invalidateNarrowedAuthority(m) s.modules[m.Module] = m s.refreshDeveloperRoles() s.principals[m.Principal] = m.Module @@ -841,6 +842,46 @@ func (s *State) ApplyManifest(m Manifest) ([]string, error) { return ids, nil } +// invalidateNarrowedAuthority advances the existing live revisions when delivery +// removes role or binding authority. Person roots can carry binding scopes for a +// later module hop without that module's epoch, so generation alone is not enough. +// Caller holds mu; expansion does not advance these revisions. +func (s *State) invalidateNarrowedAuthority(next Manifest) { + previous, ok := s.modules[next.Module] + if !ok { + return + } + bindingsNarrowed := false + for id, old := range previous.Bindings { + binding, exists := next.Bindings[id] + if !exists || old.Audience != binding.Audience || + !Attenuates(old.InvokeScopes, binding.InvokeScopes) || + !Attenuates(old.HeadlessScopes, binding.HeadlessScopes) || + !Attenuates(old.LookupScopes, binding.LookupScopes) { + bindingsNarrowed = true + break + } + } + changed := map[string]bool{} + for id, inst := range s.installations { + if inst.Target != next.Module || inst.Status != Active { + continue + } + rolesNarrowed := previous.Kind == "solution" && + (!Attenuates(previous.Roles[inst.Role], next.Roles[inst.Role]) || + !Attenuates(previous.Roles[InstallerRole], next.Roles[InstallerRole])) + if !bindingsNarrowed && !rolesNarrowed { + continue + } + inst.Revision++ + s.installations[id] = inst + changed[inst.Organization] = true + } + for org := range changed { + s.revisions[org]++ + } +} + // takePendingInstalls removes and returns the fixture installations waiting for // this target; the caller holds the lock. func (s *State) takePendingInstalls(target string) []FixtureInstallation { @@ -1017,6 +1058,35 @@ func (s *State) Installation(id string) (Installation, bool) { return i, ok } +// InstallationScopes resolves the selected role in the installed solution's +// declaration, never in the organisation's independent role catalogue. The +// installation, role, audience and authorization revision share one snapshot. +// Issuance must not stamp these scopes with a different revision. +func (s *State) InstallationScopes(id, audience string) (Installation, []*basev0.WorkScopeV1, uint64, error) { + s.mu.RLock() + defer s.mu.RUnlock() + inst, ok := s.installations[id] + if !ok { + return Installation{}, nil, 0, fmt.Errorf("%w: installation %q", ErrUnknown, id) + } + if inst.Status != Active { + return Installation{}, nil, 0, fmt.Errorf("%w: installation %s is %s", workcontext.ErrNoSeal, id, inst.Status) + } + target, ok := s.modules[inst.Target] + if !ok || target.Kind != "solution" { + return Installation{}, nil, 0, fmt.Errorf("%w: installation target is not a delivered solution", ErrUnknown) + } + held, ok := target.Roles[inst.Role] + if !ok { + return Installation{}, nil, 0, fmt.Errorf("%w: role %q is no longer declared by %s", ErrUnknown, inst.Role, inst.Target) + } + served, err := s.moduleByPrincipal(audience) + if err != nil { + return Installation{}, nil, 0, err + } + return inst, Intersect(held, served.ResourceKinds), s.revisions[inst.Organization] + 1, nil +} + // List answers what one organisation has installed, sorted by id. func (s *State) List(org string) []Installation { s.mu.RLock() diff --git a/modules/authority/services/gateway/code/plugins/registry_test/live_test.go b/modules/authority/services/gateway/code/plugins/registry_test/live_test.go index 3107a3aa..6d6c9990 100644 --- a/modules/authority/services/gateway/code/plugins/registry_test/live_test.go +++ b/modules/authority/services/gateway/code/plugins/registry_test/live_test.go @@ -37,6 +37,6 @@ func TestARunningMiniGatewayPassesTheSuite(t *testing.T) { Organization: "acme", ViewerToken: "fixture-maya", AdminToken: "fixture-alex", Audience: "records", ReadKind: "records.records", Elsewhere: "model", WorkloadModule: "org-directory", Binding: "records", BindingAudience: "records", - InstalledSolution: state.InstallationID("acme", "wiki"), + InstalledSolution: state.InstallationID("acme", "wiki"), InstalledAudience: "wiki", }) } diff --git a/modules/authority/services/gateway/code/plugins/registry_test/minigateway_test.go b/modules/authority/services/gateway/code/plugins/registry_test/minigateway_test.go index d157a81e..2472755b 100644 --- a/modules/authority/services/gateway/code/plugins/registry_test/minigateway_test.go +++ b/modules/authority/services/gateway/code/plugins/registry_test/minigateway_test.go @@ -58,7 +58,7 @@ func TestTheMiniGatewayPassesTheSuiteEveryImplementationMustPass(t *testing.T) { // records binding; a context at records is refused at model. Audience: "records", ReadKind: "records.records", WorkloadModule: "org-directory", WorkloadCredential: k.Credential(t, "org-directory"), Binding: "records", BindingAudience: "records", Elsewhere: "model", - InstalledSolution: state.InstallationID("acme", "wiki"), + InstalledSolution: state.InstallationID("acme", "wiki"), InstalledAudience: "wiki", // Deliveries are signed with the kit's release key and carried by the // module's own workload under the launcher. HostCoordinate: "local", HostDomain: "local", From 3b8bac8c77fd4ee75920e5da8e1cb2f6669eae04 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:23:15 -0400 Subject: [PATCH 09/45] feat: add Go and TypeScript resource scope readers --- README.md | 8 +- libraries/authority-client/go/README.md | 39 + .../gen/host/composition/v1/composition.pb.go | 2 +- libraries/authority-client/go/go.mod | 16 +- libraries/authority-client/go/go.sum | 34 +- .../authority-client/go/workcontext/claims.go | 94 + .../go/workcontext/claims_test.go | 169 + .../authority-client/library.codefly.yaml | 6 +- .../_gen/host/authority/v1/authority_pb2.py | 36 +- .../authority-client/typescript/README.md | 40 + .../typescript/package-lock.json | 48 + .../authority-client/typescript/package.json | 33 + .../typescript/src/gen/api_pb.ts | 60 + .../src/gen/buf/validate/validate_pb.ts | 5034 +++++++++++++++++ .../src/gen/google/api/annotations_pb.ts | 39 + .../typescript/src/gen/google/api/http_pb.ts | 474 ++ .../src/gen/host/artifact/v1/artifact_pb.ts | 276 + .../src/gen/host/authority/v1/authority_pb.ts | 787 +++ .../gen/host/authority/v1/work_context_pb.ts | 651 +++ .../gen/host/composition/v1/composition_pb.ts | 460 ++ .../host/installation/v1/installation_pb.ts | 358 ++ .../src/gen/host/session/v1/session_pb.ts | 251 + .../authority-client/typescript/src/index.ts | 11 + .../typescript/tests/work-context.test.mjs | 81 + .../authority-client/typescript/tsconfig.json | 16 + .../typescript/work-context.ts | 78 + 26 files changed, 9070 insertions(+), 31 deletions(-) create mode 100644 libraries/authority-client/go/README.md create mode 100644 libraries/authority-client/go/workcontext/claims.go create mode 100644 libraries/authority-client/go/workcontext/claims_test.go create mode 100644 libraries/authority-client/typescript/README.md create mode 100644 libraries/authority-client/typescript/package-lock.json create mode 100644 libraries/authority-client/typescript/package.json create mode 100644 libraries/authority-client/typescript/src/gen/api_pb.ts create mode 100644 libraries/authority-client/typescript/src/gen/buf/validate/validate_pb.ts create mode 100644 libraries/authority-client/typescript/src/gen/google/api/annotations_pb.ts create mode 100644 libraries/authority-client/typescript/src/gen/google/api/http_pb.ts create mode 100644 libraries/authority-client/typescript/src/gen/host/artifact/v1/artifact_pb.ts create mode 100644 libraries/authority-client/typescript/src/gen/host/authority/v1/authority_pb.ts create mode 100644 libraries/authority-client/typescript/src/gen/host/authority/v1/work_context_pb.ts create mode 100644 libraries/authority-client/typescript/src/gen/host/composition/v1/composition_pb.ts create mode 100644 libraries/authority-client/typescript/src/gen/host/installation/v1/installation_pb.ts create mode 100644 libraries/authority-client/typescript/src/gen/host/session/v1/session_pb.ts create mode 100644 libraries/authority-client/typescript/src/index.ts create mode 100644 libraries/authority-client/typescript/tests/work-context.test.mjs create mode 100644 libraries/authority-client/typescript/tsconfig.json create mode 100644 libraries/authority-client/typescript/work-context.ts diff --git a/README.md b/README.md index 0d816eba..b8ebcc60 100644 --- a/README.md +++ b/README.md @@ -305,9 +305,15 @@ consumer's organisation in an import path). GitHub redirects the old location. (`codefly generate client --no-facade`), importable as `github.com/codefly-dev/authority/libraries/authority-client/go`. The Python library publishes generated bindings under `libraries/authority-client/python`. -Its [bound-context reader](libraries/authority-client/python/README.md) projects +The [Python bound-context reader](libraries/authority-client/python/README.md) projects effective resource scopes without another verification call. Consumers read the hop stamp first and retain their domain resource and installation checks. +The [Go reader](libraries/authority-client/go/README.md) reuses Core Decode and +scope containment; the [TypeScript reader](libraries/authority-client/typescript/README.md) +uses Codefly-generated protobuf bindings. Shared Core-produced fixtures cover +their scope decisions. These projections do not authenticate a context or replace +live checks; TypeScript hop-binding integration remains separate work. Go also +retains Core's stronger structural validation. ## Rules of change (owner, 2026-10-22) diff --git a/libraries/authority-client/go/README.md b/libraries/authority-client/go/README.md new file mode 100644 index 00000000..452da689 --- /dev/null +++ b/libraries/authority-client/go/README.md @@ -0,0 +1,39 @@ +# Authority Go client + +Codefly generates the RPC bindings in `gen/`. The `workcontext` package adds a +non-verifying projection using Core's maintained decoder and scope containment: + +```go +import claims "github.com/codefly-dev/authority/libraries/authority-client/go/workcontext" + +// First read the trusted hop stamp through codefly's hop library. +c, err := claims.ReadClaims(identity.Context) +// Handle err, compare c.Audience() to this module's delivered audience, then: +allowed := c.Allows(resourceKind, action, exactResourceID) +``` + +`Scopes()` returns deep copies of the final actor's grants, or the owner's grants +when there is no actor. An empty final actor grants nothing. `Allows` evaluates +each candidate independently. An empty requested resource ID asks for the entire +kind; a restricted grant cannot satisfy it. Kind-wide grants cover named IDs. +Actions are literal: no admin implication, wildcard expansion, prefix matching +or union of one grant's actions with another's resources. + +The reader checks a bounded canonical envelope and 64-byte signature shape, then +uses Core Decode. Go deliberately retains Core's stronger structural/schema and +attenuation checks; the Python and TypeScript readers only project readable +claims. All three agree on the shared Core-produced fixtures. None verifies a +signature, current time, live revisions or replay. Even a digest-bound context +may have been refused in warning mode. These claims alone never prove authority. +Keep audience, installed-definition, ownership, resource and live-visibility +checks at their existing owners. The exposed protobuf scopes are Core's current +public types during the authority migration; do not mutate returned copies and +expect to change the claims snapshot. + +From this directory, run `GOWORK=off go test -race ./...` and `go vet ./...`. +The tests consume `../testdata/claims.json`, whose gateway-side oracle verifies +Core-minted tokens at a fixed fixture clock. Synthetic mutated tokens in unit +tests exercise parsing only and are not valid credentials. + +Handwritten code lives outside `gen/`, the directory Codefly replaces. The root +module scaffold is preserved by `codefly generate client`. diff --git a/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go b/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go index 61ab7143..156549b7 100644 --- a/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go +++ b/libraries/authority-client/go/gen/host/composition/v1/composition.pb.go @@ -16,7 +16,7 @@ import ( sync "sync" unsafe "unsafe" - v1 "github.com/codefly-dev/authority-client-go/gen/host/authority/v1" + v1 "github.com/codefly-dev/authority/libraries/authority-client/go/gen/host/authority/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) diff --git a/libraries/authority-client/go/go.mod b/libraries/authority-client/go/go.mod index 29687193..9f0957b1 100644 --- a/libraries/authority-client/go/go.mod +++ b/libraries/authority-client/go/go.mod @@ -1,17 +1,25 @@ module github.com/codefly-dev/authority/libraries/authority-client/go -go 1.26.0 +go 1.27.0 require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 + github.com/codefly-dev/core v0.19.6-0.20261018051341-b81d92e6feda google.golang.org/genproto/googleapis/api v0.0.0-20261019180522-ce908d29299b google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 ) require ( - golang.org/x/net v0.57.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect + buf.build/go/protovalidate v1.4.0 // indirect + cel.dev/cel-go v0.32.0 // indirect + cel.dev/expr v0.25.3 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/google/uuid v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect + golang.org/x/net v0.60.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.42.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 // indirect ) diff --git a/libraries/authority-client/go/go.sum b/libraries/authority-client/go/go.sum index eefd92f5..79254b0b 100644 --- a/libraries/authority-client/go/go.sum +++ b/libraries/authority-client/go/go.sum @@ -1,15 +1,37 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM= +buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/codefly-dev/core v0.19.6-0.20261018051341-b81d92e6feda h1:ytTihtIEfVhQ7llQl+ls5swIL8JgJu89X0wlDMeqM+o= +github.com/codefly-dev/core v0.19.6-0.20261018051341-b81d92e6feda/go.mod h1:0uX/7lTBVaGV0RI2Mb3mpEr/PPdx/jZqXHym/U8FVDI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.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= 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-20261019180522-ce908d29299b h1:nB3spRIaSPtxJpxb0k41yljROvm2Jrwfs2pSNdKS5y8= diff --git a/libraries/authority-client/go/workcontext/claims.go b/libraries/authority-client/go/workcontext/claims.go new file mode 100644 index 00000000..3a162198 --- /dev/null +++ b/libraries/authority-client/go/workcontext/claims.go @@ -0,0 +1,94 @@ +// Package workcontext projects an already hop-bound Work Context. It is not a +// verifier: readable claims prove neither authentication nor live authority. +package workcontext + +import ( + "encoding/base64" + "errors" + "fmt" + "strings" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + core "github.com/codefly-dev/core/workcontext" + "google.golang.org/protobuf/proto" +) + +// ErrUnreadableContext names a token that cannot be projected. Core's structural +// refusals are also retained in the error chain; no signature is verified. +var ErrUnreadableContext = errors.New("authority: unreadable context") + +// Claims owns a snapshot of the audience and Core's effective scopes. +// It can be read concurrently. Its zero value grants nothing. +type Claims struct { + audience string + scopes []*basev0.WorkScopeV1 +} + +// ReadClaims reads the token bound by the hop, using Core's maintained decoder. +// In addition to canonical envelope checks, Go retains Core's structural checks; +// the Python/TypeScript projections do not implement that structural validator. +// None of these readers verifies signatures, time, live revisions or replay. +func ReadClaims(token string) (*Claims, error) { + if len(token) == 0 || len(token) > core.MaxTokenSize { + return nil, fmt.Errorf("%w: expected a bounded context", ErrUnreadableContext) + } + payload, signature, ok := strings.Cut(token, ".") + if !ok || strings.Contains(signature, ".") { + return nil, fmt.Errorf("%w: expected payload.signature", ErrUnreadableContext) + } + for n, segment := range []string{payload, signature} { + decoded, err := base64.RawURLEncoding.DecodeString(segment) + if err != nil || base64.RawURLEncoding.EncodeToString(decoded) != segment || (n == 1 && len(decoded) != 64) { + return nil, fmt.Errorf("%w: noncanonical envelope or invalid signature length", ErrUnreadableContext) + } + } + decoded, err := core.Decode(token) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrUnreadableContext, err) + } + context := decoded.Context() + scopes := context.GetAuthorityScopes() + if chain := context.GetActorChain(); len(chain) > 0 { + scopes = chain[len(chain)-1].GetGrantedScopes() + } + return &Claims{audience: context.GetAudience(), scopes: scopes}, nil +} + +// Audience is data to compare with this module's delivered audience. +func (c *Claims) Audience() string { + if c == nil { + return "" + } + return c.audience +} + +// Scopes returns deep copies; changing them cannot change later Allows results. +func (c *Claims) Scopes() []*basev0.WorkScopeV1 { + if c == nil { + return nil + } + out := make([]*basev0.WorkScopeV1, len(c.scopes)) + for n, scope := range c.scopes { + out[n] = proto.Clone(scope).(*basev0.WorkScopeV1) + } + return out +} + +// Allows asks for one literal action on one exact resource. An empty resourceID +// asks for the whole kind; a resource-limited grant cannot satisfy that request. +// No wildcard expansion, action implication or cross-grant merging occurs. +func (c *Claims) Allows(kind, action, resourceID string) bool { + if c == nil || kind == "" || action == "" { + return false + } + ask := &basev0.WorkScopeV1{ResourceKind: kind, Actions: []string{action}} + if resourceID != "" { + ask.ResourceIds = []string{resourceID} + } + for _, scope := range c.scopes { + if core.ScopeContained(ask, []*basev0.WorkScopeV1{scope}) { + return true + } + } + return false +} diff --git a/libraries/authority-client/go/workcontext/claims_test.go b/libraries/authority-client/go/workcontext/claims_test.go new file mode 100644 index 00000000..afc1eb94 --- /dev/null +++ b/libraries/authority-client/go/workcontext/claims_test.go @@ -0,0 +1,169 @@ +package workcontext_test + +import ( + "encoding/base64" + "encoding/json" + "errors" + "os" + "strings" + "testing" + + reader "github.com/codefly-dev/authority/libraries/authority-client/go/workcontext" + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + core "github.com/codefly-dev/core/workcontext" + "google.golang.org/protobuf/proto" +) + +type fixture struct { + Name, Token, Audience string + Scopes []*basev0.WorkScopeV1 + Probes []struct { + Kind, Action string + ResourceID string `json:"resource_id"` + Allowed bool + } +} + +func fixtures(t *testing.T) []fixture { + t.Helper() + data, err := os.ReadFile("../../testdata/claims.json") + if err != nil { + t.Fatal(err) + } + var out []fixture + if err := json.Unmarshal(data, &out); err != nil { + t.Fatal(err) + } + return out +} +func body(t *testing.T, name string) *basev0.WorkContextV1 { + t.Helper() + for _, f := range fixtures(t) { + if f.Name == name { + d, err := core.Decode(f.Token) + if err != nil { + t.Fatal(err) + } + return d.Context() + } + } + t.Fatal("fixture missing") + return nil +} + +// Synthetic signatures test projection only; they are not authenticated tokens. +func tokenFor(t *testing.T, c *basev0.WorkContextV1) string { + t.Helper() + b, err := (proto.MarshalOptions{Deterministic: true}).Marshal(c) + if err != nil { + t.Fatal(err) + } + return base64.RawURLEncoding.EncodeToString(b) + "." + base64.RawURLEncoding.EncodeToString(make([]byte, 64)) +} +func read(t *testing.T, c *basev0.WorkContextV1) *reader.Claims { + t.Helper() + out, err := reader.ReadClaims(tokenFor(t, c)) + if err != nil { + t.Fatal(err) + } + return out +} +func TestCoreFixtures(t *testing.T) { + for _, f := range fixtures(t) { + t.Run(f.Name, func(t *testing.T) { + c, err := reader.ReadClaims(f.Token) + if err != nil { + t.Fatal(err) + } + if c.Audience() != f.Audience { + t.Fatal("audience differs") + } + scopes := c.Scopes() + if len(scopes) != len(f.Scopes) { + t.Fatal("effective scope count differs") + } + for n, scope := range scopes { + if !proto.Equal(scope, f.Scopes[n]) { + t.Fatalf("scope %d differs", n) + } + } + for _, p := range f.Probes { + if c.Allows(p.Kind, p.Action, p.ResourceID) != p.Allowed { + t.Fatalf("probe %s %s %s", p.Kind, p.Action, p.ResourceID) + } + } + }) + } +} +func TestSeparateGrantsAndWholeKind(t *testing.T) { + c := body(t, "session") + c.AuthorityScopes = []*basev0.WorkScopeV1{ + {ResourceKind: "record", Actions: []string{"read"}, ResourceIds: []string{"a"}}, + {ResourceKind: "record", Actions: []string{"invoke"}, ResourceIds: []string{"b"}}, + } + claims := read(t, c) + for _, q := range []struct { + action, id string + allowed bool + }{{"read", "a", true}, {"invoke", "b", true}, {"invoke", "a", false}, {"read", "b", false}, {"read", "", false}} { + if claims.Allows("record", q.action, q.id) != q.allowed { + t.Fatal(q) + } + } + copied := claims.Scopes() + copied[0].Actions = append(copied[0].Actions, "invoke") + copied[0].ResourceIds = append(copied[0].ResourceIds, "b") + if claims.Allows("record", "invoke", "a") { + t.Fatal("mutable scope escaped") + } + c.AuthorityScopes = []*basev0.WorkScopeV1{{ResourceKind: "record", Actions: []string{"read", "admin", "*"}}} + claims = read(t, c) + if !claims.Allows("record", "read", "") || !claims.Allows("record", "read", "a") || claims.Allows("record", "invoke", "a") || claims.Allows("", "read", "") || claims.Allows("record", "", "") { + t.Fatal("literal scope semantics changed") + } + var empty reader.Claims + if empty.Allows("record", "read", "") { + t.Fatal("zero claims grant authority") + } +} +func TestEmptyFinalActorGrantsNothing(t *testing.T) { + c := body(t, "delegated") + c.ActorChain[len(c.ActorChain)-1].GrantedScopes = nil + claims := read(t, c) + if len(claims.Scopes()) != 0 || claims.Allows("record", "read", "record-conformance") { + t.Fatal("earlier scopes leaked") + } +} +func TestUnreadableContexts(t *testing.T) { + valid := tokenFor(t, body(t, "session")) + payload, _, _ := strings.Cut(valid, ".") + for _, token := range []string{"", "abc", "a.b.c", ".", "é.x", strings.Repeat("a", 32*1024+1), valid + "=", payload + ".AA", payload + "." + strings.Repeat("A", 85) + "B"} { + if _, err := reader.ReadClaims(token); !errors.Is(err, reader.ErrUnreadableContext) { + t.Fatalf("envelope not refused: %v", err) + } + } + for _, where := range []string{"root", "nested", "duplicate", "audience", "structural"} { + t.Run(where, func(t *testing.T) { + c := body(t, "session") + switch where { + case "root": + c.ProtoReflect().SetUnknown([]byte{0xf8, 0x07, 0x01}) + case "nested": + c.AuthorityScopes[0].ProtoReflect().SetUnknown([]byte{0x78, 0x01}) + case "audience": + c.Audience = "" + case "structural": + c.Seal = nil + } + token := tokenFor(t, c) + if where == "duplicate" { + p, s, _ := strings.Cut(token, ".") + b, _ := base64.RawURLEncoding.DecodeString(p) + token = base64.RawURLEncoding.EncodeToString(append(b, b...)) + "." + s + } + if _, err := reader.ReadClaims(token); !errors.Is(err, reader.ErrUnreadableContext) { + t.Fatalf("malformed claims not refused: %v", err) + } + }) + } +} diff --git a/libraries/authority-client/library.codefly.yaml b/libraries/authority-client/library.codefly.yaml index 0fe106c3..073cc028 100644 --- a/libraries/authority-client/library.codefly.yaml +++ b/libraries/authority-client/library.codefly.yaml @@ -10,13 +10,17 @@ languages: path: python/ exports: - authority_client + - name: typescript + path: typescript/ + exports: + - '@codefly-dev/authority-client' sources: - package: local version: 0.0.0 module: authority service: gateway endpoint: grpc - contract-digest: sha256:89baa9ae5c5f7940f17b303afdb58824e913427d3fb826c81b24f78e5378d599 + contract-digest: sha256:f589836f5fb21d4e1ed9a83659debaacb4eda07b8231fb55d01c1ce7603b898d services: - GatewayService generated-by: diff --git a/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2.py b/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2.py index 0d05a98f..22b2dab4 100644 --- a/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2.py +++ b/libraries/authority-client/python/authority_client/_gen/host/authority/v1/authority_pb2.py @@ -25,7 +25,7 @@ from authority_client._gen.host.authority.v1 import work_context_pb2 as host_dot_authority_dot_v1_dot_work__context__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!host/authority/v1/authority.proto\x12\x11host.authority.v1\x1a$host/authority/v1/work_context.proto\"\xf3\x04\n\x0cIssueRequest\x12:\n\x06person\x18\x01 \x01(\x0b\x32 .host.authority.v1.PersonSessionH\x00R\x06person\x12\x43\n\x08workload\x18\x02 \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialH\x00R\x08workload\x12H\n\x0cinstallation\x18\x03 \x01(\x0b\x32\".host.authority.v1.InstallationRefH\x00R\x0cinstallation\x12#\n\x0cparent_token\x18\x04 \x01(\tH\x00R\x0bparentToken\x12\x42\n\ndelegation\x18\x0b \x01(\x0b\x32 .host.authority.v1.DelegationRefH\x00R\ndelegation\x12\x1a\n\x08\x61udience\x18\x05 \x01(\tR\x08\x61udience\x12\x36\n\x06scopes\x18\x06 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x1f\n\x0bttl_seconds\x18\x07 \x01(\x03R\nttlSeconds\x12\x30\n\x14operation_binding_id\x18\x08 \x01(\tR\x12operationBindingId\x12\'\n\x0forganization_id\x18\t \x01(\tR\x0eorganizationId\x12;\n\x05\x61\x63tor\x18\n \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialR\x05\x61\x63tor\x12\x1c\n\toperation\x18\x0c \x01(\tR\toperationB\x04\n\x02\x61s\"4\n\rDelegationRef\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\"{\n\x0f\x44\x65legateRequest\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\x12\x36\n\x06scopes\x18\x02 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x18\n\x07purpose\x18\x03 \x01(\tR\x07purpose\"\xf6\x01\n\nDelegation\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\x12\'\n\x0forganization_id\x18\x02 \x01(\tR\x0eorganizationId\x12\x18\n\x07grantor\x18\x03 \x01(\tR\x07grantor\x12\x16\n\x06module\x18\x04 \x01(\tR\x06module\x12\x36\n\x06scopes\x18\x05 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x18\n\x07purpose\x18\x06 \x01(\tR\x07purpose\x12\x16\n\x06\x61\x63tive\x18\x07 \x01(\x08R\x06\x61\x63tive\"x\n\x14GetDelegationRequest\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\x12;\n\x05\x61\x63tor\x18\x02 \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialR\x05\x61\x63tor\">\n\x17RevokeDelegationRequest\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\"Y\n\x18RevokeDelegationResponse\x12=\n\ndelegation\x18\x01 \x01(\x0b\x32\x1d.host.authority.v1.DelegationR\ndelegation\"!\n\rPersonSessionJ\x04\x08\x01\x10\x02R\nsession_id\"4\n\x12WorkloadCredential\x12\x1e\n\ncredential\x18\x01 \x01(\tR\ncredential\":\n\x0fInstallationRef\x12\'\n\x0finstallation_id\x18\x01 \x01(\tR\x0einstallationId\"\x89\x01\n\rIssueResponse\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12&\n\x0f\x65xpires_at_unix\x18\x02 \x01(\x03R\rexpiresAtUnix\x12:\n\x07\x63ontext\x18\x03 \x01(\x0b\x32 .host.authority.v1.WorkContextV1R\x07\x63ontext\"X\n\x0c\x43heckRequest\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12\x16\n\x06method\x18\x03 \x01(\tR\x06method\"\x82\x01\n\rCheckResponse\x12:\n\x07\x63ontext\x18\x01 \x01(\x0b\x32 .host.authority.v1.WorkContextV1R\x07\x63ontext\x12\x35\n\x16\x61uthorization_revision\x18\x02 \x01(\x04R\x15\x61uthorizationRevision\"\r\n\x0bKeysRequest\"X\n\x0cKeysResponse\x12\x16\n\x06issuer\x18\x01 \x01(\tR\x06issuer\x12\x30\n\x04keys\x18\x02 \x03(\x0b\x32\x1c.host.authority.v1.PublicKeyR\x04keys\"_\n\tPublicKey\x12\x15\n\x06key_id\x18\x01 \x01(\tR\x05keyId\x12\x1c\n\talgorithm\x18\x02 \x01(\tR\talgorithm\x12\x1d\n\npublic_key\x18\x03 \x01(\x0cR\tpublicKey\"\x85\x01\n\x14ListResourcesRequest\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12#\n\rresource_kind\x18\x03 \x01(\tR\x0cresourceKind\x12\x16\n\x06\x61\x63tion\x18\x04 \x01(\tR\x06\x61\x63tion\"Q\n\x15ListResourcesResponse\x12\x38\n\x06grants\x18\x01 \x03(\x0b\x32 .host.authority.v1.ResourceGrantR\x06grants\"t\n\rResourceGrant\x12\x12\n\x03\x61ll\x18\x01 \x01(\x08H\x00R\x03\x61ll\x12\x10\n\x02id\x18\x02 \x01(\tH\x00R\x02id\x12\x18\n\x06prefix\x18\x03 \x01(\tH\x00R\x06prefix\x12\x1a\n\x07pattern\x18\x04 \x01(\tH\x00R\x07patternB\x07\n\x05match2\xec\x04\n\x10\x41uthorityService\x12J\n\x05Issue\x12\x1f.host.authority.v1.IssueRequest\x1a .host.authority.v1.IssueResponse\x12J\n\x05\x43heck\x12\x1f.host.authority.v1.CheckRequest\x1a .host.authority.v1.CheckResponse\x12G\n\x04Keys\x12\x1e.host.authority.v1.KeysRequest\x1a\x1f.host.authority.v1.KeysResponse\x12\x62\n\rListResources\x12\'.host.authority.v1.ListResourcesRequest\x1a(.host.authority.v1.ListResourcesResponse\x12M\n\x08\x44\x65legate\x12\".host.authority.v1.DelegateRequest\x1a\x1d.host.authority.v1.Delegation\x12k\n\x10RevokeDelegation\x12*.host.authority.v1.RevokeDelegationRequest\x1a+.host.authority.v1.RevokeDelegationResponse\x12W\n\rGetDelegation\x12\'.host.authority.v1.GetDelegationRequest\x1a\x1d.host.authority.v1.Delegationb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!host/authority/v1/authority.proto\x12\x11host.authority.v1\x1a$host/authority/v1/work_context.proto\"\xf3\x04\n\x0cIssueRequest\x12:\n\x06person\x18\x01 \x01(\x0b\x32 .host.authority.v1.PersonSessionH\x00R\x06person\x12\x43\n\x08workload\x18\x02 \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialH\x00R\x08workload\x12H\n\x0cinstallation\x18\x03 \x01(\x0b\x32\".host.authority.v1.InstallationRefH\x00R\x0cinstallation\x12#\n\x0cparent_token\x18\x04 \x01(\tH\x00R\x0bparentToken\x12\x42\n\ndelegation\x18\x0b \x01(\x0b\x32 .host.authority.v1.DelegationRefH\x00R\ndelegation\x12\x1a\n\x08\x61udience\x18\x05 \x01(\tR\x08\x61udience\x12\x36\n\x06scopes\x18\x06 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x1f\n\x0bttl_seconds\x18\x07 \x01(\x03R\nttlSeconds\x12\x30\n\x14operation_binding_id\x18\x08 \x01(\tR\x12operationBindingId\x12\'\n\x0forganization_id\x18\t \x01(\tR\x0eorganizationId\x12;\n\x05\x61\x63tor\x18\n \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialR\x05\x61\x63tor\x12\x1c\n\toperation\x18\x0c \x01(\tR\toperationB\x04\n\x02\x61s\"4\n\rDelegationRef\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\"{\n\x0f\x44\x65legateRequest\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\x12\x36\n\x06scopes\x18\x02 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x18\n\x07purpose\x18\x03 \x01(\tR\x07purpose\"\xf6\x01\n\nDelegation\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\x12\'\n\x0forganization_id\x18\x02 \x01(\tR\x0eorganizationId\x12\x18\n\x07grantor\x18\x03 \x01(\tR\x07grantor\x12\x16\n\x06module\x18\x04 \x01(\tR\x06module\x12\x36\n\x06scopes\x18\x05 \x03(\x0b\x32\x1e.host.authority.v1.WorkScopeV1R\x06scopes\x12\x18\n\x07purpose\x18\x06 \x01(\tR\x07purpose\x12\x16\n\x06\x61\x63tive\x18\x07 \x01(\x08R\x06\x61\x63tive\"x\n\x14GetDelegationRequest\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\x12;\n\x05\x61\x63tor\x18\x02 \x01(\x0b\x32%.host.authority.v1.WorkloadCredentialR\x05\x61\x63tor\">\n\x17RevokeDelegationRequest\x12#\n\rdelegation_id\x18\x01 \x01(\tR\x0c\x64\x65legationId\"Y\n\x18RevokeDelegationResponse\x12=\n\ndelegation\x18\x01 \x01(\x0b\x32\x1d.host.authority.v1.DelegationR\ndelegation\"!\n\rPersonSessionJ\x04\x08\x01\x10\x02R\nsession_id\"4\n\x12WorkloadCredential\x12\x1e\n\ncredential\x18\x01 \x01(\tR\ncredential\":\n\x0fInstallationRef\x12\'\n\x0finstallation_id\x18\x01 \x01(\tR\x0einstallationId\"\x89\x01\n\rIssueResponse\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12&\n\x0f\x65xpires_at_unix\x18\x02 \x01(\x03R\rexpiresAtUnix\x12:\n\x07\x63ontext\x18\x03 \x01(\x0b\x32 .host.authority.v1.WorkContextV1R\x07\x63ontext\"t\n\x0c\x43heckRequest\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12\x16\n\x06method\x18\x03 \x01(\tR\x06method\x12\x1a\n\x08\x61\x64visory\x18\x04 \x01(\x08R\x08\x61\x64visory\"\x9c\x01\n\rCheckResponse\x12:\n\x07\x63ontext\x18\x01 \x01(\x0b\x32 .host.authority.v1.WorkContextV1R\x07\x63ontext\x12\x35\n\x16\x61uthorization_revision\x18\x02 \x01(\x04R\x15\x61uthorizationRevision\x12\x18\n\x07refusal\x18\x03 \x01(\tR\x07refusal\"\r\n\x0bKeysRequest\"X\n\x0cKeysResponse\x12\x16\n\x06issuer\x18\x01 \x01(\tR\x06issuer\x12\x30\n\x04keys\x18\x02 \x03(\x0b\x32\x1c.host.authority.v1.PublicKeyR\x04keys\"_\n\tPublicKey\x12\x15\n\x06key_id\x18\x01 \x01(\tR\x05keyId\x12\x1c\n\talgorithm\x18\x02 \x01(\tR\talgorithm\x12\x1d\n\npublic_key\x18\x03 \x01(\x0cR\tpublicKey\"\x85\x01\n\x14ListResourcesRequest\x12\x14\n\x05token\x18\x01 \x01(\tR\x05token\x12\x1a\n\x08\x61udience\x18\x02 \x01(\tR\x08\x61udience\x12#\n\rresource_kind\x18\x03 \x01(\tR\x0cresourceKind\x12\x16\n\x06\x61\x63tion\x18\x04 \x01(\tR\x06\x61\x63tion\"Q\n\x15ListResourcesResponse\x12\x38\n\x06grants\x18\x01 \x03(\x0b\x32 .host.authority.v1.ResourceGrantR\x06grants\"t\n\rResourceGrant\x12\x12\n\x03\x61ll\x18\x01 \x01(\x08H\x00R\x03\x61ll\x12\x10\n\x02id\x18\x02 \x01(\tH\x00R\x02id\x12\x18\n\x06prefix\x18\x03 \x01(\tH\x00R\x06prefix\x12\x1a\n\x07pattern\x18\x04 \x01(\tH\x00R\x07patternB\x07\n\x05match2\xec\x04\n\x10\x41uthorityService\x12J\n\x05Issue\x12\x1f.host.authority.v1.IssueRequest\x1a .host.authority.v1.IssueResponse\x12J\n\x05\x43heck\x12\x1f.host.authority.v1.CheckRequest\x1a .host.authority.v1.CheckResponse\x12G\n\x04Keys\x12\x1e.host.authority.v1.KeysRequest\x1a\x1f.host.authority.v1.KeysResponse\x12\x62\n\rListResources\x12\'.host.authority.v1.ListResourcesRequest\x1a(.host.authority.v1.ListResourcesResponse\x12M\n\x08\x44\x65legate\x12\".host.authority.v1.DelegateRequest\x1a\x1d.host.authority.v1.Delegation\x12k\n\x10RevokeDelegation\x12*.host.authority.v1.RevokeDelegationRequest\x1a+.host.authority.v1.RevokeDelegationResponse\x12W\n\rGetDelegation\x12\'.host.authority.v1.GetDelegationRequest\x1a\x1d.host.authority.v1.Delegationb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -55,21 +55,21 @@ _globals['_ISSUERESPONSE']._serialized_start=1579 _globals['_ISSUERESPONSE']._serialized_end=1716 _globals['_CHECKREQUEST']._serialized_start=1718 - _globals['_CHECKREQUEST']._serialized_end=1806 - _globals['_CHECKRESPONSE']._serialized_start=1809 - _globals['_CHECKRESPONSE']._serialized_end=1939 - _globals['_KEYSREQUEST']._serialized_start=1941 - _globals['_KEYSREQUEST']._serialized_end=1954 - _globals['_KEYSRESPONSE']._serialized_start=1956 - _globals['_KEYSRESPONSE']._serialized_end=2044 - _globals['_PUBLICKEY']._serialized_start=2046 - _globals['_PUBLICKEY']._serialized_end=2141 - _globals['_LISTRESOURCESREQUEST']._serialized_start=2144 - _globals['_LISTRESOURCESREQUEST']._serialized_end=2277 - _globals['_LISTRESOURCESRESPONSE']._serialized_start=2279 - _globals['_LISTRESOURCESRESPONSE']._serialized_end=2360 - _globals['_RESOURCEGRANT']._serialized_start=2362 - _globals['_RESOURCEGRANT']._serialized_end=2478 - _globals['_AUTHORITYSERVICE']._serialized_start=2481 - _globals['_AUTHORITYSERVICE']._serialized_end=3101 + _globals['_CHECKREQUEST']._serialized_end=1834 + _globals['_CHECKRESPONSE']._serialized_start=1837 + _globals['_CHECKRESPONSE']._serialized_end=1993 + _globals['_KEYSREQUEST']._serialized_start=1995 + _globals['_KEYSREQUEST']._serialized_end=2008 + _globals['_KEYSRESPONSE']._serialized_start=2010 + _globals['_KEYSRESPONSE']._serialized_end=2098 + _globals['_PUBLICKEY']._serialized_start=2100 + _globals['_PUBLICKEY']._serialized_end=2195 + _globals['_LISTRESOURCESREQUEST']._serialized_start=2198 + _globals['_LISTRESOURCESREQUEST']._serialized_end=2331 + _globals['_LISTRESOURCESRESPONSE']._serialized_start=2333 + _globals['_LISTRESOURCESRESPONSE']._serialized_end=2414 + _globals['_RESOURCEGRANT']._serialized_start=2416 + _globals['_RESOURCEGRANT']._serialized_end=2532 + _globals['_AUTHORITYSERVICE']._serialized_start=2535 + _globals['_AUTHORITYSERVICE']._serialized_end=3155 # @@protoc_insertion_point(module_scope) diff --git a/libraries/authority-client/typescript/README.md b/libraries/authority-client/typescript/README.md new file mode 100644 index 00000000..8b8de849 --- /dev/null +++ b/libraries/authority-client/typescript/README.md @@ -0,0 +1,40 @@ +# Authority TypeScript client + +Codefly generates the RPC bindings exported by `@codefly-dev/authority-client`. +The handwritten, non-verifying claims reader has a separate public export: + +```ts +import { readClaims } from '@codefly-dev/authority-client/work-context'; + +const claims = readClaims(boundToken); +// Compare claims.audience to the delivered module audience. +const allowed = claims.allows(resourceKind, action, exactResourceID); +``` + +The input must already be bound by a trusted integration. This package supplies +no TypeScript hop reader; readable claims are not proof of authentication. The +projection verifies neither signatures, current time, live revisions nor replay. +A warning-mode hop can bind a context the authority refused. Preserve the +integration's authentication and the module's audience, installed-definition, +ownership and live resource-visibility checks. + +`claims.scopes` contains frozen copies of the final actor's grants, or the owner's +when no actor exists. Empty final grants stay empty. Each scope is evaluated +independently. An omitted/empty resource ID asks for the whole kind; a restricted +grant cannot satisfy it. Whole-kind grants include named IDs. No wildcard or +admin implication, prefix/pattern match or cross-scope action/ID merging occurs. + +`UnreadableContext` names malformed/oversized envelopes, noncanonical base64url, +wrong signature length, unreadable protobuf, absent audience, unknown fields and +noncanonical protobuf bytes. This is a projection through the generated schema, +not Core's structural validator. Go also applies Core Decode's stronger schema +and attenuation checks. Shared Core-produced fixtures prove equal audience, +effective scope and exact-resource answers across the three language readers. + +Run `npm ci --ignore-scripts` and `npm test`. Tests import the built public package +exports, including the generated schema. No running SaaS, service or provider is +needed. Test signatures and fixed-clock fixtures are not live credentials. + +The generator owns `src/`, including its index. The helper lives at the package +root; preserved `package.json`/`tsconfig.json` scaffolds export and compile it +alongside generated bindings. Never edit the generated index to add this helper. diff --git a/libraries/authority-client/typescript/package-lock.json b/libraries/authority-client/typescript/package-lock.json new file mode 100644 index 00000000..8d24dcab --- /dev/null +++ b/libraries/authority-client/typescript/package-lock.json @@ -0,0 +1,48 @@ +{ + "name": "@codefly-dev/authority-client", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@codefly-dev/authority-client", + "version": "0.0.1", + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "@connectrpc/connect": "^2.0.0" + }, + "devDependencies": { + "typescript": "^5.9.3" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.16.0.tgz", + "integrity": "sha512-FWa0sPlqYGJgpTs6OxBRcL/AW4JT0OIO+W1ajerluoTVh8CV0B7XM1qsNCRgnkSCkRCgOICKHRQlY17jrsm0+Q==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@connectrpc/connect": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.2.0.tgz", + "integrity": "sha512-M63OQ1ZFcPUm85CuewJ9G9CMV8LjzGR9935xOnXNQGSazAbwMb3x2W7vXBYM1zqrP7XFD8uxp/dUAhirclgwnQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^2.7.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/libraries/authority-client/typescript/package.json b/libraries/authority-client/typescript/package.json new file mode 100644 index 00000000..9386ffd0 --- /dev/null +++ b/libraries/authority-client/typescript/package.json @@ -0,0 +1,33 @@ +{ + "name": "@codefly-dev/authority-client", + "version": "0.0.1", + "type": "module", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./work-context": { + "types": "./dist/work-context.d.ts", + "import": "./dist/work-context.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "npm run build && node --test tests/*.test.mjs" + }, + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "@connectrpc/connect": "^2.0.0" + }, + "devDependencies": { + "typescript": "^5.9.3" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + } +} diff --git a/libraries/authority-client/typescript/src/gen/api_pb.ts b/libraries/authority-client/typescript/src/gen/api_pb.ts new file mode 100644 index 00000000..9abd2a3a --- /dev/null +++ b/libraries/authority-client/typescript/src/gen/api_pb.ts @@ -0,0 +1,60 @@ +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=js" +// @generated from file api.proto (package api, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_google_api_annotations } from "./google/api/annotations_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file api.proto. + */ +export const file_api: GenFile = /*@__PURE__*/ + fileDesc("CglhcGkucHJvdG8SA2FwaSIQCg5WZXJzaW9uUmVxdWVzdCIiCg9WZXJzaW9uUmVzcG9uc2USDwoHdmVyc2lvbhgBIAEoCTJYCg5HYXRld2F5U2VydmljZRJGCgdWZXJzaW9uEhMuYXBpLlZlcnNpb25SZXF1ZXN0GhQuYXBpLlZlcnNpb25SZXNwb25zZSIQgtPkkwIKEggvdmVyc2lvbkIVWhNnYXRld2F5L3BrZy9nZW47Z2VuYgZwcm90bzM", [file_google_api_annotations]); + +/** + * @generated from message api.VersionRequest + */ +export type VersionRequest = Message<"api.VersionRequest"> & { +}; + +/** + * Describes the message api.VersionRequest. + * Use `create(VersionRequestSchema)` to create a new message. + */ +export const VersionRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api, 0); + +/** + * @generated from message api.VersionResponse + */ +export type VersionResponse = Message<"api.VersionResponse"> & { + /** + * @generated from field: string version = 1; + */ + version: string; +}; + +/** + * Describes the message api.VersionResponse. + * Use `create(VersionResponseSchema)` to create a new message. + */ +export const VersionResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api, 1); + +/** + * @generated from service api.GatewayService + */ +export const GatewayService: GenService<{ + /** + * @generated from rpc api.GatewayService.Version + */ + version: { + methodKind: "unary"; + input: typeof VersionRequestSchema; + output: typeof VersionResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_api, 0); + diff --git a/libraries/authority-client/typescript/src/gen/buf/validate/validate_pb.ts b/libraries/authority-client/typescript/src/gen/buf/validate/validate_pb.ts new file mode 100644 index 00000000..3d68b110 --- /dev/null +++ b/libraries/authority-client/typescript/src/gen/buf/validate/validate_pb.ts @@ -0,0 +1,5034 @@ +// Copyright 2023-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=js" +// @generated from file buf/validate/validate.proto (package buf.validate, syntax proto2) +/* eslint-disable */ + +// [Protovalidate](https://protovalidate.com/) is the semantic validation library for Protobuf. +// It provides standard annotations to validate common rules on messages and fields, as well as the ability to use [CEL](https://cel.dev) to write custom rules. +// It's the next generation of [protoc-gen-validate](https://github.com/bufbuild/protoc-gen-validate). +// +// This package provides the options, messages, and enums that power Protovalidate. +// Apply its options to messages, fields, and oneofs in your Protobuf schemas to add validation rules: +// +// ```proto +// message User { +// string id = 1 [(buf.validate.field).string.uuid = true]; +// string first_name = 2 [(buf.validate.field).string.max_len = 64]; +// string last_name = 3 [(buf.validate.field).string.max_len = 64]; +// +// option (buf.validate.message).cel = { +// id: "first_name_requires_last_name" +// message: "last_name must be present if first_name is present" +// expression: "!has(this.first_name) || has(this.last_name)" +// }; +// } +// ``` +// +// These rules are enforced at runtime by language-specific libraries. +// See the [developer quickstart](https://protovalidate.com/quickstart/) to get started, or go directly to the runtime library for your language: +// [Go](https://github.com/bufbuild/protovalidate-go), +// [JavaScript/TypeScript](https://github.com/bufbuild/protovalidate-es), +// [Java](https://github.com/bufbuild/protovalidate-java), +// [Python](https://github.com/bufbuild/protovalidate-py), +// or [C++](https://github.com/bufbuild/protovalidate-cc). + +import type { GenEnum, GenExtension, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, extDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Duration, FieldDescriptorProto_Type, FieldMask, FieldOptions, MessageOptions, OneofOptions, Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_field_mask, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file buf/validate/validate.proto. + */ +export const file_buf_validate_validate: GenFile = /*@__PURE__*/ + fileDesc("ChtidWYvdmFsaWRhdGUvdmFsaWRhdGUucHJvdG8SDGJ1Zi52YWxpZGF0ZSI3CgRSdWxlEgoKAmlkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSEgoKZXhwcmVzc2lvbhgDIAEoCSKGAQoMTWVzc2FnZVJ1bGVzEhYKDmNlbF9leHByZXNzaW9uGAUgAygJEh8KA2NlbBgDIAMoCzISLmJ1Zi52YWxpZGF0ZS5SdWxlEi0KBW9uZW9mGAQgAygLMh4uYnVmLnZhbGlkYXRlLk1lc3NhZ2VPbmVvZlJ1bGVKBAgBEAJSCGRpc2FibGVkIjQKEE1lc3NhZ2VPbmVvZlJ1bGUSDgoGZmllbGRzGAEgAygJEhAKCHJlcXVpcmVkGAIgASgIIh4KCk9uZW9mUnVsZXMSEAoIcmVxdWlyZWQYASABKAgiiwkKCkZpZWxkUnVsZXMSFgoOY2VsX2V4cHJlc3Npb24YHSADKAkSHwoDY2VsGBcgAygLMhIuYnVmLnZhbGlkYXRlLlJ1bGUSEAoIcmVxdWlyZWQYGSABKAgSJAoGaWdub3JlGBsgASgOMhQuYnVmLnZhbGlkYXRlLklnbm9yZRIpCgVmbG9hdBgBIAEoCzIYLmJ1Zi52YWxpZGF0ZS5GbG9hdFJ1bGVzSAASKwoGZG91YmxlGAIgASgLMhkuYnVmLnZhbGlkYXRlLkRvdWJsZVJ1bGVzSAASKQoFaW50MzIYAyABKAsyGC5idWYudmFsaWRhdGUuSW50MzJSdWxlc0gAEikKBWludDY0GAQgASgLMhguYnVmLnZhbGlkYXRlLkludDY0UnVsZXNIABIrCgZ1aW50MzIYBSABKAsyGS5idWYudmFsaWRhdGUuVUludDMyUnVsZXNIABIrCgZ1aW50NjQYBiABKAsyGS5idWYudmFsaWRhdGUuVUludDY0UnVsZXNIABIrCgZzaW50MzIYByABKAsyGS5idWYudmFsaWRhdGUuU0ludDMyUnVsZXNIABIrCgZzaW50NjQYCCABKAsyGS5idWYudmFsaWRhdGUuU0ludDY0UnVsZXNIABItCgdmaXhlZDMyGAkgASgLMhouYnVmLnZhbGlkYXRlLkZpeGVkMzJSdWxlc0gAEi0KB2ZpeGVkNjQYCiABKAsyGi5idWYudmFsaWRhdGUuRml4ZWQ2NFJ1bGVzSAASLwoIc2ZpeGVkMzIYCyABKAsyGy5idWYudmFsaWRhdGUuU0ZpeGVkMzJSdWxlc0gAEi8KCHNmaXhlZDY0GAwgASgLMhsuYnVmLnZhbGlkYXRlLlNGaXhlZDY0UnVsZXNIABInCgRib29sGA0gASgLMhcuYnVmLnZhbGlkYXRlLkJvb2xSdWxlc0gAEisKBnN0cmluZxgOIAEoCzIZLmJ1Zi52YWxpZGF0ZS5TdHJpbmdSdWxlc0gAEikKBWJ5dGVzGA8gASgLMhguYnVmLnZhbGlkYXRlLkJ5dGVzUnVsZXNIABInCgRlbnVtGBAgASgLMhcuYnVmLnZhbGlkYXRlLkVudW1SdWxlc0gAEi8KCHJlcGVhdGVkGBIgASgLMhsuYnVmLnZhbGlkYXRlLlJlcGVhdGVkUnVsZXNIABIlCgNtYXAYEyABKAsyFi5idWYudmFsaWRhdGUuTWFwUnVsZXNIABIlCgNhbnkYFCABKAsyFi5idWYudmFsaWRhdGUuQW55UnVsZXNIABIvCghkdXJhdGlvbhgVIAEoCzIbLmJ1Zi52YWxpZGF0ZS5EdXJhdGlvblJ1bGVzSAASMgoKZmllbGRfbWFzaxgcIAEoCzIcLmJ1Zi52YWxpZGF0ZS5GaWVsZE1hc2tSdWxlc0gAEjEKCXRpbWVzdGFtcBgWIAEoCzIcLmJ1Zi52YWxpZGF0ZS5UaW1lc3RhbXBSdWxlc0gAQgYKBHR5cGVKBAgYEBlKBAgaEBtSB3NraXBwZWRSDGlnbm9yZV9lbXB0eSJVCg9QcmVkZWZpbmVkUnVsZXMSHwoDY2VsGAEgAygLMhIuYnVmLnZhbGlkYXRlLlJ1bGVKBAgYEBlKBAgaEBtSB3NraXBwZWRSDGlnbm9yZV9lbXB0eSL4FgoKRmxvYXRSdWxlcxJ9CgVjb25zdBgBIAEoAkJuwkhrCmkKC2Zsb2F0LmNvbnN0Glp0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICdtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSmQEKAmx0GAIgASgCQooBwkiGAQqDAQoIZmxvYXQubHQadyFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQpPyAnbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASqQEKA2x0ZRgDIAEoAkKZAcJIlQEKkgEKCWZsb2F0Lmx0ZRqEASFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUpPyAnbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEtAHCgJndBgEIAEoAkLBB8JIvQcKhgEKCGZsb2F0Lmd0GnohaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwq9AQoLZmxvYXQuZ3RfbHQarQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrHAQoVZmxvYXQuZ3RfbHRfZXhjbHVzaXZlGq0BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCkpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKzQEKDGZsb2F0Lmd0X2x0ZRq8AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCtcBChZmbG9hdC5ndF9sdGVfZXhjbHVzaXZlGrwBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARKcCAoDZ3RlGAUgASgCQowIwkiICAqVAQoJZmxvYXQuZ3RlGocBIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCswBCgxmbG9hdC5ndGVfbHQauwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCtYBChZmbG9hdC5ndGVfbHRfZXhjbHVzaXZlGrsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrcAQoNZmxvYXQuZ3RlX2x0ZRrKAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK5gEKF2Zsb2F0Lmd0ZV9sdGVfZXhjbHVzaXZlGsoBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSkpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEnkKAmluGAYgAygCQm3CSGoKaAoIZmxvYXQuaW4aXCEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ211c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnAKBm5vdF9pbhgHIAMoAkJgwkhdClsKDGZsb2F0Lm5vdF9pbhpLdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAnbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEm8KBmZpbml0ZRgIIAEoCEJfwkhcCloKDGZsb2F0LmZpbml0ZRpKcnVsZXMuZmluaXRlID8gKHRoaXMuaXNOYW4oKSB8fCB0aGlzLmlzSW5mKCkgPyAnbXVzdCBiZSBmaW5pdGUnIDogJycpIDogJycSKwoHZXhhbXBsZRgJIAMoAkIawkgXChUKDWZsb2F0LmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIooXCgtEb3VibGVSdWxlcxJ+CgVjb25zdBgBIAEoAUJvwkhsCmoKDGRvdWJsZS5jb25zdBpadGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAnbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEpoBCgJsdBgCIAEoAUKLAcJIhwEKhAEKCWRvdWJsZS5sdBp3IWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA+PSBydWxlcy5sdCk/ICdtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKqAQoDbHRlGAMgASgBQpoBwkiWAQqTAQoKZG91YmxlLmx0ZRqEASFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiAodGhpcy5pc05hbigpIHx8IHRoaXMgPiBydWxlcy5sdGUpPyAnbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEtUHCgJndBgEIAEoAULGB8JIwgcKhwEKCWRvdWJsZS5ndBp6IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKvgEKDGRvdWJsZS5ndF9sdBqtAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCsgBChZkb3VibGUuZ3RfbHRfZXhjbHVzaXZlGq0BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCkpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKzgEKDWRvdWJsZS5ndF9sdGUavAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrYAQoXZG91YmxlLmd0X2x0ZV9leGNsdXNpdmUavAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAodGhpcy5pc05hbigpIHx8IChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEqEICgNndGUYBSABKAFCkQjCSI0ICpYBCgpkb3VibGUuZ3RlGocBIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmICh0aGlzLmlzTmFuKCkgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCs0BCg1kb3VibGUuZ3RlX2x0GrsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrXAQoXZG91YmxlLmd0ZV9sdF9leGNsdXNpdmUauwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmICh0aGlzLmlzTmFuKCkgfHwgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSkpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCt0BCg5kb3VibGUuZ3RlX2x0ZRrKAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMuaXNOYW4oKSB8fCB0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK5wEKGGRvdWJsZS5ndGVfbHRlX2V4Y2x1c2l2ZRrKAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAodGhpcy5pc05hbigpIHx8IChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ6CgJpbhgGIAMoAUJuwkhrCmkKCWRvdWJsZS5pbhpcISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAnbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycScQoGbm90X2luGAcgAygBQmHCSF4KXAoNZG91YmxlLm5vdF9pbhpLdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAnbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEnAKBmZpbml0ZRgIIAEoCEJgwkhdClsKDWRvdWJsZS5maW5pdGUaSnJ1bGVzLmZpbml0ZSA/ICh0aGlzLmlzTmFuKCkgfHwgdGhpcy5pc0luZigpID8gJ211c3QgYmUgZmluaXRlJyA6ICcnKSA6ICcnEiwKB2V4YW1wbGUYCSADKAFCG8JIGAoWCg5kb3VibGUuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4isBQKCkludDMyUnVsZXMSfQoFY29uc3QYASABKAVCbsJIawppCgtpbnQzMi5jb25zdBpadGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAnbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEoQBCgJsdBgCIAEoBUJ2wkhzCnEKCGludDMyLmx0GmUhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ211c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEpUBCgNsdGUYAyABKAVChQHCSIEBCn8KCWludDMyLmx0ZRpyIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICdtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAAS+QYKAmd0GAQgASgFQuoGwkjmBgp0CghpbnQzMi5ndBpoIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKrQEKC2ludDMyLmd0X2x0Gp0BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq1AQoVaW50MzIuZ3RfbHRfZXhjbHVzaXZlGpsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvQEKDGludDMyLmd0X2x0ZRqsAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKxQEKFmludDMyLmd0X2x0ZV9leGNsdXNpdmUaqgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEsUHCgNndGUYBSABKAVCtQfCSLEHCoIBCglpbnQzMi5ndGUadSFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwq8AQoMaW50MzIuZ3RlX2x0GqsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCsQBChZpbnQzMi5ndGVfbHRfZXhjbHVzaXZlGqkBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrMAQoNaW50MzIuZ3RlX2x0ZRq6AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrUAQoXaW50MzIuZ3RlX2x0ZV9leGNsdXNpdmUauAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESeQoCaW4YBiADKAVCbcJIagpoCghpbnQzMi5pbhpcISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAnbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycScAoGbm90X2luGAcgAygFQmDCSF0KWwoMaW50MzIubm90X2luGkt0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICdtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSKwoHZXhhbXBsZRgIIAMoBUIawkgXChUKDWludDMyLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIrAUCgpJbnQ2NFJ1bGVzEn0KBWNvbnN0GAEgASgDQm7CSGsKaQoLaW50NjQuY29uc3QaWnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKEAQoCbHQYAiABKANCdsJIcwpxCghpbnQ2NC5sdBplIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICdtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKVAQoDbHRlGAMgASgDQoUBwkiBAQp/CglpbnQ2NC5sdGUaciFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAnbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEvkGCgJndBgEIAEoA0LqBsJI5gYKdAoIaW50NjQuZ3QaaCFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCq0BCgtpbnQ2NC5ndF9sdBqdAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKtQEKFWludDY0Lmd0X2x0X2V4Y2x1c2l2ZRqbAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr0BCgxpbnQ2NC5ndF9sdGUarAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCsUBChZpbnQ2NC5ndF9sdGVfZXhjbHVzaXZlGqoBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLFBwoDZ3RlGAUgASgDQrUHwkixBwqCAQoJaW50NjQuZ3RlGnUhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKvAEKDGludDY0Lmd0ZV9sdBqrAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrEAQoWaW50NjQuZ3RlX2x0X2V4Y2x1c2l2ZRqpAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzAEKDWludDY0Lmd0ZV9sdGUaugFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK1AEKF2ludDY0Lmd0ZV9sdGVfZXhjbHVzaXZlGrgBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEnkKAmluGAYgAygDQm3CSGoKaAoIaW50NjQuaW4aXCEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ211c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnAKBm5vdF9pbhgHIAMoA0JgwkhdClsKDGludDY0Lm5vdF9pbhpLdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAnbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEisKB2V4YW1wbGUYCSADKANCGsJIFwoVCg1pbnQ2NC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiLCFAoLVUludDMyUnVsZXMSfgoFY29uc3QYASABKA1Cb8JIbApqCgx1aW50MzIuY29uc3QaWnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKFAQoCbHQYAiABKA1Cd8JIdApyCgl1aW50MzIubHQaZSFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID49IHJ1bGVzLmx0PyAnbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASlwEKA2x0ZRgDIAEoDUKHAcJIgwEKgAEKCnVpbnQzMi5sdGUaciFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAnbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEv4GCgJndBgEIAEoDULvBsJI6wYKdQoJdWludDMyLmd0GmghaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwquAQoMdWludDMyLmd0X2x0Gp0BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq2AQoWdWludDMyLmd0X2x0X2V4Y2x1c2l2ZRqbAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr4BCg11aW50MzIuZ3RfbHRlGqwBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrGAQoXdWludDMyLmd0X2x0ZV9leGNsdXNpdmUaqgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEsoHCgNndGUYBSABKA1CugfCSLYHCoMBCgp1aW50MzIuZ3RlGnUhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKvQEKDXVpbnQzMi5ndGVfbHQaqwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKxQEKF3VpbnQzMi5ndGVfbHRfZXhjbHVzaXZlGqkBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrNAQoOdWludDMyLmd0ZV9sdGUaugFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK1QEKGHVpbnQzMi5ndGVfbHRlX2V4Y2x1c2l2ZRq4AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ6CgJpbhgGIAMoDUJuwkhrCmkKCXVpbnQzMi5pbhpcISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAnbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycScQoGbm90X2luGAcgAygNQmHCSF4KXAoNdWludDMyLm5vdF9pbhpLdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAnbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKA1CG8JIGAoWCg51aW50MzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4iwhQKC1VJbnQ2NFJ1bGVzEn4KBWNvbnN0GAEgASgEQm/CSGwKagoMdWludDY0LmNvbnN0Glp0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICdtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycShQEKAmx0GAIgASgEQnfCSHQKcgoJdWludDY0Lmx0GmUhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ211c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEpcBCgNsdGUYAyABKARChwHCSIMBCoABCgp1aW50NjQubHRlGnIhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ211c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABL+BgoCZ3QYBCABKARC7wbCSOsGCnUKCXVpbnQ2NC5ndBpoIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKrgEKDHVpbnQ2NC5ndF9sdBqdAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKtgEKFnVpbnQ2NC5ndF9sdF9leGNsdXNpdmUamwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq+AQoNdWludDY0Lmd0X2x0ZRqsAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKxgEKF3VpbnQ2NC5ndF9sdGVfZXhjbHVzaXZlGqoBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLKBwoDZ3RlGAUgASgEQroHwki2BwqDAQoKdWludDY0Lmd0ZRp1IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCr0BCg11aW50NjQuZ3RlX2x0GqsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCsUBChd1aW50NjQuZ3RlX2x0X2V4Y2x1c2l2ZRqpAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzQEKDnVpbnQ2NC5ndGVfbHRlGroBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtUBChh1aW50NjQuZ3RlX2x0ZV9leGNsdXNpdmUauAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESegoCaW4YBiADKARCbsJIawppCgl1aW50NjQuaW4aXCEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ211c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnEKBm5vdF9pbhgHIAMoBEJhwkheClwKDXVpbnQ2NC5ub3RfaW4aS3RoaXMgaW4gcnVsZXMubm90X2luID8gJ211c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIsCgdleGFtcGxlGAggAygEQhvCSBgKFgoOdWludDY0LmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIsIUCgtTSW50MzJSdWxlcxJ+CgVjb25zdBgBIAEoEUJvwkhsCmoKDHNpbnQzMi5jb25zdBpadGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAnbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEoUBCgJsdBgCIAEoEUJ3wkh0CnIKCXNpbnQzMi5sdBplIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICdtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKXAQoDbHRlGAMgASgRQocBwkiDAQqAAQoKc2ludDMyLmx0ZRpyIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICdtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAAS/gYKAmd0GAQgASgRQu8GwkjrBgp1CglzaW50MzIuZ3QaaCFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCq4BCgxzaW50MzIuZ3RfbHQanQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrYBChZzaW50MzIuZ3RfbHRfZXhjbHVzaXZlGpsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvgEKDXNpbnQzMi5ndF9sdGUarAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCsYBChdzaW50MzIuZ3RfbHRlX2V4Y2x1c2l2ZRqqAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAESygcKA2d0ZRgFIAEoEUK6B8JItgcKgwEKCnNpbnQzMi5ndGUadSFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwq9AQoNc2ludDMyLmd0ZV9sdBqrAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3RlICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrFAQoXc2ludDMyLmd0ZV9sdF9leGNsdXNpdmUaqQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCs0BCg5zaW50MzIuZ3RlX2x0ZRq6AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrVAQoYc2ludDMyLmd0ZV9sdGVfZXhjbHVzaXZlGrgBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJ0gBEnoKAmluGAYgAygRQm7CSGsKaQoJc2ludDMyLmluGlwhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICdtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJxCgZub3RfaW4YByADKBFCYcJIXgpcCg1zaW50MzIubm90X2luGkt0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICdtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSLAoHZXhhbXBsZRgIIAMoEUIbwkgYChYKDnNpbnQzMi5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCCwoJbGVzc190aGFuQg4KDGdyZWF0ZXJfdGhhbiLCFAoLU0ludDY0UnVsZXMSfgoFY29uc3QYASABKBJCb8JIbApqCgxzaW50NjQuY29uc3QaWnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKFAQoCbHQYAiABKBJCd8JIdApyCglzaW50NjQubHQaZSFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID49IHJ1bGVzLmx0PyAnbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAASlwEKA2x0ZRgDIAEoEkKHAcJIgwEKgAEKCnNpbnQ2NC5sdGUaciFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID4gcnVsZXMubHRlPyAnbXVzdCBiZSBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMubHRlXSkgOiAnJ0gAEv4GCgJndBgEIAEoEkLvBsJI6wYKdQoJc2ludDY0Lmd0GmghaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwquAQoMc2ludDY0Lmd0X2x0Gp0BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq2AQoWc2ludDY0Lmd0X2x0X2V4Y2x1c2l2ZRqbAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndCAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCr4BCg1zaW50NjQuZ3RfbHRlGqwBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrGAQoXc2ludDY0Lmd0X2x0ZV9leGNsdXNpdmUaqgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEsoHCgNndGUYBSABKBJCugfCSLYHCoMBCgpzaW50NjQuZ3RlGnUhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKvQEKDXNpbnQ2NC5ndGVfbHQaqwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKxQEKF3NpbnQ2NC5ndGVfbHRfZXhjbHVzaXZlGqkBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrNAQoOc2ludDY0Lmd0ZV9sdGUaugFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK1QEKGHNpbnQ2NC5ndGVfbHRlX2V4Y2x1c2l2ZRq4AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ6CgJpbhgGIAMoEkJuwkhrCmkKCXNpbnQ2NC5pbhpcISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAnbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycScQoGbm90X2luGAcgAygSQmHCSF4KXAoNc2ludDY0Lm5vdF9pbhpLdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAnbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEiwKB2V4YW1wbGUYCCADKBJCG8JIGAoWCg5zaW50NjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4i0xQKDEZpeGVkMzJSdWxlcxJ/CgVjb25zdBgBIAEoB0JwwkhtCmsKDWZpeGVkMzIuY29uc3QaWnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKGAQoCbHQYAiABKAdCeMJIdQpzCgpmaXhlZDMyLmx0GmUhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ211c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEpgBCgNsdGUYAyABKAdCiAHCSIQBCoEBCgtmaXhlZDMyLmx0ZRpyIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICdtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASgwcKAmd0GAQgASgHQvQGwkjwBgp2CgpmaXhlZDMyLmd0GmghaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqvAQoNZml4ZWQzMi5ndF9sdBqdAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKtwEKF2ZpeGVkMzIuZ3RfbHRfZXhjbHVzaXZlGpsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvwEKDmZpeGVkMzIuZ3RfbHRlGqwBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrHAQoYZml4ZWQzMi5ndF9sdGVfZXhjbHVzaXZlGqoBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLPBwoDZ3RlGAUgASgHQr8Hwki7BwqEAQoLZml4ZWQzMi5ndGUadSFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwq+AQoOZml4ZWQzMi5ndGVfbHQaqwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKxgEKGGZpeGVkMzIuZ3RlX2x0X2V4Y2x1c2l2ZRqpAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzgEKD2ZpeGVkMzIuZ3RlX2x0ZRq6AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrWAQoZZml4ZWQzMi5ndGVfbHRlX2V4Y2x1c2l2ZRq4AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ7CgJpbhgGIAMoB0JvwkhsCmoKCmZpeGVkMzIuaW4aXCEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ211c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnIKBm5vdF9pbhgHIAMoB0JiwkhfCl0KDmZpeGVkMzIubm90X2luGkt0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICdtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSLQoHZXhhbXBsZRgIIAMoB0IcwkgZChcKD2ZpeGVkMzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4i0xQKDEZpeGVkNjRSdWxlcxJ/CgVjb25zdBgBIAEoBkJwwkhtCmsKDWZpeGVkNjQuY29uc3QaWnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKGAQoCbHQYAiABKAZCeMJIdQpzCgpmaXhlZDY0Lmx0GmUhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ211c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAEpgBCgNsdGUYAyABKAZCiAHCSIQBCoEBCgtmaXhlZDY0Lmx0ZRpyIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICdtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASgwcKAmd0GAQgASgGQvQGwkjwBgp2CgpmaXhlZDY0Lmd0GmghaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqvAQoNZml4ZWQ2NC5ndF9sdBqdAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKtwEKF2ZpeGVkNjQuZ3RfbHRfZXhjbHVzaXZlGpsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKvwEKDmZpeGVkNjQuZ3RfbHRlGqwBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrHAQoYZml4ZWQ2NC5ndF9sdGVfZXhjbHVzaXZlGqoBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlIDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJydIARLPBwoDZ3RlGAUgASgGQr8Hwki7BwqEAQoLZml4ZWQ2NC5ndGUadSFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwq+AQoOZml4ZWQ2NC5ndGVfbHQaqwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKxgEKGGZpeGVkNjQuZ3RlX2x0X2V4Y2x1c2l2ZRqpAWhhcyhydWxlcy5sdCkgJiYgcnVsZXMubHQgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKzgEKD2ZpeGVkNjQuZ3RlX2x0ZRq6AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrWAQoZZml4ZWQ2NC5ndGVfbHRlX2V4Y2x1c2l2ZRq4AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ7CgJpbhgGIAMoBkJvwkhsCmoKCmZpeGVkNjQuaW4aXCEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ211c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEnIKBm5vdF9pbhgHIAMoBkJiwkhfCl0KDmZpeGVkNjQubm90X2luGkt0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICdtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSLQoHZXhhbXBsZRgIIAMoBkIcwkgZChcKD2ZpeGVkNjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4i5RQKDVNGaXhlZDMyUnVsZXMSgAEKBWNvbnN0GAEgASgPQnHCSG4KbAoOc2ZpeGVkMzIuY29uc3QaWnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKHAQoCbHQYAiABKA9CecJIdgp0CgtzZml4ZWQzMi5sdBplIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICdtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKZAQoDbHRlGAMgASgPQokBwkiFAQqCAQoMc2ZpeGVkMzIubHRlGnIhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ211c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKIBwoCZ3QYBCABKA9C+QbCSPUGCncKC3NmaXhlZDMyLmd0GmghaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqwAQoOc2ZpeGVkMzIuZ3RfbHQanQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrgBChhzZml4ZWQzMi5ndF9sdF9leGNsdXNpdmUamwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrAAQoPc2ZpeGVkMzIuZ3RfbHRlGqwBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrIAQoZc2ZpeGVkMzIuZ3RfbHRlX2V4Y2x1c2l2ZRqqAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES1AcKA2d0ZRgFIAEoD0LEB8JIwAcKhQEKDHNmaXhlZDMyLmd0ZRp1IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCr8BCg9zZml4ZWQzMi5ndGVfbHQaqwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKxwEKGXNmaXhlZDMyLmd0ZV9sdF9leGNsdXNpdmUaqQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCs8BChBzZml4ZWQzMi5ndGVfbHRlGroBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtcBChpzZml4ZWQzMi5ndGVfbHRlX2V4Y2x1c2l2ZRq4AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ8CgJpbhgGIAMoD0JwwkhtCmsKC3NmaXhlZDMyLmluGlwhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICdtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJzCgZub3RfaW4YByADKA9CY8JIYApeCg9zZml4ZWQzMi5ub3RfaW4aS3RoaXMgaW4gcnVsZXMubm90X2luID8gJ211c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIuCgdleGFtcGxlGAggAygPQh3CSBoKGAoQc2ZpeGVkMzIuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4i5RQKDVNGaXhlZDY0UnVsZXMSgAEKBWNvbnN0GAEgASgQQnHCSG4KbAoOc2ZpeGVkNjQuY29uc3QaWnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKHAQoCbHQYAiABKBBCecJIdgp0CgtzZml4ZWQ2NC5sdBplIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPj0gcnVsZXMubHQ/ICdtdXN0IGJlIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5sdF0pIDogJydIABKZAQoDbHRlGAMgASgQQokBwkiFAQqCAQoMc2ZpeGVkNjQubHRlGnIhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+IHJ1bGVzLmx0ZT8gJ211c3QgYmUgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmx0ZV0pIDogJydIABKIBwoCZ3QYBCABKBBC+QbCSPUGCncKC3NmaXhlZDY0Lmd0GmghaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8PSBydWxlcy5ndD8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0XSkgOiAnJwqwAQoOc2ZpeGVkNjQuZ3RfbHQanQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrgBChhzZml4ZWQ2NC5ndF9sdF9leGNsdXNpdmUamwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3QgJiYgKHJ1bGVzLmx0IDw9IHRoaXMgJiYgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBvciBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwrAAQoPc2ZpeGVkNjQuZ3RfbHRlGqwBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJwrIAQoZc2ZpeGVkNjQuZ3RfbHRlX2V4Y2x1c2l2ZRqqAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES1AcKA2d0ZRgFIAEoEELEB8JIwAcKhQEKDHNmaXhlZDY0Lmd0ZRp1IWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPCBydWxlcy5ndGU/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGVdKSA6ICcnCr8BCg9zZml4ZWQ2NC5ndGVfbHQaqwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKxwEKGXNmaXhlZDY0Lmd0ZV9sdF9leGNsdXNpdmUaqQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0IDwgcnVsZXMuZ3RlICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCs8BChBzZml4ZWQ2NC5ndGVfbHRlGroBaGFzKHJ1bGVzLmx0ZSkgJiYgcnVsZXMubHRlID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnCtcBChpzZml4ZWQ2NC5ndGVfbHRlX2V4Y2x1c2l2ZRq4AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJ8CgJpbhgGIAMoEEJwwkhtCmsKC3NmaXhlZDY0LmluGlwhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICdtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJzCgZub3RfaW4YByADKBBCY8JIYApeCg9zZml4ZWQ2NC5ub3RfaW4aS3RoaXMgaW4gcnVsZXMubm90X2luID8gJ211c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxIuCgdleGFtcGxlGAggAygQQh3CSBoKGAoQc2ZpeGVkNjQuZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4iwAEKCUJvb2xSdWxlcxJ8CgVjb25zdBgBIAEoCEJtwkhqCmgKCmJvb2wuY29uc3QaWnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxIqCgdleGFtcGxlGAIgAygIQhnCSBYKFAoMYm9vbC5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAIi6jwKC1N0cmluZ1J1bGVzEoABCgVjb25zdBgBIAEoCUJxwkhuCmwKDHN0cmluZy5jb25zdBpcdGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAnbXVzdCBlcXVhbCBgJXNgJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycScQoDbGVuGBMgASgEQmTCSGEKXwoKc3RyaW5nLmxlbhpRdWludCh0aGlzLnNpemUoKSkgIT0gcnVsZXMubGVuID8gJ211c3QgYmUgJXMgY2hhcmFjdGVycycuZm9ybWF0KFtydWxlcy5sZW5dKSA6ICcnEokBCgdtaW5fbGVuGAIgASgEQnjCSHUKcwoOc3RyaW5nLm1pbl9sZW4aYXVpbnQodGhpcy5zaXplKCkpIDwgcnVsZXMubWluX2xlbiA/ICdtdXN0IGJlIGF0IGxlYXN0ICVzIGNoYXJhY3RlcnMnLmZvcm1hdChbcnVsZXMubWluX2xlbl0pIDogJycSiAEKB21heF9sZW4YAyABKARCd8JIdApyCg5zdHJpbmcubWF4X2xlbhpgdWludCh0aGlzLnNpemUoKSkgPiBydWxlcy5tYXhfbGVuID8gJ211c3QgYmUgYXQgbW9zdCAlcyBjaGFyYWN0ZXJzJy5mb3JtYXQoW3J1bGVzLm1heF9sZW5dKSA6ICcnEosBCglsZW5fYnl0ZXMYFCABKARCeMJIdQpzChBzdHJpbmcubGVuX2J5dGVzGl91aW50KGJ5dGVzKHRoaXMpLnNpemUoKSkgIT0gcnVsZXMubGVuX2J5dGVzID8gJ211c3QgYmUgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubGVuX2J5dGVzXSkgOiAnJxKUAQoJbWluX2J5dGVzGAQgASgEQoABwkh9CnsKEHN0cmluZy5taW5fYnl0ZXMaZ3VpbnQoYnl0ZXModGhpcykuc2l6ZSgpKSA8IHJ1bGVzLm1pbl9ieXRlcyA/ICdtdXN0IGJlIGF0IGxlYXN0ICVzIGJ5dGVzJy5mb3JtYXQoW3J1bGVzLm1pbl9ieXRlc10pIDogJycSkgEKCW1heF9ieXRlcxgFIAEoBEJ/wkh8CnoKEHN0cmluZy5tYXhfYnl0ZXMaZnVpbnQoYnl0ZXModGhpcykuc2l6ZSgpKSA+IHJ1bGVzLm1heF9ieXRlcyA/ICdtdXN0IGJlIGF0IG1vc3QgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubWF4X2J5dGVzXSkgOiAnJxKHAQoHcGF0dGVybhgGIAEoCUJ2wkhzCnEKDnN0cmluZy5wYXR0ZXJuGl8hdGhpcy5tYXRjaGVzKHJ1bGVzLnBhdHRlcm4pID8gJ2RvZXMgbm90IG1hdGNoIHJlZ2V4IHBhdHRlcm4gYCVzYCcuZm9ybWF0KFtydWxlcy5wYXR0ZXJuXSkgOiAnJxJ+CgZwcmVmaXgYByABKAlCbsJIawppCg1zdHJpbmcucHJlZml4GlghdGhpcy5zdGFydHNXaXRoKHJ1bGVzLnByZWZpeCkgPyAnZG9lcyBub3QgaGF2ZSBwcmVmaXggYCVzYCcuZm9ybWF0KFtydWxlcy5wcmVmaXhdKSA6ICcnEnwKBnN1ZmZpeBgIIAEoCUJswkhpCmcKDXN0cmluZy5zdWZmaXgaViF0aGlzLmVuZHNXaXRoKHJ1bGVzLnN1ZmZpeCkgPyAnZG9lcyBub3QgaGF2ZSBzdWZmaXggYCVzYCcuZm9ybWF0KFtydWxlcy5zdWZmaXhdKSA6ICcnEooBCghjb250YWlucxgJIAEoCUJ4wkh1CnMKD3N0cmluZy5jb250YWlucxpgIXRoaXMuY29udGFpbnMocnVsZXMuY29udGFpbnMpID8gJ2RvZXMgbm90IGNvbnRhaW4gc3Vic3RyaW5nIGAlc2AnLmZvcm1hdChbcnVsZXMuY29udGFpbnNdKSA6ICcnEpEBCgxub3RfY29udGFpbnMYFyABKAlCe8JIeAp2ChNzdHJpbmcubm90X2NvbnRhaW5zGl90aGlzLmNvbnRhaW5zKHJ1bGVzLm5vdF9jb250YWlucykgPyAnY29udGFpbnMgc3Vic3RyaW5nIGAlc2AnLmZvcm1hdChbcnVsZXMubm90X2NvbnRhaW5zXSkgOiAnJxJ6CgJpbhgKIAMoCUJuwkhrCmkKCXN0cmluZy5pbhpcISh0aGlzIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSkgPyAnbXVzdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnaW4nKV0pIDogJycScQoGbm90X2luGAsgAygJQmHCSF4KXAoNc3RyaW5nLm5vdF9pbhpLdGhpcyBpbiBydWxlcy5ub3RfaW4gPyAnbXVzdCBub3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtydWxlcy5ub3RfaW5dKSA6ICcnEtkBCgVlbWFpbBgMIAEoCELHAcJIwwEKWwoMc3RyaW5nLmVtYWlsEh1tdXN0IGJlIGEgdmFsaWQgZW1haWwgYWRkcmVzcxosIXJ1bGVzLmVtYWlsIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0VtYWlsKCkKZAoSc3RyaW5nLmVtYWlsX2VtcHR5EjJ2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgZW1haWwgYWRkcmVzcxoaIXJ1bGVzLmVtYWlsIHx8IHRoaXMgIT0gJydIABLhAQoIaG9zdG5hbWUYDSABKAhCzAHCSMgBCl8KD3N0cmluZy5ob3N0bmFtZRIYbXVzdCBiZSBhIHZhbGlkIGhvc3RuYW1lGjIhcnVsZXMuaG9zdG5hbWUgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSG9zdG5hbWUoKQplChVzdHJpbmcuaG9zdG5hbWVfZW1wdHkSLXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBob3N0bmFtZRodIXJ1bGVzLmhvc3RuYW1lIHx8IHRoaXMgIT0gJydIABLBAQoCaXAYDiABKAhCsgHCSK4BCk8KCXN0cmluZy5pcBIabXVzdCBiZSBhIHZhbGlkIElQIGFkZHJlc3MaJiFydWxlcy5pcCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcCgpClsKD3N0cmluZy5pcF9lbXB0eRIvdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIGFkZHJlc3MaFyFydWxlcy5pcCB8fCB0aGlzICE9ICcnSAAS0AEKBGlwdjQYDyABKAhCvwHCSLsBClYKC3N0cmluZy5pcHY0EhxtdXN0IGJlIGEgdmFsaWQgSVB2NCBhZGRyZXNzGikhcnVsZXMuaXB2NCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcCg0KQphChFzdHJpbmcuaXB2NF9lbXB0eRIxdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjQgYWRkcmVzcxoZIXJ1bGVzLmlwdjQgfHwgdGhpcyAhPSAnJ0gAEtABCgRpcHY2GBAgASgIQr8Bwki7AQpWCgtzdHJpbmcuaXB2NhIcbXVzdCBiZSBhIHZhbGlkIElQdjYgYWRkcmVzcxopIXJ1bGVzLmlwdjYgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXAoNikKYQoRc3RyaW5nLmlwdjZfZW1wdHkSMXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY2IGFkZHJlc3MaGSFydWxlcy5pcHY2IHx8IHRoaXMgIT0gJydIABK5AQoDdXJpGBEgASgIQqkBwkilAQpLCgpzdHJpbmcudXJpEhNtdXN0IGJlIGEgdmFsaWQgVVJJGighcnVsZXMudXJpIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc1VyaSgpClYKEHN0cmluZy51cmlfZW1wdHkSKHZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBVUkkaGCFydWxlcy51cmkgfHwgdGhpcyAhPSAnJ0gAEmoKB3VyaV9yZWYYEiABKAhCV8JIVApSCg5zdHJpbmcudXJpX3JlZhIdbXVzdCBiZSBhIHZhbGlkIFVSSSBSZWZlcmVuY2UaISFydWxlcy51cmlfcmVmIHx8IHRoaXMuaXNVcmlSZWYoKUgAEokCCgdhZGRyZXNzGBUgASgIQvUBwkjxAQp7Cg5zdHJpbmcuYWRkcmVzcxInbXVzdCBiZSBhIHZhbGlkIGhvc3RuYW1lLCBvciBpcCBhZGRyZXNzGkAhcnVsZXMuYWRkcmVzcyB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNIb3N0bmFtZSgpIHx8IHRoaXMuaXNJcCgpCnIKFHN0cmluZy5hZGRyZXNzX2VtcHR5Ejx2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgaG9zdG5hbWUsIG9yIGlwIGFkZHJlc3MaHCFydWxlcy5hZGRyZXNzIHx8IHRoaXMgIT0gJydIABKSAgoEdXVpZBgWIAEoCEKBAsJI/QEKnwEKC3N0cmluZy51dWlkEhRtdXN0IGJlIGEgdmFsaWQgVVVJRBp6IXJ1bGVzLnV1aWQgfHwgdGhpcyA9PSAnJyB8fCB0aGlzLm1hdGNoZXMoJ15bMC05YS1mQS1GXXs4fS1bMC05YS1mQS1GXXs0fS1bMC05YS1mQS1GXXs0fS1bMC05YS1mQS1GXXs0fS1bMC05YS1mQS1GXXsxMn0kJykKWQoRc3RyaW5nLnV1aWRfZW1wdHkSKXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBVVUlEGhkhcnVsZXMudXVpZCB8fCB0aGlzICE9ICcnSAAS6gEKBXR1dWlkGCEgASgIQtgBwkjUAQptCgxzdHJpbmcudHV1aWQSHG11c3QgYmUgYSB2YWxpZCB0cmltbWVkIFVVSUQaPyFydWxlcy50dXVpZCB8fCB0aGlzID09ICcnIHx8IHRoaXMubWF0Y2hlcygnXlswLTlhLWZBLUZdezMyfSQnKQpjChJzdHJpbmcudHV1aWRfZW1wdHkSMXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCB0cmltbWVkIFVVSUQaGiFydWxlcy50dXVpZCB8fCB0aGlzICE9ICcnSAASkAIKEWlwX3dpdGhfcHJlZml4bGVuGBogASgIQvIBwkjuAQpyChhzdHJpbmcuaXBfd2l0aF9wcmVmaXhsZW4SGW11c3QgYmUgYSB2YWxpZCBJUCBwcmVmaXgaOyFydWxlcy5pcF93aXRoX3ByZWZpeGxlbiB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcFByZWZpeCgpCngKHnN0cmluZy5pcF93aXRoX3ByZWZpeGxlbl9lbXB0eRIudmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIHByZWZpeBomIXJ1bGVzLmlwX3dpdGhfcHJlZml4bGVuIHx8IHRoaXMgIT0gJydIABLJAgoTaXB2NF93aXRoX3ByZWZpeGxlbhgbIAEoCEKpAsJIpQIKjQEKGnN0cmluZy5pcHY0X3dpdGhfcHJlZml4bGVuEi9tdXN0IGJlIGEgdmFsaWQgSVB2NCBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBo+IXJ1bGVzLmlwdjRfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoNCkKkgEKIHN0cmluZy5pcHY0X3dpdGhfcHJlZml4bGVuX2VtcHR5EkR2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NCBhZGRyZXNzIHdpdGggcHJlZml4IGxlbmd0aBooIXJ1bGVzLmlwdjRfd2l0aF9wcmVmaXhsZW4gfHwgdGhpcyAhPSAnJ0gAEskCChNpcHY2X3dpdGhfcHJlZml4bGVuGBwgASgIQqkCwkilAgqNAQoac3RyaW5nLmlwdjZfd2l0aF9wcmVmaXhsZW4SL211c3QgYmUgYSB2YWxpZCBJUHY2IGFkZHJlc3Mgd2l0aCBwcmVmaXggbGVuZ3RoGj4hcnVsZXMuaXB2Nl93aXRoX3ByZWZpeGxlbiB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcFByZWZpeCg2KQqSAQogc3RyaW5nLmlwdjZfd2l0aF9wcmVmaXhsZW5fZW1wdHkSRHZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBJUHY2IGFkZHJlc3Mgd2l0aCBwcmVmaXggbGVuZ3RoGighcnVsZXMuaXB2Nl93aXRoX3ByZWZpeGxlbiB8fCB0aGlzICE9ICcnSAAS7AEKCWlwX3ByZWZpeBgdIAEoCELWAcJI0gEKZgoQc3RyaW5nLmlwX3ByZWZpeBIZbXVzdCBiZSBhIHZhbGlkIElQIHByZWZpeBo3IXJ1bGVzLmlwX3ByZWZpeCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNJcFByZWZpeCh0cnVlKQpoChZzdHJpbmcuaXBfcHJlZml4X2VtcHR5Ei52YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVAgcHJlZml4Gh4hcnVsZXMuaXBfcHJlZml4IHx8IHRoaXMgIT0gJydIABL9AQoLaXB2NF9wcmVmaXgYHiABKAhC5QHCSOEBCm8KEnN0cmluZy5pcHY0X3ByZWZpeBIbbXVzdCBiZSBhIHZhbGlkIElQdjQgcHJlZml4GjwhcnVsZXMuaXB2NF9wcmVmaXggfHwgdGhpcyA9PSAnJyB8fCB0aGlzLmlzSXBQcmVmaXgoNCwgdHJ1ZSkKbgoYc3RyaW5nLmlwdjRfcHJlZml4X2VtcHR5EjB2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NCBwcmVmaXgaICFydWxlcy5pcHY0X3ByZWZpeCB8fCB0aGlzICE9ICcnSAAS/QEKC2lwdjZfcHJlZml4GB8gASgIQuUBwkjhAQpvChJzdHJpbmcuaXB2Nl9wcmVmaXgSG211c3QgYmUgYSB2YWxpZCBJUHY2IHByZWZpeBo8IXJ1bGVzLmlwdjZfcHJlZml4IHx8IHRoaXMgPT0gJycgfHwgdGhpcy5pc0lwUHJlZml4KDYsIHRydWUpCm4KGHN0cmluZy5pcHY2X3ByZWZpeF9lbXB0eRIwdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjYgcHJlZml4GiAhcnVsZXMuaXB2Nl9wcmVmaXggfHwgdGhpcyAhPSAnJ0gAEq8CCg1ob3N0X2FuZF9wb3J0GCAgASgIQpUCwkiRAgqTAQoUc3RyaW5nLmhvc3RfYW5kX3BvcnQSO211c3QgYmUgYSB2YWxpZCBob3N0IChob3N0bmFtZSBvciBJUCBhZGRyZXNzKSBhbmQgcG9ydCBwYWlyGj4hcnVsZXMuaG9zdF9hbmRfcG9ydCB8fCB0aGlzID09ICcnIHx8IHRoaXMuaXNIb3N0QW5kUG9ydCh0cnVlKQp5ChpzdHJpbmcuaG9zdF9hbmRfcG9ydF9lbXB0eRI3dmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIGhvc3QgYW5kIHBvcnQgcGFpchoiIXJ1bGVzLmhvc3RfYW5kX3BvcnQgfHwgdGhpcyAhPSAnJ0gAEu4BCgR1bGlkGCMgASgIQt0BwkjZAQp8CgtzdHJpbmcudWxpZBIUbXVzdCBiZSBhIHZhbGlkIFVMSUQaVyFydWxlcy51bGlkIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5tYXRjaGVzKCdeWzAtN11bMC05QS1ISktNTlAtVFYtWmEtaGprbW5wLXR2LXpdezI1fSQnKQpZChFzdHJpbmcudWxpZF9lbXB0eRIpdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIFVMSUQaGSFydWxlcy51bGlkIHx8IHRoaXMgIT0gJydIABLUAgoMcHJvdG9idWZfZnFuGCUgASgIQrsCwki3AgqvAQoTc3RyaW5nLnByb3RvYnVmX2ZxbhItbXVzdCBiZSBhIHZhbGlkIGZ1bGx5LXF1YWxpZmllZCBQcm90b2J1ZiBuYW1lGmkhcnVsZXMucHJvdG9idWZfZnFuIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5tYXRjaGVzKCdeW0EtWmEtel9dW0EtWmEtel8wLTldKihcXC5bQS1aYS16X11bQS1aYS16XzAtOV0qKSokJykKggEKGXN0cmluZy5wcm90b2J1Zl9mcW5fZW1wdHkSQnZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBmdWxseS1xdWFsaWZpZWQgUHJvdG9idWYgbmFtZRohIXJ1bGVzLnByb3RvYnVmX2ZxbiB8fCB0aGlzICE9ICcnSAASkQMKEHByb3RvYnVmX2RvdF9mcW4YJiABKAhC9ALCSPACCs0BChdzdHJpbmcucHJvdG9idWZfZG90X2ZxbhJAbXVzdCBiZSBhIHZhbGlkIGZ1bGx5LXF1YWxpZmllZCBQcm90b2J1ZiBuYW1lIHdpdGggYSBsZWFkaW5nIGRvdBpwIXJ1bGVzLnByb3RvYnVmX2RvdF9mcW4gfHwgdGhpcyA9PSAnJyB8fCB0aGlzLm1hdGNoZXMoJ15cXC5bQS1aYS16X11bQS1aYS16XzAtOV0qKFxcLltBLVphLXpfXVtBLVphLXpfMC05XSopKiQnKQqdAQodc3RyaW5nLnByb3RvYnVmX2RvdF9mcW5fZW1wdHkSVXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBmdWxseS1xdWFsaWZpZWQgUHJvdG9idWYgbmFtZSB3aXRoIGEgbGVhZGluZyBkb3QaJSFydWxlcy5wcm90b2J1Zl9kb3RfZnFuIHx8IHRoaXMgIT0gJydIABKcBQoQd2VsbF9rbm93bl9yZWdleBgYIAEoDjIYLmJ1Zi52YWxpZGF0ZS5Lbm93blJlZ2V4QuUEwkjhBArqAQojc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX25hbWUSIG11c3QgYmUgYSB2YWxpZCBIVFRQIGhlYWRlciBuYW1lGqABcnVsZXMud2VsbF9rbm93bl9yZWdleCAhPSAxIHx8IHRoaXMgPT0gJycgfHwgdGhpcy5tYXRjaGVzKCFoYXMocnVsZXMuc3RyaWN0KSB8fCBydWxlcy5zdHJpY3QgPydeOj9bMC05YS16QS1aISMkJSZcJyorLS5eX3x+XHg2MF0rJCcgOideW15cdTAwMDBcdTAwMEFcdTAwMERdKyQnKQqNAQopc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX25hbWVfZW1wdHkSNXZhbHVlIGlzIGVtcHR5LCB3aGljaCBpcyBub3QgYSB2YWxpZCBIVFRQIGhlYWRlciBuYW1lGilydWxlcy53ZWxsX2tub3duX3JlZ2V4ICE9IDEgfHwgdGhpcyAhPSAnJwrhAQokc3RyaW5nLndlbGxfa25vd25fcmVnZXguaGVhZGVyX3ZhbHVlEiFtdXN0IGJlIGEgdmFsaWQgSFRUUCBoZWFkZXIgdmFsdWUalQFydWxlcy53ZWxsX2tub3duX3JlZ2V4ICE9IDIgfHwgdGhpcy5tYXRjaGVzKCFoYXMocnVsZXMuc3RyaWN0KSB8fCBydWxlcy5zdHJpY3QgPydeW15cdTAwMDAtXHUwMDA4XHUwMDBBLVx1MDAxRlx1MDA3Rl0qJCcgOideW15cdTAwMDBcdTAwMEFcdTAwMERdKiQnKUgAEg4KBnN0cmljdBgZIAEoCBIsCgdleGFtcGxlGCIgAygJQhvCSBgKFgoOc3RyaW5nLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkIMCgp3ZWxsX2tub3duIt0RCgpCeXRlc1J1bGVzEnoKBWNvbnN0GAEgASgMQmvCSGgKZgoLYnl0ZXMuY29uc3QaV3RoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgYmUgJXgnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxJrCgNsZW4YDSABKARCXsJIWwpZCglieXRlcy5sZW4aTHVpbnQodGhpcy5zaXplKCkpICE9IHJ1bGVzLmxlbiA/ICdtdXN0IGJlICVzIGJ5dGVzJy5mb3JtYXQoW3J1bGVzLmxlbl0pIDogJycSgwEKB21pbl9sZW4YAiABKARCcsJIbwptCg1ieXRlcy5taW5fbGVuGlx1aW50KHRoaXMuc2l6ZSgpKSA8IHJ1bGVzLm1pbl9sZW4gPyAnbXVzdCBiZSBhdCBsZWFzdCAlcyBieXRlcycuZm9ybWF0KFtydWxlcy5taW5fbGVuXSkgOiAnJxKCAQoHbWF4X2xlbhgDIAEoBEJxwkhuCmwKDWJ5dGVzLm1heF9sZW4aW3VpbnQodGhpcy5zaXplKCkpID4gcnVsZXMubWF4X2xlbiA/ICdtdXN0IGJlIGF0IG1vc3QgJXMgYnl0ZXMnLmZvcm1hdChbcnVsZXMubWF4X2xlbl0pIDogJycSigEKB3BhdHRlcm4YBCABKAlCecJIdgp0Cg1ieXRlcy5wYXR0ZXJuGmMhc3RyaW5nKHRoaXMpLm1hdGNoZXMocnVsZXMucGF0dGVybikgPyAnbXVzdCBtYXRjaCByZWdleCBwYXR0ZXJuIGAlc2AnLmZvcm1hdChbcnVsZXMucGF0dGVybl0pIDogJycSewoGcHJlZml4GAUgASgMQmvCSGgKZgoMYnl0ZXMucHJlZml4GlYhdGhpcy5zdGFydHNXaXRoKHJ1bGVzLnByZWZpeCkgPyAnZG9lcyBub3QgaGF2ZSBwcmVmaXggJXgnLmZvcm1hdChbcnVsZXMucHJlZml4XSkgOiAnJxJ5CgZzdWZmaXgYBiABKAxCacJIZgpkCgxieXRlcy5zdWZmaXgaVCF0aGlzLmVuZHNXaXRoKHJ1bGVzLnN1ZmZpeCkgPyAnZG9lcyBub3QgaGF2ZSBzdWZmaXggJXgnLmZvcm1hdChbcnVsZXMuc3VmZml4XSkgOiAnJxJ9Cghjb250YWlucxgHIAEoDEJrwkhoCmYKDmJ5dGVzLmNvbnRhaW5zGlQhdGhpcy5jb250YWlucyhydWxlcy5jb250YWlucykgPyAnZG9lcyBub3QgY29udGFpbiAleCcuZm9ybWF0KFtydWxlcy5jb250YWluc10pIDogJycSoQEKAmluGAggAygMQpQBwkiQAQqNAQoIYnl0ZXMuaW4agAFnZXRGaWVsZChydWxlcywgJ2luJykuc2l6ZSgpID4gMCAmJiAhKHRoaXMgaW4gZ2V0RmllbGQocnVsZXMsICdpbicpKSA/ICdtdXN0IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdpbicpXSkgOiAnJxJwCgZub3RfaW4YCSADKAxCYMJIXQpbCgxieXRlcy5ub3RfaW4aS3RoaXMgaW4gcnVsZXMubm90X2luID8gJ211c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxLlAQoCaXAYCiABKAhC1gHCSNIBCm4KCGJ5dGVzLmlwEhptdXN0IGJlIGEgdmFsaWQgSVAgYWRkcmVzcxpGIXJ1bGVzLmlwIHx8IHRoaXMuc2l6ZSgpID09IDAgfHwgdGhpcy5zaXplKCkgPT0gNCB8fCB0aGlzLnNpemUoKSA9PSAxNgpgCg5ieXRlcy5pcF9lbXB0eRIvdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQIGFkZHJlc3MaHSFydWxlcy5pcCB8fCB0aGlzLnNpemUoKSAhPSAwSAAS3gEKBGlwdjQYCyABKAhCzQHCSMkBCl8KCmJ5dGVzLmlwdjQSHG11c3QgYmUgYSB2YWxpZCBJUHY0IGFkZHJlc3MaMyFydWxlcy5pcHY0IHx8IHRoaXMuc2l6ZSgpID09IDAgfHwgdGhpcy5zaXplKCkgPT0gNApmChBieXRlcy5pcHY0X2VtcHR5EjF2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgSVB2NCBhZGRyZXNzGh8hcnVsZXMuaXB2NCB8fCB0aGlzLnNpemUoKSAhPSAwSAAS3wEKBGlwdjYYDCABKAhCzgHCSMoBCmAKCmJ5dGVzLmlwdjYSHG11c3QgYmUgYSB2YWxpZCBJUHY2IGFkZHJlc3MaNCFydWxlcy5pcHY2IHx8IHRoaXMuc2l6ZSgpID09IDAgfHwgdGhpcy5zaXplKCkgPT0gMTYKZgoQYnl0ZXMuaXB2Nl9lbXB0eRIxdmFsdWUgaXMgZW1wdHksIHdoaWNoIGlzIG5vdCBhIHZhbGlkIElQdjYgYWRkcmVzcxofIXJ1bGVzLmlwdjYgfHwgdGhpcy5zaXplKCkgIT0gMEgAEs8BCgR1dWlkGA8gASgIQr4Bwki6AQpYCgpieXRlcy51dWlkEhRtdXN0IGJlIGEgdmFsaWQgVVVJRBo0IXJ1bGVzLnV1aWQgfHwgdGhpcy5zaXplKCkgPT0gMCB8fCB0aGlzLnNpemUoKSA9PSAxNgpeChBieXRlcy51dWlkX2VtcHR5Eil2YWx1ZSBpcyBlbXB0eSwgd2hpY2ggaXMgbm90IGEgdmFsaWQgVVVJRBofIXJ1bGVzLnV1aWQgfHwgdGhpcy5zaXplKCkgIT0gMEgAEisKB2V4YW1wbGUYDiADKAxCGsJIFwoVCg1ieXRlcy5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAJCDAoKd2VsbF9rbm93biLBAwoJRW51bVJ1bGVzEnwKBWNvbnN0GAEgASgFQm3CSGoKaAoKZW51bS5jb25zdBpadGhpcyAhPSBnZXRGaWVsZChydWxlcywgJ2NvbnN0JykgPyAnbXVzdCBlcXVhbCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2NvbnN0JyldKSA6ICcnEhQKDGRlZmluZWRfb25seRgCIAEoCBJ4CgJpbhgDIAMoBUJswkhpCmcKB2VudW0uaW4aXCEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ211c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEm8KBm5vdF9pbhgEIAMoBUJfwkhcCloKC2VudW0ubm90X2luGkt0aGlzIGluIHJ1bGVzLm5vdF9pbiA/ICdtdXN0IG5vdCBiZSBpbiBsaXN0ICVzJy5mb3JtYXQoW3J1bGVzLm5vdF9pbl0pIDogJycSKgoHZXhhbXBsZRgFIAMoBUIZwkgWChQKDGVudW0uZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACIu0DCg1SZXBlYXRlZFJ1bGVzEpYBCgltaW5faXRlbXMYASABKARCggHCSH8KfQoScmVwZWF0ZWQubWluX2l0ZW1zGmd1aW50KHRoaXMuc2l6ZSgpKSA8IHJ1bGVzLm1pbl9pdGVtcyA/ICdtdXN0IGNvbnRhaW4gYXQgbGVhc3QgJWQgaXRlbShzKScuZm9ybWF0KFtydWxlcy5taW5faXRlbXNdKSA6ICcnEpwBCgltYXhfaXRlbXMYAiABKARCiAHCSIQBCoEBChJyZXBlYXRlZC5tYXhfaXRlbXMaa3VpbnQodGhpcy5zaXplKCkpID4gcnVsZXMubWF4X2l0ZW1zID8gJ211c3QgY29udGFpbiBubyBtb3JlIHRoYW4gJXMgaXRlbShzKScuZm9ybWF0KFtydWxlcy5tYXhfaXRlbXNdKSA6ICcnEnAKBnVuaXF1ZRgDIAEoCEJgwkhdClsKD3JlcGVhdGVkLnVuaXF1ZRIocmVwZWF0ZWQgdmFsdWUgbXVzdCBjb250YWluIHVuaXF1ZSBpdGVtcxoeIXJ1bGVzLnVuaXF1ZSB8fCB0aGlzLnVuaXF1ZSgpEicKBWl0ZW1zGAQgASgLMhguYnVmLnZhbGlkYXRlLkZpZWxkUnVsZXMqCQjoBxCAgICAAiKKAwoITWFwUnVsZXMSjwEKCW1pbl9wYWlycxgBIAEoBEJ8wkh5CncKDW1hcC5taW5fcGFpcnMaZnVpbnQodGhpcy5zaXplKCkpIDwgcnVsZXMubWluX3BhaXJzID8gJ21hcCBtdXN0IGJlIGF0IGxlYXN0ICVkIGVudHJpZXMnLmZvcm1hdChbcnVsZXMubWluX3BhaXJzXSkgOiAnJxKOAQoJbWF4X3BhaXJzGAIgASgEQnvCSHgKdgoNbWFwLm1heF9wYWlycxpldWludCh0aGlzLnNpemUoKSkgPiBydWxlcy5tYXhfcGFpcnMgPyAnbWFwIG11c3QgYmUgYXQgbW9zdCAlZCBlbnRyaWVzJy5mb3JtYXQoW3J1bGVzLm1heF9wYWlyc10pIDogJycSJgoEa2V5cxgEIAEoCzIYLmJ1Zi52YWxpZGF0ZS5GaWVsZFJ1bGVzEigKBnZhbHVlcxgFIAEoCzIYLmJ1Zi52YWxpZGF0ZS5GaWVsZFJ1bGVzKgkI6AcQgICAgAIiJgoIQW55UnVsZXMSCgoCaW4YAiADKAkSDgoGbm90X2luGAMgAygJIr8WCg1EdXJhdGlvblJ1bGVzEpsBCgVjb25zdBgCIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkJxwkhuCmwKDmR1cmF0aW9uLmNvbnN0Glp0aGlzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKSA/ICdtdXN0IGVxdWFsICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKV0pIDogJycSogEKAmx0GAMgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQnnCSHYKdAoLZHVyYXRpb24ubHQaZSFoYXMocnVsZXMuZ3RlKSAmJiAhaGFzKHJ1bGVzLmd0KSAmJiB0aGlzID49IHJ1bGVzLmx0PyAnbXVzdCBiZSBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMubHRdKSA6ICcnSAAStAEKA2x0ZRgEIAEoCzIZLmdvb2dsZS5wcm90b2J1Zi5EdXJhdGlvbkKJAcJIhQEKggEKDGR1cmF0aW9uLmx0ZRpyIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICdtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASowcKAmd0GAUgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQvkGwkj1Bgp3CgtkdXJhdGlvbi5ndBpoIWhhcyhydWxlcy5sdCkgJiYgIWhhcyhydWxlcy5sdGUpICYmIHRoaXMgPD0gcnVsZXMuZ3Q/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndF0pIDogJycKsAEKDmR1cmF0aW9uLmd0X2x0Gp0BaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndCAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0XSkgOiAnJwq4AQoYZHVyYXRpb24uZ3RfbHRfZXhjbHVzaXZlGpsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKwAEKD2R1cmF0aW9uLmd0X2x0ZRqsAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndCAmJiAodGhpcyA+IHJ1bGVzLmx0ZSB8fCB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIGFuZCBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3QsIHJ1bGVzLmx0ZV0pIDogJycKyAEKGWR1cmF0aW9uLmd0X2x0ZV9leGNsdXNpdmUaqgFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndCAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDw9IHJ1bGVzLmd0KT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRlXSkgOiAnJ0gBEu8HCgNndGUYBiABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CxAfCSMAHCoUBCgxkdXJhdGlvbi5ndGUadSFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDwgcnVsZXMuZ3RlPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlXSkgOiAnJwq/AQoPZHVyYXRpb24uZ3RlX2x0GqsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPj0gcnVsZXMubHQgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRdKSA6ICcnCscBChlkdXJhdGlvbi5ndGVfbHRfZXhjbHVzaXZlGqkBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrPAQoQZHVyYXRpb24uZ3RlX2x0ZRq6AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA+PSBydWxlcy5ndGUgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZSwgcnVsZXMubHRlXSkgOiAnJwrXAQoaZHVyYXRpb24uZ3RlX2x0ZV9leGNsdXNpdmUauAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPCBydWxlcy5ndGUgJiYgKHJ1bGVzLmx0ZSA8IHRoaXMgJiYgdGhpcyA8IHJ1bGVzLmd0ZSk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiBvciBlcXVhbCB0byAlcyBvciBsZXNzIHRoYW4gb3IgZXF1YWwgdG8gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdGVdKSA6ICcnSAESlwEKAmluGAcgAygLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQnDCSG0KawoLZHVyYXRpb24uaW4aXCEodGhpcyBpbiBnZXRGaWVsZChydWxlcywgJ2luJykpID8gJ211c3QgYmUgaW4gbGlzdCAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEo4BCgZub3RfaW4YCCADKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25CY8JIYApeCg9kdXJhdGlvbi5ub3RfaW4aS3RoaXMgaW4gcnVsZXMubm90X2luID8gJ211c3Qgbm90IGJlIGluIGxpc3QgJXMnLmZvcm1hdChbcnVsZXMubm90X2luXSkgOiAnJxJJCgdleGFtcGxlGAkgAygLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uQh3CSBoKGAoQZHVyYXRpb24uZXhhbXBsZRoEdHJ1ZSoJCOgHEICAgIACQgsKCWxlc3NfdGhhbkIOCgxncmVhdGVyX3RoYW4i6wUKDkZpZWxkTWFza1J1bGVzErkBCgVjb25zdBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCjQHCSIkBCoYBChBmaWVsZF9tYXNrLmNvbnN0GnJ0aGlzLnBhdGhzICE9IGdldEZpZWxkKHJ1bGVzLCAnY29uc3QnKS5wYXRocyA/ICdtdXN0IGVxdWFsIHBhdGhzICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnY29uc3QnKS5wYXRoc10pIDogJycS0wEKAmluGAIgAygJQsYBwkjCAQq/AQoNZmllbGRfbWFzay5pbhqtASF0aGlzLnBhdGhzLmFsbChwLCBwIGluIGdldEZpZWxkKHJ1bGVzLCAnaW4nKSB8fCBnZXRGaWVsZChydWxlcywgJ2luJykuZXhpc3RzKGYsIHAuc3RhcnRzV2l0aChmKycuJykpKSA/ICdtdXN0IG9ubHkgY29udGFpbiBwYXRocyBpbiAlcycuZm9ybWF0KFtnZXRGaWVsZChydWxlcywgJ2luJyldKSA6ICcnEu0BCgZub3RfaW4YAyADKAlC3AHCSNgBCtUBChFmaWVsZF9tYXNrLm5vdF9pbhq/ASF0aGlzLnBhdGhzLmFsbChwLCAhKHAgaW4gZ2V0RmllbGQocnVsZXMsICdub3RfaW4nKSB8fCBnZXRGaWVsZChydWxlcywgJ25vdF9pbicpLmV4aXN0cyhmLCBwLnN0YXJ0c1dpdGgoZisnLicpKSkpID8gJ211c3Qgbm90IGNvbnRhaW4gYW55IHBhdGhzIGluICVzJy5mb3JtYXQoW2dldEZpZWxkKHJ1bGVzLCAnbm90X2luJyldKSA6ICcnEkwKB2V4YW1wbGUYBCADKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQh/CSBwKGgoSZmllbGRfbWFzay5leGFtcGxlGgR0cnVlKgkI6AcQgICAgAIisBcKDlRpbWVzdGFtcFJ1bGVzEp0BCgVjb25zdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCcsJIbwptCg90aW1lc3RhbXAuY29uc3QaWnRoaXMgIT0gZ2V0RmllbGQocnVsZXMsICdjb25zdCcpID8gJ211c3QgZXF1YWwgJXMnLmZvcm1hdChbZ2V0RmllbGQocnVsZXMsICdjb25zdCcpXSkgOiAnJxKkAQoCbHQYAyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQnrCSHcKdQoMdGltZXN0YW1wLmx0GmUhaGFzKHJ1bGVzLmd0ZSkgJiYgIWhhcyhydWxlcy5ndCkgJiYgdGhpcyA+PSBydWxlcy5sdD8gJ211c3QgYmUgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmx0XSkgOiAnJ0gAErYBCgNsdGUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQooBwkiGAQqDAQoNdGltZXN0YW1wLmx0ZRpyIWhhcyhydWxlcy5ndGUpICYmICFoYXMocnVsZXMuZ3QpICYmIHRoaXMgPiBydWxlcy5sdGU/ICdtdXN0IGJlIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5sdGVdKSA6ICcnSAASZgoGbHRfbm93GAcgASgIQlTCSFEKTwoQdGltZXN0YW1wLmx0X25vdxo7KHJ1bGVzLmx0X25vdyAmJiB0aGlzID4gbm93KSA/ICdtdXN0IGJlIGxlc3MgdGhhbiBub3cnIDogJydIABKpBwoCZ3QYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQv4Gwkj6Bgp4Cgx0aW1lc3RhbXAuZ3QaaCFoYXMocnVsZXMubHQpICYmICFoYXMocnVsZXMubHRlKSAmJiB0aGlzIDw9IHJ1bGVzLmd0PyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RdKSA6ICcnCrEBCg90aW1lc3RhbXAuZ3RfbHQanQFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ICYmICh0aGlzID49IHJ1bGVzLmx0IHx8IHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgYW5kIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndCwgcnVsZXMubHRdKSA6ICcnCrkBChl0aW1lc3RhbXAuZ3RfbHRfZXhjbHVzaXZlGpsBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdCA8PSB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdF0pIDogJycKwQEKEHRpbWVzdGFtcC5ndF9sdGUarAFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3QgJiYgKHRoaXMgPiBydWxlcy5sdGUgfHwgdGhpcyA8PSBydWxlcy5ndCk/ICdtdXN0IGJlIGdyZWF0ZXIgdGhhbiAlcyBhbmQgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnCskBChp0aW1lc3RhbXAuZ3RfbHRlX2V4Y2x1c2l2ZRqqAWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ICYmIChydWxlcy5sdGUgPCB0aGlzICYmIHRoaXMgPD0gcnVsZXMuZ3QpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gJXMgb3IgbGVzcyB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0LCBydWxlcy5sdGVdKSA6ICcnSAES9QcKA2d0ZRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCyQfCSMUHCoYBCg10aW1lc3RhbXAuZ3RlGnUhaGFzKHJ1bGVzLmx0KSAmJiAhaGFzKHJ1bGVzLmx0ZSkgJiYgdGhpcyA8IHJ1bGVzLmd0ZT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzJy5mb3JtYXQoW3J1bGVzLmd0ZV0pIDogJycKwAEKEHRpbWVzdGFtcC5ndGVfbHQaqwFoYXMocnVsZXMubHQpICYmIHJ1bGVzLmx0ID49IHJ1bGVzLmd0ZSAmJiAodGhpcyA+PSBydWxlcy5sdCB8fCB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIGFuZCBsZXNzIHRoYW4gJXMnLmZvcm1hdChbcnVsZXMuZ3RlLCBydWxlcy5sdF0pIDogJycKyAEKGnRpbWVzdGFtcC5ndGVfbHRfZXhjbHVzaXZlGqkBaGFzKHJ1bGVzLmx0KSAmJiBydWxlcy5sdCA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHQgPD0gdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0XSkgOiAnJwrQAQoRdGltZXN0YW1wLmd0ZV9sdGUaugFoYXMocnVsZXMubHRlKSAmJiBydWxlcy5sdGUgPj0gcnVsZXMuZ3RlICYmICh0aGlzID4gcnVsZXMubHRlIHx8IHRoaXMgPCBydWxlcy5ndGUpPyAnbXVzdCBiZSBncmVhdGVyIHRoYW4gb3IgZXF1YWwgdG8gJXMgYW5kIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJycK2AEKG3RpbWVzdGFtcC5ndGVfbHRlX2V4Y2x1c2l2ZRq4AWhhcyhydWxlcy5sdGUpICYmIHJ1bGVzLmx0ZSA8IHJ1bGVzLmd0ZSAmJiAocnVsZXMubHRlIDwgdGhpcyAmJiB0aGlzIDwgcnVsZXMuZ3RlKT8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG9yIGVxdWFsIHRvICVzIG9yIGxlc3MgdGhhbiBvciBlcXVhbCB0byAlcycuZm9ybWF0KFtydWxlcy5ndGUsIHJ1bGVzLmx0ZV0pIDogJydIARJpCgZndF9ub3cYCCABKAhCV8JIVApSChB0aW1lc3RhbXAuZ3Rfbm93Gj4ocnVsZXMuZ3Rfbm93ICYmIHRoaXMgPCBub3cpID8gJ211c3QgYmUgZ3JlYXRlciB0aGFuIG5vdycgOiAnJ0gBErEBCgZ3aXRoaW4YCSABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRpb25ChQHCSIEBCn8KEHRpbWVzdGFtcC53aXRoaW4aa3RoaXMgPCBub3ctcnVsZXMud2l0aGluIHx8IHRoaXMgPiBub3crcnVsZXMud2l0aGluID8gJ211c3QgYmUgd2l0aGluICVzIG9mIG5vdycuZm9ybWF0KFtydWxlcy53aXRoaW5dKSA6ICcnEksKB2V4YW1wbGUYCiADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQh7CSBsKGQoRdGltZXN0YW1wLmV4YW1wbGUaBHRydWUqCQjoBxCAgICAAkILCglsZXNzX3RoYW5CDgoMZ3JlYXRlcl90aGFuIjkKClZpb2xhdGlvbnMSKwoKdmlvbGF0aW9ucxgBIAMoCzIXLmJ1Zi52YWxpZGF0ZS5WaW9sYXRpb24inwEKCVZpb2xhdGlvbhImCgVmaWVsZBgFIAEoCzIXLmJ1Zi52YWxpZGF0ZS5GaWVsZFBhdGgSJQoEcnVsZRgGIAEoCzIXLmJ1Zi52YWxpZGF0ZS5GaWVsZFBhdGgSDwoHcnVsZV9pZBgCIAEoCRIPCgdtZXNzYWdlGAMgASgJEg8KB2Zvcl9rZXkYBCABKAhKBAgBEAJSCmZpZWxkX3BhdGgiPQoJRmllbGRQYXRoEjAKCGVsZW1lbnRzGAEgAygLMh4uYnVmLnZhbGlkYXRlLkZpZWxkUGF0aEVsZW1lbnQi6QIKEEZpZWxkUGF0aEVsZW1lbnQSFAoMZmllbGRfbnVtYmVyGAEgASgFEhIKCmZpZWxkX25hbWUYAiABKAkSPgoKZmllbGRfdHlwZRgDIAEoDjIqLmdvb2dsZS5wcm90b2J1Zi5GaWVsZERlc2NyaXB0b3JQcm90by5UeXBlEjwKCGtleV90eXBlGAQgASgOMiouZ29vZ2xlLnByb3RvYnVmLkZpZWxkRGVzY3JpcHRvclByb3RvLlR5cGUSPgoKdmFsdWVfdHlwZRgFIAEoDjIqLmdvb2dsZS5wcm90b2J1Zi5GaWVsZERlc2NyaXB0b3JQcm90by5UeXBlEg8KBWluZGV4GAYgASgESAASEgoIYm9vbF9rZXkYByABKAhIABIRCgdpbnRfa2V5GAggASgDSAASEgoIdWludF9rZXkYCSABKARIABIUCgpzdHJpbmdfa2V5GAogASgJSABCCwoJc3Vic2NyaXB0KqEBCgZJZ25vcmUSFgoSSUdOT1JFX1VOU1BFQ0lGSUVEEAASGAoUSUdOT1JFX0lGX1pFUk9fVkFMVUUQARIRCg1JR05PUkVfQUxXQVlTEAMiBAgCEAIqDElHTk9SRV9FTVBUWSoOSUdOT1JFX0RFRkFVTFQqF0lHTk9SRV9JRl9ERUZBVUxUX1ZBTFVFKhVJR05PUkVfSUZfVU5QT1BVTEFURUQqbgoKS25vd25SZWdleBIbChdLTk9XTl9SRUdFWF9VTlNQRUNJRklFRBAAEiAKHEtOT1dOX1JFR0VYX0hUVFBfSEVBREVSX05BTUUQARIhCh1LTk9XTl9SRUdFWF9IVFRQX0hFQURFUl9WQUxVRRACOlYKB21lc3NhZ2USHy5nb29nbGUucHJvdG9idWYuTWVzc2FnZU9wdGlvbnMYhwkgASgLMhouYnVmLnZhbGlkYXRlLk1lc3NhZ2VSdWxlc1IHbWVzc2FnZTpOCgVvbmVvZhIdLmdvb2dsZS5wcm90b2J1Zi5PbmVvZk9wdGlvbnMYhwkgASgLMhguYnVmLnZhbGlkYXRlLk9uZW9mUnVsZXNSBW9uZW9mOk4KBWZpZWxkEh0uZ29vZ2xlLnByb3RvYnVmLkZpZWxkT3B0aW9ucxiHCSABKAsyGC5idWYudmFsaWRhdGUuRmllbGRSdWxlc1IFZmllbGQ6XQoKcHJlZGVmaW5lZBIdLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE9wdGlvbnMYiAkgASgLMh0uYnVmLnZhbGlkYXRlLlByZWRlZmluZWRSdWxlc1IKcHJlZGVmaW5lZEJuChJidWlsZC5idWYudmFsaWRhdGVCDVZhbGlkYXRlUHJvdG9QAVpHYnVmLmJ1aWxkL2dlbi9nby9idWZidWlsZC9wcm90b3ZhbGlkYXRlL3Byb3RvY29sYnVmZmVycy9nby9idWYvdmFsaWRhdGU", [file_google_protobuf_descriptor, file_google_protobuf_duration, file_google_protobuf_field_mask, file_google_protobuf_timestamp]); + +/** + * `Rule` represents a validation rule written in the Common Expression + * Language (CEL) syntax. Each Rule includes a unique identifier, an + * optional error message, and the CEL expression to evaluate. For more + * information, [see our documentation](https://protovalidate.com/schemas/custom-rules/). + * + * ```proto + * message Foo { + * option (buf.validate.message).cel = { + * id: "foo.bar" + * message: "bar must be greater than 0" + * expression: "this.bar > 0" + * }; + * int32 bar = 1; + * } + * ``` + * + * @generated from message buf.validate.Rule + */ +export type Rule = Message<"buf.validate.Rule"> & { + /** + * `id` is a string that serves as a machine-readable name for this Rule. + * It should be unique within its scope, which could be either a message or a field. + * + * @generated from field: optional string id = 1; + */ + id: string; + + /** + * `message` is an optional field that provides a human-readable error message + * for this Rule when the CEL expression evaluates to false. If a + * non-empty message is provided, any strings resulting from the CEL + * expression evaluation are ignored. + * + * @generated from field: optional string message = 2; + */ + message: string; + + /** + * `expression` is the actual CEL expression that will be evaluated for + * validation. This string must resolve to either a boolean or a string + * value. If the expression evaluates to false or a non-empty string, the + * validation is considered failed, and the message is rejected. + * + * @generated from field: optional string expression = 3; + */ + expression: string; +}; + +/** + * Describes the message buf.validate.Rule. + * Use `create(RuleSchema)` to create a new message. + */ +export const RuleSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 0); + +/** + * MessageRules represents validation rules that are applied to the entire message. + * It includes disabling options and a list of Rule messages representing Common Expression Language (CEL) validation rules. + * + * @generated from message buf.validate.MessageRules + */ +export type MessageRules = Message<"buf.validate.MessageRules"> & { + /** + * `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation + * rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax. + * + * This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for + * simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will + * be same as the `expression`. + * + * For more information, [see our documentation](https://protovalidate.com/schemas/custom-rules/). + * + * ```proto + * message MyMessage { + * // The field `foo` must be greater than 42. + * option (buf.validate.message).cel_expression = "this.foo > 42"; + * // The field `foo` must be less than 84. + * option (buf.validate.message).cel_expression = "this.foo < 84"; + * optional int32 foo = 1; + * } + * ``` + * + * @generated from field: repeated string cel_expression = 5; + */ + celExpression: string[]; + + /** + * `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message. + * These rules are written in Common Expression Language (CEL) syntax. For more information, + * [see our documentation](https://protovalidate.com/schemas/custom-rules/). + * + * + * ```proto + * message MyMessage { + * // The field `foo` must be greater than 42. + * option (buf.validate.message).cel = { + * id: "my_message.value", + * message: "must be greater than 42", + * expression: "this.foo > 42", + * }; + * optional int32 foo = 1; + * } + * ``` + * + * @generated from field: repeated buf.validate.Rule cel = 3; + */ + cel: Rule[]; + + /** + * `oneof` is a repeated field of type MessageOneofRule that specifies a list of fields + * of which at most one can be present. If `required` is also specified, then exactly one + * of the specified fields _must_ be present. + * + * This will enforce oneof-like constraints with a few features not provided by + * actual Protobuf oneof declarations: + * 1. Repeated and map fields are allowed in this validation. In a Protobuf oneof, + * only scalar fields are allowed. + * 2. Fields with implicit presence are allowed. In a Protobuf oneof, all member + * fields have explicit presence. This means that, for the purpose of determining + * how many fields are set, explicitly setting such a field to its zero value is + * effectively the same as not setting it at all. + * 3. This will always generate validation errors for a message unmarshalled from + * serialized data that sets more than one field. With a Protobuf oneof, when + * multiple fields are present in the serialized form, earlier values are usually + * silently ignored when unmarshalling, with only the last field being set when + * unmarshalling completes. + * + * Note that adding a field to a `oneof` will also set the IGNORE_IF_ZERO_VALUE on the fields. This means + * only the field that is set will be validated and the unset fields are not validated according to the field rules. + * This behavior can be overridden by setting `ignore` against a field. + * + * ```proto + * message MyMessage { + * // Only one of `field1` or `field2` _can_ be present in this message. + * option (buf.validate.message).oneof = { fields: ["field1", "field2"] }; + * // Exactly one of `field3` or `field4` _must_ be present in this message. + * option (buf.validate.message).oneof = { fields: ["field3", "field4"], required: true }; + * string field1 = 1; + * bytes field2 = 2; + * bool field3 = 3; + * int32 field4 = 4; + * } + * ``` + * + * @generated from field: repeated buf.validate.MessageOneofRule oneof = 4; + */ + oneof: MessageOneofRule[]; +}; + +/** + * Describes the message buf.validate.MessageRules. + * Use `create(MessageRulesSchema)` to create a new message. + */ +export const MessageRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 1); + +/** + * @generated from message buf.validate.MessageOneofRule + */ +export type MessageOneofRule = Message<"buf.validate.MessageOneofRule"> & { + /** + * A list of field names to include in the oneof. All field names must be + * defined in the message. At least one field must be specified, and + * duplicates are not permitted. + * + * @generated from field: repeated string fields = 1; + */ + fields: string[]; + + /** + * If true, one of the fields specified _must_ be set. + * + * @generated from field: optional bool required = 2; + */ + required: boolean; +}; + +/** + * Describes the message buf.validate.MessageOneofRule. + * Use `create(MessageOneofRuleSchema)` to create a new message. + */ +export const MessageOneofRuleSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 2); + +/** + * The `OneofRules` message type enables you to manage rules for + * oneof fields in your protobuf messages. + * + * @generated from message buf.validate.OneofRules + */ +export type OneofRules = Message<"buf.validate.OneofRules"> & { + /** + * If `required` is true, exactly one field of the oneof must be set. A + * validation error is returned if no fields in the oneof are set. Further rules + * should be placed on the fields themselves to ensure they are valid values, + * such as `min_len` or `gt`. + * + * ```proto + * message MyMessage { + * oneof value { + * // Either `a` or `b` must be set. If `a` is set, it must also be + * // non-empty; whereas if `b` is set, it can still be an empty string. + * option (buf.validate.oneof).required = true; + * string a = 1 [(buf.validate.field).string.min_len = 1]; + * string b = 2; + * } + * } + * ``` + * + * @generated from field: optional bool required = 1; + */ + required: boolean; +}; + +/** + * Describes the message buf.validate.OneofRules. + * Use `create(OneofRulesSchema)` to create a new message. + */ +export const OneofRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 3); + +/** + * FieldRules encapsulates the rules for each type of field. Depending on + * the field, the correct set should be used to ensure proper validations. + * + * @generated from message buf.validate.FieldRules + */ +export type FieldRules = Message<"buf.validate.FieldRules"> & { + /** + * `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation + * rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax. + * + * This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for + * simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will + * be same as the `expression`. + * + * For more information, [see our documentation](https://protovalidate.com/schemas/custom-rules/). + * + * ```proto + * message MyMessage { + * // The field `value` must be greater than 42. + * optional int32 value = 1 [(buf.validate.field).cel_expression = "this > 42"]; + * } + * ``` + * + * @generated from field: repeated string cel_expression = 29; + */ + celExpression: string[]; + + /** + * `cel` is a repeated field used to represent a textual expression + * in the Common Expression Language (CEL) syntax. For more information, + * [see our documentation](https://protovalidate.com/schemas/custom-rules/). + * + * ```proto + * message MyMessage { + * // The field `value` must be greater than 42. + * optional int32 value = 1 [(buf.validate.field).cel = { + * id: "my_message.value", + * message: "must be greater than 42", + * expression: "this > 42", + * }]; + * } + * ``` + * + * @generated from field: repeated buf.validate.Rule cel = 23; + */ + cel: Rule[]; + + /** + * If `required` is true, the field must be set. A validation error is returned + * if the field is not set. + * + * ```proto + * syntax="proto3"; + * + * message FieldsWithPresence { + * // Requires any string to be set, including the empty string. + * optional string link = 1 [ + * (buf.validate.field).required = true + * ]; + * // Requires true or false to be set. + * optional bool disabled = 2 [ + * (buf.validate.field).required = true + * ]; + * // Requires a message to be set, including the empty message. + * SomeMessage msg = 4 [ + * (buf.validate.field).required = true + * ]; + * } + * ``` + * + * All fields in the example above track presence. By default, Protovalidate + * ignores rules on those fields if no value is set. `required` ensures that + * the fields are set and valid. + * + * Fields that don't track presence are always validated by Protovalidate, + * whether they are set or not. It is not necessary to add `required`. It + * can be added to indicate that the field cannot be the zero value. + * + * ```proto + * syntax="proto3"; + * + * message FieldsWithoutPresence { + * // `string.email` always applies, even to an empty string. + * string link = 1 [ + * (buf.validate.field).string.email = true + * ]; + * // `repeated.min_items` always applies, even to an empty list. + * repeated string labels = 2 [ + * (buf.validate.field).repeated.min_items = 1 + * ]; + * // `required`, for fields that don't track presence, indicates + * // the value of the field can't be the zero value. + * int32 zero_value_not_allowed = 3 [ + * (buf.validate.field).required = true + * ]; + * } + * ``` + * + * To learn which fields track presence, see the + * [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat). + * + * Note: While field rules can be applied to repeated items, map keys, and map + * values, the elements are always considered to be set. Consequently, + * specifying `repeated.items.required` is redundant. + * + * @generated from field: optional bool required = 25; + */ + required: boolean; + + /** + * Ignore validation rules on the field if its value matches the specified + * criteria. See the `Ignore` enum for details. + * + * ```proto + * message UpdateRequest { + * // The uri rule only applies if the field is not an empty string. + * string url = 1 [ + * (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + * (buf.validate.field).string.uri = true + * ]; + * } + * ``` + * + * @generated from field: optional buf.validate.Ignore ignore = 27; + */ + ignore: Ignore; + + /** + * @generated from oneof buf.validate.FieldRules.type + */ + type: { + /** + * Scalar Field Types + * + * @generated from field: buf.validate.FloatRules float = 1; + */ + value: FloatRules; + case: "float"; + } | { + /** + * @generated from field: buf.validate.DoubleRules double = 2; + */ + value: DoubleRules; + case: "double"; + } | { + /** + * @generated from field: buf.validate.Int32Rules int32 = 3; + */ + value: Int32Rules; + case: "int32"; + } | { + /** + * @generated from field: buf.validate.Int64Rules int64 = 4; + */ + value: Int64Rules; + case: "int64"; + } | { + /** + * @generated from field: buf.validate.UInt32Rules uint32 = 5; + */ + value: UInt32Rules; + case: "uint32"; + } | { + /** + * @generated from field: buf.validate.UInt64Rules uint64 = 6; + */ + value: UInt64Rules; + case: "uint64"; + } | { + /** + * @generated from field: buf.validate.SInt32Rules sint32 = 7; + */ + value: SInt32Rules; + case: "sint32"; + } | { + /** + * @generated from field: buf.validate.SInt64Rules sint64 = 8; + */ + value: SInt64Rules; + case: "sint64"; + } | { + /** + * @generated from field: buf.validate.Fixed32Rules fixed32 = 9; + */ + value: Fixed32Rules; + case: "fixed32"; + } | { + /** + * @generated from field: buf.validate.Fixed64Rules fixed64 = 10; + */ + value: Fixed64Rules; + case: "fixed64"; + } | { + /** + * @generated from field: buf.validate.SFixed32Rules sfixed32 = 11; + */ + value: SFixed32Rules; + case: "sfixed32"; + } | { + /** + * @generated from field: buf.validate.SFixed64Rules sfixed64 = 12; + */ + value: SFixed64Rules; + case: "sfixed64"; + } | { + /** + * @generated from field: buf.validate.BoolRules bool = 13; + */ + value: BoolRules; + case: "bool"; + } | { + /** + * @generated from field: buf.validate.StringRules string = 14; + */ + value: StringRules; + case: "string"; + } | { + /** + * @generated from field: buf.validate.BytesRules bytes = 15; + */ + value: BytesRules; + case: "bytes"; + } | { + /** + * Complex Field Types + * + * @generated from field: buf.validate.EnumRules enum = 16; + */ + value: EnumRules; + case: "enum"; + } | { + /** + * @generated from field: buf.validate.RepeatedRules repeated = 18; + */ + value: RepeatedRules; + case: "repeated"; + } | { + /** + * @generated from field: buf.validate.MapRules map = 19; + */ + value: MapRules; + case: "map"; + } | { + /** + * Well-Known Field Types + * + * @generated from field: buf.validate.AnyRules any = 20; + */ + value: AnyRules; + case: "any"; + } | { + /** + * @generated from field: buf.validate.DurationRules duration = 21; + */ + value: DurationRules; + case: "duration"; + } | { + /** + * @generated from field: buf.validate.FieldMaskRules field_mask = 28; + */ + value: FieldMaskRules; + case: "fieldMask"; + } | { + /** + * @generated from field: buf.validate.TimestampRules timestamp = 22; + */ + value: TimestampRules; + case: "timestamp"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message buf.validate.FieldRules. + * Use `create(FieldRulesSchema)` to create a new message. + */ +export const FieldRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 4); + +/** + * PredefinedRules are custom rules that can be re-used with + * multiple fields. + * + * @generated from message buf.validate.PredefinedRules + */ +export type PredefinedRules = Message<"buf.validate.PredefinedRules"> & { + /** + * `cel` is a repeated field used to represent a textual expression + * in the Common Expression Language (CEL) syntax. For more information, + * [see our documentation](https://protovalidate.com/schemas/predefined-rules/). + * + * ```proto + * message MyMessage { + * // The field `value` must be greater than 42. + * optional int32 value = 1 [(buf.validate.predefined).cel = { + * id: "my_message.value", + * message: "must be greater than 42", + * expression: "this > 42", + * }]; + * } + * ``` + * + * @generated from field: repeated buf.validate.Rule cel = 1; + */ + cel: Rule[]; +}; + +/** + * Describes the message buf.validate.PredefinedRules. + * Use `create(PredefinedRulesSchema)` to create a new message. + */ +export const PredefinedRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 5); + +/** + * FloatRules describes the rules applied to `float` values. These + * rules may also be applied to the `google.protobuf.FloatValue` Well-Known-Type. + * + * @generated from message buf.validate.FloatRules + */ +export type FloatRules = Message<"buf.validate.FloatRules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MyFloat { + * // value must equal 42.0 + * float value = 1 [(buf.validate.field).float.const = 42.0]; + * } + * ``` + * + * @generated from field: optional float const = 1; + */ + const: number; + + /** + * @generated from oneof buf.validate.FloatRules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field < + * value). If the field value is equal to or greater than the specified value, + * an error message is generated. + * + * ```proto + * message MyFloat { + * // must be less than 10.0 + * float value = 1 [(buf.validate.field).float.lt = 10.0]; + * } + * ``` + * + * @generated from field: float lt = 2; + */ + value: number; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MyFloat { + * // must be less than or equal to 10.0 + * float value = 1 [(buf.validate.field).float.lte = 10.0]; + * } + * ``` + * + * @generated from field: float lte = 3; + */ + value: number; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.FloatRules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyFloat { + * // must be greater than 5.0 [float.gt] + * float value = 1 [(buf.validate.field).float.gt = 5.0]; + * + * // must be greater than 5 and less than 10.0 [float.gt_lt] + * float other_value = 2 [(buf.validate.field).float = { gt: 5.0, lt: 10.0 }]; + * + * // must be greater than 10 or less than 5.0 [float.gt_lt_exclusive] + * float another_value = 3 [(buf.validate.field).float = { gt: 10.0, lt: 5.0 }]; + * } + * ``` + * + * @generated from field: float gt = 4; + */ + value: number; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyFloat { + * // must be greater than or equal to 5.0 [float.gte] + * float value = 1 [(buf.validate.field).float.gte = 5.0]; + * + * // must be greater than or equal to 5.0 and less than 10.0 [float.gte_lt] + * float other_value = 2 [(buf.validate.field).float = { gte: 5.0, lt: 10.0 }]; + * + * // must be greater than or equal to 10.0 or less than 5.0 [float.gte_lt_exclusive] + * float another_value = 3 [(buf.validate.field).float = { gte: 10.0, lt: 5.0 }]; + * } + * ``` + * + * @generated from field: float gte = 5; + */ + value: number; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message + * is generated. + * + * ```proto + * message MyFloat { + * // must be in list [1.0, 2.0, 3.0] + * float value = 1 [(buf.validate.field).float = { in: [1.0, 2.0, 3.0] }]; + * } + * ``` + * + * @generated from field: repeated float in = 6; + */ + in: number[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MyFloat { + * // value must not be in list [1.0, 2.0, 3.0] + * float value = 1 [(buf.validate.field).float = { not_in: [1.0, 2.0, 3.0] }]; + * } + * ``` + * + * @generated from field: repeated float not_in = 7; + */ + notIn: number[]; + + /** + * `finite` requires the field value to be finite. If the field value is + * infinite or NaN, an error message is generated. + * + * @generated from field: optional bool finite = 8; + */ + finite: boolean; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyFloat { + * float value = 1 [ + * (buf.validate.field).float.example = 1.0, + * (buf.validate.field).float.example = inf + * ]; + * } + * ``` + * + * @generated from field: repeated float example = 9; + */ + example: number[]; +}; + +/** + * Describes the message buf.validate.FloatRules. + * Use `create(FloatRulesSchema)` to create a new message. + */ +export const FloatRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 6); + +/** + * DoubleRules describes the rules applied to `double` values. These + * rules may also be applied to the `google.protobuf.DoubleValue` Well-Known-Type. + * + * @generated from message buf.validate.DoubleRules + */ +export type DoubleRules = Message<"buf.validate.DoubleRules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MyDouble { + * // value must equal 42.0 + * double value = 1 [(buf.validate.field).double.const = 42.0]; + * } + * ``` + * + * @generated from field: optional double const = 1; + */ + const: number; + + /** + * @generated from oneof buf.validate.DoubleRules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field < + * value). If the field value is equal to or greater than the specified + * value, an error message is generated. + * + * ```proto + * message MyDouble { + * // must be less than 10.0 + * double value = 1 [(buf.validate.field).double.lt = 10.0]; + * } + * ``` + * + * @generated from field: double lt = 2; + */ + value: number; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified value + * (field <= value). If the field value is greater than the specified value, + * an error message is generated. + * + * ```proto + * message MyDouble { + * // must be less than or equal to 10.0 + * double value = 1 [(buf.validate.field).double.lte = 10.0]; + * } + * ``` + * + * @generated from field: double lte = 3; + */ + value: number; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.DoubleRules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or `lte`, + * the range is reversed, and the field value must be outside the specified + * range. If the field value doesn't meet the required conditions, an error + * message is generated. + * + * ```proto + * message MyDouble { + * // must be greater than 5.0 [double.gt] + * double value = 1 [(buf.validate.field).double.gt = 5.0]; + * + * // must be greater than 5 and less than 10.0 [double.gt_lt] + * double other_value = 2 [(buf.validate.field).double = { gt: 5.0, lt: 10.0 }]; + * + * // must be greater than 10 or less than 5.0 [double.gt_lt_exclusive] + * double another_value = 3 [(buf.validate.field).double = { gt: 10.0, lt: 5.0 }]; + * } + * ``` + * + * @generated from field: double gt = 4; + */ + value: number; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyDouble { + * // must be greater than or equal to 5.0 [double.gte] + * double value = 1 [(buf.validate.field).double.gte = 5.0]; + * + * // must be greater than or equal to 5.0 and less than 10.0 [double.gte_lt] + * double other_value = 2 [(buf.validate.field).double = { gte: 5.0, lt: 10.0 }]; + * + * // must be greater than or equal to 10.0 or less than 5.0 [double.gte_lt_exclusive] + * double another_value = 3 [(buf.validate.field).double = { gte: 10.0, lt: 5.0 }]; + * } + * ``` + * + * @generated from field: double gte = 5; + */ + value: number; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message is + * generated. + * + * ```proto + * message MyDouble { + * // must be in list [1.0, 2.0, 3.0] + * double value = 1 [(buf.validate.field).double = { in: [1.0, 2.0, 3.0] }]; + * } + * ``` + * + * @generated from field: repeated double in = 6; + */ + in: number[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MyDouble { + * // value must not be in list [1.0, 2.0, 3.0] + * double value = 1 [(buf.validate.field).double = { not_in: [1.0, 2.0, 3.0] }]; + * } + * ``` + * + * @generated from field: repeated double not_in = 7; + */ + notIn: number[]; + + /** + * `finite` requires the field value to be finite. If the field value is + * infinite or NaN, an error message is generated. + * + * @generated from field: optional bool finite = 8; + */ + finite: boolean; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyDouble { + * double value = 1 [ + * (buf.validate.field).double.example = 1.0, + * (buf.validate.field).double.example = inf + * ]; + * } + * ``` + * + * @generated from field: repeated double example = 9; + */ + example: number[]; +}; + +/** + * Describes the message buf.validate.DoubleRules. + * Use `create(DoubleRulesSchema)` to create a new message. + */ +export const DoubleRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 7); + +/** + * Int32Rules describes the rules applied to `int32` values. These + * rules may also be applied to the `google.protobuf.Int32Value` Well-Known-Type. + * + * @generated from message buf.validate.Int32Rules + */ +export type Int32Rules = Message<"buf.validate.Int32Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MyInt32 { + * // value must equal 42 + * int32 value = 1 [(buf.validate.field).int32.const = 42]; + * } + * ``` + * + * @generated from field: optional int32 const = 1; + */ + const: number; + + /** + * @generated from oneof buf.validate.Int32Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field + * < value). If the field value is equal to or greater than the specified + * value, an error message is generated. + * + * ```proto + * message MyInt32 { + * // must be less than 10 + * int32 value = 1 [(buf.validate.field).int32.lt = 10]; + * } + * ``` + * + * @generated from field: int32 lt = 2; + */ + value: number; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MyInt32 { + * // must be less than or equal to 10 + * int32 value = 1 [(buf.validate.field).int32.lte = 10]; + * } + * ``` + * + * @generated from field: int32 lte = 3; + */ + value: number; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.Int32Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyInt32 { + * // must be greater than 5 [int32.gt] + * int32 value = 1 [(buf.validate.field).int32.gt = 5]; + * + * // must be greater than 5 and less than 10 [int32.gt_lt] + * int32 other_value = 2 [(buf.validate.field).int32 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [int32.gt_lt_exclusive] + * int32 another_value = 3 [(buf.validate.field).int32 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: int32 gt = 4; + */ + value: number; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified value + * (exclusive). If the value of `gte` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyInt32 { + * // must be greater than or equal to 5 [int32.gte] + * int32 value = 1 [(buf.validate.field).int32.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [int32.gte_lt] + * int32 other_value = 2 [(buf.validate.field).int32 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [int32.gte_lt_exclusive] + * int32 another_value = 3 [(buf.validate.field).int32 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: int32 gte = 5; + */ + value: number; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message is + * generated. + * + * ```proto + * message MyInt32 { + * // must be in list [1, 2, 3] + * int32 value = 1 [(buf.validate.field).int32 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated int32 in = 6; + */ + in: number[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error message + * is generated. + * + * ```proto + * message MyInt32 { + * // value must not be in list [1, 2, 3] + * int32 value = 1 [(buf.validate.field).int32 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated int32 not_in = 7; + */ + notIn: number[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyInt32 { + * int32 value = 1 [ + * (buf.validate.field).int32.example = 1, + * (buf.validate.field).int32.example = -10 + * ]; + * } + * ``` + * + * @generated from field: repeated int32 example = 8; + */ + example: number[]; +}; + +/** + * Describes the message buf.validate.Int32Rules. + * Use `create(Int32RulesSchema)` to create a new message. + */ +export const Int32RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 8); + +/** + * Int64Rules describes the rules applied to `int64` values. These + * rules may also be applied to the `google.protobuf.Int64Value` Well-Known-Type. + * + * @generated from message buf.validate.Int64Rules + */ +export type Int64Rules = Message<"buf.validate.Int64Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MyInt64 { + * // value must equal 42 + * int64 value = 1 [(buf.validate.field).int64.const = 42]; + * } + * ``` + * + * @generated from field: optional int64 const = 1; + */ + const: bigint; + + /** + * @generated from oneof buf.validate.Int64Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field < + * value). If the field value is equal to or greater than the specified value, + * an error message is generated. + * + * ```proto + * message MyInt64 { + * // must be less than 10 + * int64 value = 1 [(buf.validate.field).int64.lt = 10]; + * } + * ``` + * + * @generated from field: int64 lt = 2; + */ + value: bigint; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MyInt64 { + * // must be less than or equal to 10 + * int64 value = 1 [(buf.validate.field).int64.lte = 10]; + * } + * ``` + * + * @generated from field: int64 lte = 3; + */ + value: bigint; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.Int64Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyInt64 { + * // must be greater than 5 [int64.gt] + * int64 value = 1 [(buf.validate.field).int64.gt = 5]; + * + * // must be greater than 5 and less than 10 [int64.gt_lt] + * int64 other_value = 2 [(buf.validate.field).int64 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [int64.gt_lt_exclusive] + * int64 another_value = 3 [(buf.validate.field).int64 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: int64 gt = 4; + */ + value: bigint; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyInt64 { + * // must be greater than or equal to 5 [int64.gte] + * int64 value = 1 [(buf.validate.field).int64.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [int64.gte_lt] + * int64 other_value = 2 [(buf.validate.field).int64 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [int64.gte_lt_exclusive] + * int64 another_value = 3 [(buf.validate.field).int64 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: int64 gte = 5; + */ + value: bigint; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message is + * generated. + * + * ```proto + * message MyInt64 { + * // must be in list [1, 2, 3] + * int64 value = 1 [(buf.validate.field).int64 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated int64 in = 6; + */ + in: bigint[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MyInt64 { + * // value must not be in list [1, 2, 3] + * int64 value = 1 [(buf.validate.field).int64 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated int64 not_in = 7; + */ + notIn: bigint[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyInt64 { + * int64 value = 1 [ + * (buf.validate.field).int64.example = 1, + * (buf.validate.field).int64.example = -10 + * ]; + * } + * ``` + * + * @generated from field: repeated int64 example = 9; + */ + example: bigint[]; +}; + +/** + * Describes the message buf.validate.Int64Rules. + * Use `create(Int64RulesSchema)` to create a new message. + */ +export const Int64RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 9); + +/** + * UInt32Rules describes the rules applied to `uint32` values. These + * rules may also be applied to the `google.protobuf.UInt32Value` Well-Known-Type. + * + * @generated from message buf.validate.UInt32Rules + */ +export type UInt32Rules = Message<"buf.validate.UInt32Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MyUInt32 { + * // value must equal 42 + * uint32 value = 1 [(buf.validate.field).uint32.const = 42]; + * } + * ``` + * + * @generated from field: optional uint32 const = 1; + */ + const: number; + + /** + * @generated from oneof buf.validate.UInt32Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field < + * value). If the field value is equal to or greater than the specified value, + * an error message is generated. + * + * ```proto + * message MyUInt32 { + * // must be less than 10 + * uint32 value = 1 [(buf.validate.field).uint32.lt = 10]; + * } + * ``` + * + * @generated from field: uint32 lt = 2; + */ + value: number; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MyUInt32 { + * // must be less than or equal to 10 + * uint32 value = 1 [(buf.validate.field).uint32.lte = 10]; + * } + * ``` + * + * @generated from field: uint32 lte = 3; + */ + value: number; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.UInt32Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyUInt32 { + * // must be greater than 5 [uint32.gt] + * uint32 value = 1 [(buf.validate.field).uint32.gt = 5]; + * + * // must be greater than 5 and less than 10 [uint32.gt_lt] + * uint32 other_value = 2 [(buf.validate.field).uint32 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [uint32.gt_lt_exclusive] + * uint32 another_value = 3 [(buf.validate.field).uint32 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: uint32 gt = 4; + */ + value: number; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyUInt32 { + * // must be greater than or equal to 5 [uint32.gte] + * uint32 value = 1 [(buf.validate.field).uint32.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [uint32.gte_lt] + * uint32 other_value = 2 [(buf.validate.field).uint32 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [uint32.gte_lt_exclusive] + * uint32 another_value = 3 [(buf.validate.field).uint32 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: uint32 gte = 5; + */ + value: number; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message is + * generated. + * + * ```proto + * message MyUInt32 { + * // must be in list [1, 2, 3] + * uint32 value = 1 [(buf.validate.field).uint32 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated uint32 in = 6; + */ + in: number[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MyUInt32 { + * // value must not be in list [1, 2, 3] + * uint32 value = 1 [(buf.validate.field).uint32 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated uint32 not_in = 7; + */ + notIn: number[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyUInt32 { + * uint32 value = 1 [ + * (buf.validate.field).uint32.example = 1, + * (buf.validate.field).uint32.example = 10 + * ]; + * } + * ``` + * + * @generated from field: repeated uint32 example = 8; + */ + example: number[]; +}; + +/** + * Describes the message buf.validate.UInt32Rules. + * Use `create(UInt32RulesSchema)` to create a new message. + */ +export const UInt32RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 10); + +/** + * UInt64Rules describes the rules applied to `uint64` values. These + * rules may also be applied to the `google.protobuf.UInt64Value` Well-Known-Type. + * + * @generated from message buf.validate.UInt64Rules + */ +export type UInt64Rules = Message<"buf.validate.UInt64Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MyUInt64 { + * // value must equal 42 + * uint64 value = 1 [(buf.validate.field).uint64.const = 42]; + * } + * ``` + * + * @generated from field: optional uint64 const = 1; + */ + const: bigint; + + /** + * @generated from oneof buf.validate.UInt64Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field < + * value). If the field value is equal to or greater than the specified value, + * an error message is generated. + * + * ```proto + * message MyUInt64 { + * // must be less than 10 + * uint64 value = 1 [(buf.validate.field).uint64.lt = 10]; + * } + * ``` + * + * @generated from field: uint64 lt = 2; + */ + value: bigint; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MyUInt64 { + * // must be less than or equal to 10 + * uint64 value = 1 [(buf.validate.field).uint64.lte = 10]; + * } + * ``` + * + * @generated from field: uint64 lte = 3; + */ + value: bigint; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.UInt64Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyUInt64 { + * // must be greater than 5 [uint64.gt] + * uint64 value = 1 [(buf.validate.field).uint64.gt = 5]; + * + * // must be greater than 5 and less than 10 [uint64.gt_lt] + * uint64 other_value = 2 [(buf.validate.field).uint64 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [uint64.gt_lt_exclusive] + * uint64 another_value = 3 [(buf.validate.field).uint64 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: uint64 gt = 4; + */ + value: bigint; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyUInt64 { + * // must be greater than or equal to 5 [uint64.gte] + * uint64 value = 1 [(buf.validate.field).uint64.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [uint64.gte_lt] + * uint64 other_value = 2 [(buf.validate.field).uint64 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [uint64.gte_lt_exclusive] + * uint64 another_value = 3 [(buf.validate.field).uint64 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: uint64 gte = 5; + */ + value: bigint; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message is + * generated. + * + * ```proto + * message MyUInt64 { + * // must be in list [1, 2, 3] + * uint64 value = 1 [(buf.validate.field).uint64 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated uint64 in = 6; + */ + in: bigint[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MyUInt64 { + * // value must not be in list [1, 2, 3] + * uint64 value = 1 [(buf.validate.field).uint64 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated uint64 not_in = 7; + */ + notIn: bigint[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyUInt64 { + * uint64 value = 1 [ + * (buf.validate.field).uint64.example = 1, + * (buf.validate.field).uint64.example = 10 + * ]; + * } + * ``` + * + * @generated from field: repeated uint64 example = 8; + */ + example: bigint[]; +}; + +/** + * Describes the message buf.validate.UInt64Rules. + * Use `create(UInt64RulesSchema)` to create a new message. + */ +export const UInt64RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 11); + +/** + * SInt32Rules describes the rules applied to `sint32` values. + * + * @generated from message buf.validate.SInt32Rules + */ +export type SInt32Rules = Message<"buf.validate.SInt32Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MySInt32 { + * // value must equal 42 + * sint32 value = 1 [(buf.validate.field).sint32.const = 42]; + * } + * ``` + * + * @generated from field: optional sint32 const = 1; + */ + const: number; + + /** + * @generated from oneof buf.validate.SInt32Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field + * < value). If the field value is equal to or greater than the specified + * value, an error message is generated. + * + * ```proto + * message MySInt32 { + * // must be less than 10 + * sint32 value = 1 [(buf.validate.field).sint32.lt = 10]; + * } + * ``` + * + * @generated from field: sint32 lt = 2; + */ + value: number; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MySInt32 { + * // must be less than or equal to 10 + * sint32 value = 1 [(buf.validate.field).sint32.lte = 10]; + * } + * ``` + * + * @generated from field: sint32 lte = 3; + */ + value: number; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.SInt32Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MySInt32 { + * // must be greater than 5 [sint32.gt] + * sint32 value = 1 [(buf.validate.field).sint32.gt = 5]; + * + * // must be greater than 5 and less than 10 [sint32.gt_lt] + * sint32 other_value = 2 [(buf.validate.field).sint32 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [sint32.gt_lt_exclusive] + * sint32 another_value = 3 [(buf.validate.field).sint32 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: sint32 gt = 4; + */ + value: number; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MySInt32 { + * // must be greater than or equal to 5 [sint32.gte] + * sint32 value = 1 [(buf.validate.field).sint32.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [sint32.gte_lt] + * sint32 other_value = 2 [(buf.validate.field).sint32 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [sint32.gte_lt_exclusive] + * sint32 another_value = 3 [(buf.validate.field).sint32 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: sint32 gte = 5; + */ + value: number; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message is + * generated. + * + * ```proto + * message MySInt32 { + * // must be in list [1, 2, 3] + * sint32 value = 1 [(buf.validate.field).sint32 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated sint32 in = 6; + */ + in: number[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MySInt32 { + * // value must not be in list [1, 2, 3] + * sint32 value = 1 [(buf.validate.field).sint32 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated sint32 not_in = 7; + */ + notIn: number[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MySInt32 { + * sint32 value = 1 [ + * (buf.validate.field).sint32.example = 1, + * (buf.validate.field).sint32.example = -10 + * ]; + * } + * ``` + * + * @generated from field: repeated sint32 example = 8; + */ + example: number[]; +}; + +/** + * Describes the message buf.validate.SInt32Rules. + * Use `create(SInt32RulesSchema)` to create a new message. + */ +export const SInt32RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 12); + +/** + * SInt64Rules describes the rules applied to `sint64` values. + * + * @generated from message buf.validate.SInt64Rules + */ +export type SInt64Rules = Message<"buf.validate.SInt64Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MySInt64 { + * // value must equal 42 + * sint64 value = 1 [(buf.validate.field).sint64.const = 42]; + * } + * ``` + * + * @generated from field: optional sint64 const = 1; + */ + const: bigint; + + /** + * @generated from oneof buf.validate.SInt64Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field + * < value). If the field value is equal to or greater than the specified + * value, an error message is generated. + * + * ```proto + * message MySInt64 { + * // must be less than 10 + * sint64 value = 1 [(buf.validate.field).sint64.lt = 10]; + * } + * ``` + * + * @generated from field: sint64 lt = 2; + */ + value: bigint; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MySInt64 { + * // must be less than or equal to 10 + * sint64 value = 1 [(buf.validate.field).sint64.lte = 10]; + * } + * ``` + * + * @generated from field: sint64 lte = 3; + */ + value: bigint; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.SInt64Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MySInt64 { + * // must be greater than 5 [sint64.gt] + * sint64 value = 1 [(buf.validate.field).sint64.gt = 5]; + * + * // must be greater than 5 and less than 10 [sint64.gt_lt] + * sint64 other_value = 2 [(buf.validate.field).sint64 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [sint64.gt_lt_exclusive] + * sint64 another_value = 3 [(buf.validate.field).sint64 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: sint64 gt = 4; + */ + value: bigint; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MySInt64 { + * // must be greater than or equal to 5 [sint64.gte] + * sint64 value = 1 [(buf.validate.field).sint64.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [sint64.gte_lt] + * sint64 other_value = 2 [(buf.validate.field).sint64 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [sint64.gte_lt_exclusive] + * sint64 another_value = 3 [(buf.validate.field).sint64 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: sint64 gte = 5; + */ + value: bigint; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message + * is generated. + * + * ```proto + * message MySInt64 { + * // must be in list [1, 2, 3] + * sint64 value = 1 [(buf.validate.field).sint64 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated sint64 in = 6; + */ + in: bigint[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MySInt64 { + * // value must not be in list [1, 2, 3] + * sint64 value = 1 [(buf.validate.field).sint64 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated sint64 not_in = 7; + */ + notIn: bigint[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MySInt64 { + * sint64 value = 1 [ + * (buf.validate.field).sint64.example = 1, + * (buf.validate.field).sint64.example = -10 + * ]; + * } + * ``` + * + * @generated from field: repeated sint64 example = 8; + */ + example: bigint[]; +}; + +/** + * Describes the message buf.validate.SInt64Rules. + * Use `create(SInt64RulesSchema)` to create a new message. + */ +export const SInt64RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 13); + +/** + * Fixed32Rules describes the rules applied to `fixed32` values. + * + * @generated from message buf.validate.Fixed32Rules + */ +export type Fixed32Rules = Message<"buf.validate.Fixed32Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. + * If the field value doesn't match, an error message is generated. + * + * ```proto + * message MyFixed32 { + * // value must equal 42 + * fixed32 value = 1 [(buf.validate.field).fixed32.const = 42]; + * } + * ``` + * + * @generated from field: optional fixed32 const = 1; + */ + const: number; + + /** + * @generated from oneof buf.validate.Fixed32Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field < + * value). If the field value is equal to or greater than the specified value, + * an error message is generated. + * + * ```proto + * message MyFixed32 { + * // must be less than 10 + * fixed32 value = 1 [(buf.validate.field).fixed32.lt = 10]; + * } + * ``` + * + * @generated from field: fixed32 lt = 2; + */ + value: number; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MyFixed32 { + * // must be less than or equal to 10 + * fixed32 value = 1 [(buf.validate.field).fixed32.lte = 10]; + * } + * ``` + * + * @generated from field: fixed32 lte = 3; + */ + value: number; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.Fixed32Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyFixed32 { + * // must be greater than 5 [fixed32.gt] + * fixed32 value = 1 [(buf.validate.field).fixed32.gt = 5]; + * + * // must be greater than 5 and less than 10 [fixed32.gt_lt] + * fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [fixed32.gt_lt_exclusive] + * fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: fixed32 gt = 4; + */ + value: number; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyFixed32 { + * // must be greater than or equal to 5 [fixed32.gte] + * fixed32 value = 1 [(buf.validate.field).fixed32.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [fixed32.gte_lt] + * fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [fixed32.gte_lt_exclusive] + * fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: fixed32 gte = 5; + */ + value: number; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message + * is generated. + * + * ```proto + * message MyFixed32 { + * // must be in list [1, 2, 3] + * fixed32 value = 1 [(buf.validate.field).fixed32 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated fixed32 in = 6; + */ + in: number[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MyFixed32 { + * // value must not be in list [1, 2, 3] + * fixed32 value = 1 [(buf.validate.field).fixed32 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated fixed32 not_in = 7; + */ + notIn: number[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyFixed32 { + * fixed32 value = 1 [ + * (buf.validate.field).fixed32.example = 1, + * (buf.validate.field).fixed32.example = 2 + * ]; + * } + * ``` + * + * @generated from field: repeated fixed32 example = 8; + */ + example: number[]; +}; + +/** + * Describes the message buf.validate.Fixed32Rules. + * Use `create(Fixed32RulesSchema)` to create a new message. + */ +export const Fixed32RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 14); + +/** + * Fixed64Rules describes the rules applied to `fixed64` values. + * + * @generated from message buf.validate.Fixed64Rules + */ +export type Fixed64Rules = Message<"buf.validate.Fixed64Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MyFixed64 { + * // value must equal 42 + * fixed64 value = 1 [(buf.validate.field).fixed64.const = 42]; + * } + * ``` + * + * @generated from field: optional fixed64 const = 1; + */ + const: bigint; + + /** + * @generated from oneof buf.validate.Fixed64Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field < + * value). If the field value is equal to or greater than the specified value, + * an error message is generated. + * + * ```proto + * message MyFixed64 { + * // must be less than 10 + * fixed64 value = 1 [(buf.validate.field).fixed64.lt = 10]; + * } + * ``` + * + * @generated from field: fixed64 lt = 2; + */ + value: bigint; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MyFixed64 { + * // must be less than or equal to 10 + * fixed64 value = 1 [(buf.validate.field).fixed64.lte = 10]; + * } + * ``` + * + * @generated from field: fixed64 lte = 3; + */ + value: bigint; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.Fixed64Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyFixed64 { + * // must be greater than 5 [fixed64.gt] + * fixed64 value = 1 [(buf.validate.field).fixed64.gt = 5]; + * + * // must be greater than 5 and less than 10 [fixed64.gt_lt] + * fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [fixed64.gt_lt_exclusive] + * fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: fixed64 gt = 4; + */ + value: bigint; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyFixed64 { + * // must be greater than or equal to 5 [fixed64.gte] + * fixed64 value = 1 [(buf.validate.field).fixed64.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [fixed64.gte_lt] + * fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [fixed64.gte_lt_exclusive] + * fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: fixed64 gte = 5; + */ + value: bigint; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message is + * generated. + * + * ```proto + * message MyFixed64 { + * // must be in list [1, 2, 3] + * fixed64 value = 1 [(buf.validate.field).fixed64 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated fixed64 in = 6; + */ + in: bigint[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MyFixed64 { + * // value must not be in list [1, 2, 3] + * fixed64 value = 1 [(buf.validate.field).fixed64 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated fixed64 not_in = 7; + */ + notIn: bigint[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyFixed64 { + * fixed64 value = 1 [ + * (buf.validate.field).fixed64.example = 1, + * (buf.validate.field).fixed64.example = 2 + * ]; + * } + * ``` + * + * @generated from field: repeated fixed64 example = 8; + */ + example: bigint[]; +}; + +/** + * Describes the message buf.validate.Fixed64Rules. + * Use `create(Fixed64RulesSchema)` to create a new message. + */ +export const Fixed64RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 15); + +/** + * SFixed32Rules describes the rules applied to `fixed32` values. + * + * @generated from message buf.validate.SFixed32Rules + */ +export type SFixed32Rules = Message<"buf.validate.SFixed32Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MySFixed32 { + * // value must equal 42 + * sfixed32 value = 1 [(buf.validate.field).sfixed32.const = 42]; + * } + * ``` + * + * @generated from field: optional sfixed32 const = 1; + */ + const: number; + + /** + * @generated from oneof buf.validate.SFixed32Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field < + * value). If the field value is equal to or greater than the specified value, + * an error message is generated. + * + * ```proto + * message MySFixed32 { + * // must be less than 10 + * sfixed32 value = 1 [(buf.validate.field).sfixed32.lt = 10]; + * } + * ``` + * + * @generated from field: sfixed32 lt = 2; + */ + value: number; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MySFixed32 { + * // must be less than or equal to 10 + * sfixed32 value = 1 [(buf.validate.field).sfixed32.lte = 10]; + * } + * ``` + * + * @generated from field: sfixed32 lte = 3; + */ + value: number; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.SFixed32Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MySFixed32 { + * // must be greater than 5 [sfixed32.gt] + * sfixed32 value = 1 [(buf.validate.field).sfixed32.gt = 5]; + * + * // must be greater than 5 and less than 10 [sfixed32.gt_lt] + * sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [sfixed32.gt_lt_exclusive] + * sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: sfixed32 gt = 4; + */ + value: number; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MySFixed32 { + * // must be greater than or equal to 5 [sfixed32.gte] + * sfixed32 value = 1 [(buf.validate.field).sfixed32.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [sfixed32.gte_lt] + * sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [sfixed32.gte_lt_exclusive] + * sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: sfixed32 gte = 5; + */ + value: number; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message is + * generated. + * + * ```proto + * message MySFixed32 { + * // must be in list [1, 2, 3] + * sfixed32 value = 1 [(buf.validate.field).sfixed32 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated sfixed32 in = 6; + */ + in: number[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MySFixed32 { + * // value must not be in list [1, 2, 3] + * sfixed32 value = 1 [(buf.validate.field).sfixed32 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated sfixed32 not_in = 7; + */ + notIn: number[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MySFixed32 { + * sfixed32 value = 1 [ + * (buf.validate.field).sfixed32.example = 1, + * (buf.validate.field).sfixed32.example = 2 + * ]; + * } + * ``` + * + * @generated from field: repeated sfixed32 example = 8; + */ + example: number[]; +}; + +/** + * Describes the message buf.validate.SFixed32Rules. + * Use `create(SFixed32RulesSchema)` to create a new message. + */ +export const SFixed32RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 16); + +/** + * SFixed64Rules describes the rules applied to `fixed64` values. + * + * @generated from message buf.validate.SFixed64Rules + */ +export type SFixed64Rules = Message<"buf.validate.SFixed64Rules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MySFixed64 { + * // value must equal 42 + * sfixed64 value = 1 [(buf.validate.field).sfixed64.const = 42]; + * } + * ``` + * + * @generated from field: optional sfixed64 const = 1; + */ + const: bigint; + + /** + * @generated from oneof buf.validate.SFixed64Rules.less_than + */ + lessThan: { + /** + * `lt` requires the field value to be less than the specified value (field < + * value). If the field value is equal to or greater than the specified value, + * an error message is generated. + * + * ```proto + * message MySFixed64 { + * // must be less than 10 + * sfixed64 value = 1 [(buf.validate.field).sfixed64.lt = 10]; + * } + * ``` + * + * @generated from field: sfixed64 lt = 2; + */ + value: bigint; + case: "lt"; + } | { + /** + * `lte` requires the field value to be less than or equal to the specified + * value (field <= value). If the field value is greater than the specified + * value, an error message is generated. + * + * ```proto + * message MySFixed64 { + * // must be less than or equal to 10 + * sfixed64 value = 1 [(buf.validate.field).sfixed64.lte = 10]; + * } + * ``` + * + * @generated from field: sfixed64 lte = 3; + */ + value: bigint; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.SFixed64Rules.greater_than + */ + greaterThan: { + /** + * `gt` requires the field value to be greater than the specified value + * (exclusive). If the value of `gt` is larger than a specified `lt` or + * `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MySFixed64 { + * // must be greater than 5 [sfixed64.gt] + * sfixed64 value = 1 [(buf.validate.field).sfixed64.gt = 5]; + * + * // must be greater than 5 and less than 10 [sfixed64.gt_lt] + * sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gt: 5, lt: 10 }]; + * + * // must be greater than 10 or less than 5 [sfixed64.gt_lt_exclusive] + * sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gt: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: sfixed64 gt = 4; + */ + value: bigint; + case: "gt"; + } | { + /** + * `gte` requires the field value to be greater than or equal to the specified + * value (exclusive). If the value of `gte` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MySFixed64 { + * // must be greater than or equal to 5 [sfixed64.gte] + * sfixed64 value = 1 [(buf.validate.field).sfixed64.gte = 5]; + * + * // must be greater than or equal to 5 and less than 10 [sfixed64.gte_lt] + * sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gte: 5, lt: 10 }]; + * + * // must be greater than or equal to 10 or less than 5 [sfixed64.gte_lt_exclusive] + * sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gte: 10, lt: 5 }]; + * } + * ``` + * + * @generated from field: sfixed64 gte = 5; + */ + value: bigint; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` requires the field value to be equal to one of the specified values. + * If the field value isn't one of the specified values, an error message is + * generated. + * + * ```proto + * message MySFixed64 { + * // must be in list [1, 2, 3] + * sfixed64 value = 1 [(buf.validate.field).sfixed64 = { in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated sfixed64 in = 6; + */ + in: bigint[]; + + /** + * `not_in` requires the field value to not be equal to any of the specified + * values. If the field value is one of the specified values, an error + * message is generated. + * + * ```proto + * message MySFixed64 { + * // value must not be in list [1, 2, 3] + * sfixed64 value = 1 [(buf.validate.field).sfixed64 = { not_in: [1, 2, 3] }]; + * } + * ``` + * + * @generated from field: repeated sfixed64 not_in = 7; + */ + notIn: bigint[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MySFixed64 { + * sfixed64 value = 1 [ + * (buf.validate.field).sfixed64.example = 1, + * (buf.validate.field).sfixed64.example = 2 + * ]; + * } + * ``` + * + * @generated from field: repeated sfixed64 example = 8; + */ + example: bigint[]; +}; + +/** + * Describes the message buf.validate.SFixed64Rules. + * Use `create(SFixed64RulesSchema)` to create a new message. + */ +export const SFixed64RulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 17); + +/** + * BoolRules describes the rules applied to `bool` values. These rules + * may also be applied to the `google.protobuf.BoolValue` Well-Known-Type. + * + * @generated from message buf.validate.BoolRules + */ +export type BoolRules = Message<"buf.validate.BoolRules"> & { + /** + * `const` requires the field value to exactly match the specified boolean value. + * If the field value doesn't match, an error message is generated. + * + * ```proto + * message MyBool { + * // value must equal true + * bool value = 1 [(buf.validate.field).bool.const = true]; + * } + * ``` + * + * @generated from field: optional bool const = 1; + */ + const: boolean; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyBool { + * bool value = 1 [ + * (buf.validate.field).bool.example = true, + * (buf.validate.field).bool.example = false + * ]; + * } + * ``` + * + * @generated from field: repeated bool example = 2; + */ + example: boolean[]; +}; + +/** + * Describes the message buf.validate.BoolRules. + * Use `create(BoolRulesSchema)` to create a new message. + */ +export const BoolRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 18); + +/** + * StringRules describes the rules applied to `string` values These + * rules may also be applied to the `google.protobuf.StringValue` Well-Known-Type. + * + * @generated from message buf.validate.StringRules + */ +export type StringRules = Message<"buf.validate.StringRules"> & { + /** + * `const` requires the field value to exactly match the specified value. If + * the field value doesn't match, an error message is generated. + * + * ```proto + * message MyString { + * // value must equal `hello` + * string value = 1 [(buf.validate.field).string.const = "hello"]; + * } + * ``` + * + * @generated from field: optional string const = 1; + */ + const: string; + + /** + * `len` dictates that the field value must have the specified + * number of characters (Unicode code points), which may differ from the number + * of bytes in the string. If the field value does not meet the specified + * length, an error message will be generated. + * + * ```proto + * message MyString { + * // value length must be 5 characters + * string value = 1 [(buf.validate.field).string.len = 5]; + * } + * ``` + * + * @generated from field: optional uint64 len = 19; + */ + len: bigint; + + /** + * `min_len` specifies that the field value must have at least the specified + * number of characters (Unicode code points), which may differ from the number + * of bytes in the string. If the field value contains fewer characters, an error + * message will be generated. + * + * ```proto + * message MyString { + * // value length must be at least 3 characters + * string value = 1 [(buf.validate.field).string.min_len = 3]; + * } + * ``` + * + * @generated from field: optional uint64 min_len = 2; + */ + minLen: bigint; + + /** + * `max_len` specifies that the field value must have no more than the specified + * number of characters (Unicode code points), which may differ from the + * number of bytes in the string. If the field value contains more characters, + * an error message will be generated. + * + * ```proto + * message MyString { + * // value length must be at most 10 characters + * string value = 1 [(buf.validate.field).string.max_len = 10]; + * } + * ``` + * + * @generated from field: optional uint64 max_len = 3; + */ + maxLen: bigint; + + /** + * `len_bytes` dictates that the field value must have the specified number of + * bytes. If the field value does not match the specified length in bytes, + * an error message will be generated. + * + * ```proto + * message MyString { + * // value length must be 6 bytes + * string value = 1 [(buf.validate.field).string.len_bytes = 6]; + * } + * ``` + * + * @generated from field: optional uint64 len_bytes = 20; + */ + lenBytes: bigint; + + /** + * `min_bytes` specifies that the field value must have at least the specified + * number of bytes. If the field value contains fewer bytes, an error message + * will be generated. + * + * ```proto + * message MyString { + * // value length must be at least 4 bytes + * string value = 1 [(buf.validate.field).string.min_bytes = 4]; + * } + * + * ``` + * + * @generated from field: optional uint64 min_bytes = 4; + */ + minBytes: bigint; + + /** + * `max_bytes` specifies that the field value must have no more than the + * specified number of bytes. If the field value contains more bytes, an + * error message will be generated. + * + * ```proto + * message MyString { + * // value length must be at most 8 bytes + * string value = 1 [(buf.validate.field).string.max_bytes = 8]; + * } + * ``` + * + * @generated from field: optional uint64 max_bytes = 5; + */ + maxBytes: bigint; + + /** + * `pattern` specifies that the field value must match the specified + * regular expression (RE2 syntax), with the expression provided without any + * delimiters. If the field value doesn't match the regular expression, an + * error message will be generated. + * + * ```proto + * message MyString { + * // value does not match regex pattern `^[a-zA-Z]//$` + * string value = 1 [(buf.validate.field).string.pattern = "^[a-zA-Z]//$"]; + * } + * ``` + * + * @generated from field: optional string pattern = 6; + */ + pattern: string; + + /** + * `prefix` specifies that the field value must have the + * specified substring at the beginning of the string. If the field value + * doesn't start with the specified prefix, an error message will be + * generated. + * + * ```proto + * message MyString { + * // value does not have prefix `pre` + * string value = 1 [(buf.validate.field).string.prefix = "pre"]; + * } + * ``` + * + * @generated from field: optional string prefix = 7; + */ + prefix: string; + + /** + * `suffix` specifies that the field value must have the + * specified substring at the end of the string. If the field value doesn't + * end with the specified suffix, an error message will be generated. + * + * ```proto + * message MyString { + * // value does not have suffix `post` + * string value = 1 [(buf.validate.field).string.suffix = "post"]; + * } + * ``` + * + * @generated from field: optional string suffix = 8; + */ + suffix: string; + + /** + * `contains` specifies that the field value must have the + * specified substring anywhere in the string. If the field value doesn't + * contain the specified substring, an error message will be generated. + * + * ```proto + * message MyString { + * // value does not contain substring `inside`. + * string value = 1 [(buf.validate.field).string.contains = "inside"]; + * } + * ``` + * + * @generated from field: optional string contains = 9; + */ + contains: string; + + /** + * `not_contains` specifies that the field value must not have the + * specified substring anywhere in the string. If the field value contains + * the specified substring, an error message will be generated. + * + * ```proto + * message MyString { + * // value contains substring `inside`. + * string value = 1 [(buf.validate.field).string.not_contains = "inside"]; + * } + * ``` + * + * @generated from field: optional string not_contains = 23; + */ + notContains: string; + + /** + * `in` specifies that the field value must be equal to one of the specified + * values. If the field value isn't one of the specified values, an error + * message will be generated. + * + * ```proto + * message MyString { + * // must be in list ["apple", "banana"] + * string value = 1 [(buf.validate.field).string = { in: ["apple", "banana"] }]; + * } + * ``` + * + * @generated from field: repeated string in = 10; + */ + in: string[]; + + /** + * `not_in` specifies that the field value cannot be equal to any + * of the specified values. If the field value is one of the specified values, + * an error message will be generated. + * ```proto + * message MyString { + * // value must not be in list ["orange", "grape"] + * string value = 1 [(buf.validate.field).string = { not_in: ["orange", "grape"] }]; + * } + * ``` + * + * @generated from field: repeated string not_in = 11; + */ + notIn: string[]; + + /** + * `WellKnown` rules provide advanced rules against common string + * patterns. + * + * @generated from oneof buf.validate.StringRules.well_known + */ + wellKnown: { + /** + * `email` specifies that the field value must be a valid email address, for + * example "foo@example.com". + * + * Conforms to the definition for a valid email address from the [HTML standard](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address). + * Note that this standard willfully deviates from [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322), + * which allows many unexpected forms of email addresses and will easily match + * a typographical error. + * + * If the field value isn't a valid email address, an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid email address + * string value = 1 [(buf.validate.field).string.email = true]; + * } + * ``` + * + * @generated from field: bool email = 12; + */ + value: boolean; + case: "email"; + } | { + /** + * `hostname` specifies that the field value must be a valid hostname, for + * example "foo.example.com". + * + * A valid hostname follows the rules below: + * - The name consists of one or more labels, separated by a dot ("."). + * - Each label can be 1 to 63 alphanumeric characters. + * - A label can contain hyphens ("-"), but must not start or end with a hyphen. + * - The right-most label must not be digits only. + * - The name can have a trailing dot—for example, "foo.example.com.". + * - The name can be 253 characters at most, excluding the optional trailing dot. + * + * If the field value isn't a valid hostname, an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid hostname + * string value = 1 [(buf.validate.field).string.hostname = true]; + * } + * ``` + * + * @generated from field: bool hostname = 13; + */ + value: boolean; + case: "hostname"; + } | { + /** + * `ip` specifies that the field value must be a valid IP (v4 or v6) address. + * + * IPv4 addresses are expected in the dotted decimal format—for example, "192.168.5.21". + * IPv6 addresses are expected in their text representation—for example, "::1", + * or "2001:0DB8:ABCD:0012::0". + * + * Both formats are well-defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). + * Zone identifiers for IPv6 addresses (for example, "fe80::a%en1") are supported. + * + * If the field value isn't a valid IP address, an error message will be + * generated. + * + * ```proto + * message MyString { + * // must be a valid IP address + * string value = 1 [(buf.validate.field).string.ip = true]; + * } + * ``` + * + * @generated from field: bool ip = 14; + */ + value: boolean; + case: "ip"; + } | { + /** + * `ipv4` specifies that the field value must be a valid IPv4 address—for + * example "192.168.5.21". If the field value isn't a valid IPv4 address, an + * error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid IPv4 address + * string value = 1 [(buf.validate.field).string.ipv4 = true]; + * } + * ``` + * + * @generated from field: bool ipv4 = 15; + */ + value: boolean; + case: "ipv4"; + } | { + /** + * `ipv6` specifies that the field value must be a valid IPv6 address—for + * example "::1", or "d7a:115c:a1e0:ab12:4843:cd96:626b:430b". If the field + * value is not a valid IPv6 address, an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid IPv6 address + * string value = 1 [(buf.validate.field).string.ipv6 = true]; + * } + * ``` + * + * @generated from field: bool ipv6 = 16; + */ + value: boolean; + case: "ipv6"; + } | { + /** + * `uri` specifies that the field value must be a valid URI, for example + * "https://example.com/foo/bar?baz=quux#frag". + * + * URI is defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). + * Zone Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)). + * + * If the field value isn't a valid URI, an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid URI + * string value = 1 [(buf.validate.field).string.uri = true]; + * } + * ``` + * + * @generated from field: bool uri = 17; + */ + value: boolean; + case: "uri"; + } | { + /** + * `uri_ref` specifies that the field value must be a valid URI Reference—either + * a URI such as "https://example.com/foo/bar?baz=quux#frag", or a Relative + * Reference such as "./foo/bar?query". + * + * URI, URI Reference, and Relative Reference are defined in the internet + * standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). Zone + * Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)). + * + * If the field value isn't a valid URI Reference, an error message will be + * generated. + * + * ```proto + * message MyString { + * // must be a valid URI Reference + * string value = 1 [(buf.validate.field).string.uri_ref = true]; + * } + * ``` + * + * @generated from field: bool uri_ref = 18; + */ + value: boolean; + case: "uriRef"; + } | { + /** + * `address` specifies that the field value must be either a valid hostname + * (for example, "example.com"), or a valid IP (v4 or v6) address (for example, + * "192.168.0.1", or "::1"). If the field value isn't a valid hostname or IP, + * an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid hostname, or ip address + * string value = 1 [(buf.validate.field).string.address = true]; + * } + * ``` + * + * @generated from field: bool address = 21; + */ + value: boolean; + case: "address"; + } | { + /** + * `uuid` specifies that the field value must be a valid UUID as defined by + * [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2). If the + * field value isn't a valid UUID, an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid UUID + * string value = 1 [(buf.validate.field).string.uuid = true]; + * } + * ``` + * + * @generated from field: bool uuid = 22; + */ + value: boolean; + case: "uuid"; + } | { + /** + * `tuuid` (trimmed UUID) specifies that the field value must be a valid UUID as + * defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2) with all dashes + * omitted. If the field value isn't a valid UUID without dashes, an error message + * will be generated. + * + * ```proto + * message MyString { + * // must be a valid trimmed UUID + * string value = 1 [(buf.validate.field).string.tuuid = true]; + * } + * ``` + * + * @generated from field: bool tuuid = 33; + */ + value: boolean; + case: "tuuid"; + } | { + /** + * `ip_with_prefixlen` specifies that the field value must be a valid IP + * (v4 or v6) address with prefix length—for example, "192.168.5.21/16" or + * "2001:0DB8:ABCD:0012::F1/64". If the field value isn't a valid IP with + * prefix length, an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid IP with prefix length + * string value = 1 [(buf.validate.field).string.ip_with_prefixlen = true]; + * } + * ``` + * + * @generated from field: bool ip_with_prefixlen = 26; + */ + value: boolean; + case: "ipWithPrefixlen"; + } | { + /** + * `ipv4_with_prefixlen` specifies that the field value must be a valid + * IPv4 address with prefix length—for example, "192.168.5.21/16". If the + * field value isn't a valid IPv4 address with prefix length, an error + * message will be generated. + * + * ```proto + * message MyString { + * // must be a valid IPv4 address with prefix length + * string value = 1 [(buf.validate.field).string.ipv4_with_prefixlen = true]; + * } + * ``` + * + * @generated from field: bool ipv4_with_prefixlen = 27; + */ + value: boolean; + case: "ipv4WithPrefixlen"; + } | { + /** + * `ipv6_with_prefixlen` specifies that the field value must be a valid + * IPv6 address with prefix length—for example, "2001:0DB8:ABCD:0012::F1/64". + * If the field value is not a valid IPv6 address with prefix length, + * an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid IPv6 address prefix length + * string value = 1 [(buf.validate.field).string.ipv6_with_prefixlen = true]; + * } + * ``` + * + * @generated from field: bool ipv6_with_prefixlen = 28; + */ + value: boolean; + case: "ipv6WithPrefixlen"; + } | { + /** + * `ip_prefix` specifies that the field value must be a valid IP (v4 or v6) + * prefix—for example, "192.168.0.0/16" or "2001:0DB8:ABCD:0012::0/64". + * + * The prefix must have all zeros for the unmasked bits. For example, + * "2001:0DB8:ABCD:0012::0/64" designates the left-most 64 bits for the + * prefix, and the remaining 64 bits must be zero. + * + * If the field value isn't a valid IP prefix, an error message will be + * generated. + * + * ```proto + * message MyString { + * // must be a valid IP prefix + * string value = 1 [(buf.validate.field).string.ip_prefix = true]; + * } + * ``` + * + * @generated from field: bool ip_prefix = 29; + */ + value: boolean; + case: "ipPrefix"; + } | { + /** + * `ipv4_prefix` specifies that the field value must be a valid IPv4 + * prefix, for example "192.168.0.0/16". + * + * The prefix must have all zeros for the unmasked bits. For example, + * "192.168.0.0/16" designates the left-most 16 bits for the prefix, + * and the remaining 16 bits must be zero. + * + * If the field value isn't a valid IPv4 prefix, an error message + * will be generated. + * + * ```proto + * message MyString { + * // must be a valid IPv4 prefix + * string value = 1 [(buf.validate.field).string.ipv4_prefix = true]; + * } + * ``` + * + * @generated from field: bool ipv4_prefix = 30; + */ + value: boolean; + case: "ipv4Prefix"; + } | { + /** + * `ipv6_prefix` specifies that the field value must be a valid IPv6 prefix—for + * example, "2001:0DB8:ABCD:0012::0/64". + * + * The prefix must have all zeros for the unmasked bits. For example, + * "2001:0DB8:ABCD:0012::0/64" designates the left-most 64 bits for the + * prefix, and the remaining 64 bits must be zero. + * + * If the field value is not a valid IPv6 prefix, an error message will be + * generated. + * + * ```proto + * message MyString { + * // must be a valid IPv6 prefix + * string value = 1 [(buf.validate.field).string.ipv6_prefix = true]; + * } + * ``` + * + * @generated from field: bool ipv6_prefix = 31; + */ + value: boolean; + case: "ipv6Prefix"; + } | { + /** + * `host_and_port` specifies that the field value must be a valid host/port + * pair—for example, "example.com:8080". + * + * The host can be one of: + * - An IPv4 address in dotted decimal format—for example, "192.168.5.21". + * - An IPv6 address enclosed in square brackets—for example, "[2001:0DB8:ABCD:0012::F1]". + * - A hostname—for example, "example.com". + * + * The port is separated by a colon. It must be non-empty, with a decimal number + * in the range of 0-65535, inclusive. + * + * @generated from field: bool host_and_port = 32; + */ + value: boolean; + case: "hostAndPort"; + } | { + /** + * `ulid` specifies that the field value must be a valid ULID (Universally Unique + * Lexicographically Sortable Identifier) as defined by the [ULID specification](https://github.com/ulid/spec). + * If the field value isn't a valid ULID, an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid ULID + * string value = 1 [(buf.validate.field).string.ulid = true]; + * } + * ``` + * + * @generated from field: bool ulid = 35; + */ + value: boolean; + case: "ulid"; + } | { + /** + * `protobuf_fqn` specifies that the field value must be a valid fully-qualified + * Protobuf name as defined by the [Protobuf Language Specification](https://protobuf.com/docs/language-spec). + * + * A fully-qualified Protobuf name is a dot-separated list of Protobuf identifiers, + * where each identifier starts with a letter or underscore and is followed by zero or + * more letters, underscores, or digits. + * + * Examples: "buf.validate", "google.protobuf.Timestamp", "my_package.MyMessage". + * + * Note: historically, fully-qualified Protobuf names were represented with a leading + * dot (for example, ".buf.validate.StringRules"). Modern Protobuf does not use the + * leading dot, and most fully-qualified names are represented without it. Use + * `protobuf_dot_fqn` if a leading dot is required. + * + * If the field value isn't a valid fully-qualified Protobuf name, an error message + * will be generated. + * + * ```proto + * message MyString { + * // value must be a valid fully-qualified Protobuf name + * string value = 1 [(buf.validate.field).string.protobuf_fqn = true]; + * } + * ``` + * + * @generated from field: bool protobuf_fqn = 37; + */ + value: boolean; + case: "protobufFqn"; + } | { + /** + * `protobuf_dot_fqn` specifies that the field value must be a valid fully-qualified + * Protobuf name with a leading dot, as defined by the + * [Protobuf Language Specification](https://protobuf.com/docs/language-spec). + * + * A fully-qualified Protobuf name with a leading dot is a dot followed by a + * dot-separated list of Protobuf identifiers, where each identifier starts with a + * letter or underscore and is followed by zero or more letters, underscores, or + * digits. + * + * Examples: ".buf.validate", ".google.protobuf.Timestamp", ".my_package.MyMessage". + * + * Note: this is the historical representation of fully-qualified Protobuf names, + * where a leading dot denotes an absolute reference. Modern Protobuf does not use + * the leading dot, and most fully-qualified names are represented without it. Most + * users will want to use `protobuf_fqn` instead. + * + * If the field value isn't a valid fully-qualified Protobuf name with a leading dot, + * an error message will be generated. + * + * ```proto + * message MyString { + * // value must be a valid fully-qualified Protobuf name with a leading dot + * string value = 1 [(buf.validate.field).string.protobuf_dot_fqn = true]; + * } + * ``` + * + * @generated from field: bool protobuf_dot_fqn = 38; + */ + value: boolean; + case: "protobufDotFqn"; + } | { + /** + * `well_known_regex` specifies a common well-known pattern + * defined as a regex. If the field value doesn't match the well-known + * regex, an error message will be generated. + * + * ```proto + * message MyString { + * // must be a valid HTTP header value + * string value = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_VALUE]; + * } + * ``` + * + * #### KnownRegex + * + * `well_known_regex` contains some well-known patterns. + * + * | Name | Number | Description | + * |-------------------------------|--------|-------------------------------------------| + * | KNOWN_REGEX_UNSPECIFIED | 0 | | + * | KNOWN_REGEX_HTTP_HEADER_NAME | 1 | HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2) | + * | KNOWN_REGEX_HTTP_HEADER_VALUE | 2 | HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4) | + * + * @generated from field: buf.validate.KnownRegex well_known_regex = 24; + */ + value: KnownRegex; + case: "wellKnownRegex"; + } | { case: undefined; value?: undefined }; + + /** + * This applies to regexes `HTTP_HEADER_NAME` and `HTTP_HEADER_VALUE` to + * enable strict header validation. By default, this is true, and HTTP header + * validations are [RFC-compliant](https://datatracker.ietf.org/doc/html/rfc7230#section-3). Setting to false will enable looser + * validations that only disallow `\r\n\0` characters, which can be used to + * bypass header matching rules. + * + * ```proto + * message MyString { + * // The field `value` must have be a valid HTTP headers, but not enforced with strict rules. + * string value = 1 [(buf.validate.field).string.strict = false]; + * } + * ``` + * + * @generated from field: optional bool strict = 25; + */ + strict: boolean; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyString { + * string value = 1 [ + * (buf.validate.field).string.example = "hello", + * (buf.validate.field).string.example = "world" + * ]; + * } + * ``` + * + * @generated from field: repeated string example = 34; + */ + example: string[]; +}; + +/** + * Describes the message buf.validate.StringRules. + * Use `create(StringRulesSchema)` to create a new message. + */ +export const StringRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 19); + +/** + * BytesRules describe the rules applied to `bytes` values. These rules + * may also be applied to the `google.protobuf.BytesValue` Well-Known-Type. + * + * @generated from message buf.validate.BytesRules + */ +export type BytesRules = Message<"buf.validate.BytesRules"> & { + /** + * `const` requires the field value to exactly match the specified bytes + * value. If the field value doesn't match, an error message is generated. + * + * ```proto + * message MyBytes { + * // must be "\x01\x02\x03\x04" + * bytes value = 1 [(buf.validate.field).bytes.const = "\x01\x02\x03\x04"]; + * } + * ``` + * + * @generated from field: optional bytes const = 1; + */ + const: Uint8Array; + + /** + * `len` requires the field value to have the specified length in bytes. + * If the field value doesn't match, an error message is generated. + * + * ```proto + * message MyBytes { + * // value length must be 4 bytes. + * optional bytes value = 1 [(buf.validate.field).bytes.len = 4]; + * } + * ``` + * + * @generated from field: optional uint64 len = 13; + */ + len: bigint; + + /** + * `min_len` requires the field value to have at least the specified minimum + * length in bytes. + * If the field value doesn't meet the requirement, an error message is generated. + * + * ```proto + * message MyBytes { + * // value length must be at least 2 bytes. + * optional bytes value = 1 [(buf.validate.field).bytes.min_len = 2]; + * } + * ``` + * + * @generated from field: optional uint64 min_len = 2; + */ + minLen: bigint; + + /** + * `max_len` requires the field value to have at most the specified maximum + * length in bytes. + * If the field value exceeds the requirement, an error message is generated. + * + * ```proto + * message MyBytes { + * // must be at most 6 bytes. + * optional bytes value = 1 [(buf.validate.field).bytes.max_len = 6]; + * } + * ``` + * + * @generated from field: optional uint64 max_len = 3; + */ + maxLen: bigint; + + /** + * `pattern` requires the field value to match the specified regular + * expression ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)). + * The value of the field must be valid UTF-8 or validation will fail with a + * runtime error. + * If the field value doesn't match the pattern, an error message is generated. + * + * ```proto + * message MyBytes { + * // value must match regex pattern "^[a-zA-Z0-9]+$". + * optional bytes value = 1 [(buf.validate.field).bytes.pattern = "^[a-zA-Z0-9]+$"]; + * } + * ``` + * + * @generated from field: optional string pattern = 4; + */ + pattern: string; + + /** + * `prefix` requires the field value to have the specified bytes at the + * beginning of the string. + * If the field value doesn't meet the requirement, an error message is generated. + * + * ```proto + * message MyBytes { + * // value does not have prefix \x01\x02 + * optional bytes value = 1 [(buf.validate.field).bytes.prefix = "\x01\x02"]; + * } + * ``` + * + * @generated from field: optional bytes prefix = 5; + */ + prefix: Uint8Array; + + /** + * `suffix` requires the field value to have the specified bytes at the end + * of the string. + * If the field value doesn't meet the requirement, an error message is generated. + * + * ```proto + * message MyBytes { + * // value does not have suffix \x03\x04 + * optional bytes value = 1 [(buf.validate.field).bytes.suffix = "\x03\x04"]; + * } + * ``` + * + * @generated from field: optional bytes suffix = 6; + */ + suffix: Uint8Array; + + /** + * `contains` requires the field value to have the specified bytes anywhere in + * the string. + * If the field value doesn't meet the requirement, an error message is generated. + * + * ```proto + * message MyBytes { + * // value does not contain \x02\x03 + * optional bytes value = 1 [(buf.validate.field).bytes.contains = "\x02\x03"]; + * } + * ``` + * + * @generated from field: optional bytes contains = 7; + */ + contains: Uint8Array; + + /** + * `in` requires the field value to be equal to one of the specified + * values. If the field value doesn't match any of the specified values, an + * error message is generated. + * + * ```proto + * message MyBytes { + * // value must in ["\x01\x02", "\x02\x03", "\x03\x04"] + * optional bytes value = 1 [(buf.validate.field).bytes = { in: ["\x01\x02", "\x02\x03", "\x03\x04"] }]; + * } + * ``` + * + * @generated from field: repeated bytes in = 8; + */ + in: Uint8Array[]; + + /** + * `not_in` requires the field value to be not equal to any of the specified + * values. + * If the field value matches any of the specified values, an error message is + * generated. + * + * ```proto + * message MyBytes { + * // value must not in ["\x01\x02", "\x02\x03", "\x03\x04"] + * optional bytes value = 1 [(buf.validate.field).bytes = { not_in: ["\x01\x02", "\x02\x03", "\x03\x04"] }]; + * } + * ``` + * + * @generated from field: repeated bytes not_in = 9; + */ + notIn: Uint8Array[]; + + /** + * WellKnown rules provide advanced rules against common byte + * patterns + * + * @generated from oneof buf.validate.BytesRules.well_known + */ + wellKnown: { + /** + * `ip` ensures that the field `value` is a valid IP address (v4 or v6) in byte format. + * If the field value doesn't meet this rule, an error message is generated. + * + * ```proto + * message MyBytes { + * // must be a valid IP address + * optional bytes value = 1 [(buf.validate.field).bytes.ip = true]; + * } + * ``` + * + * @generated from field: bool ip = 10; + */ + value: boolean; + case: "ip"; + } | { + /** + * `ipv4` ensures that the field `value` is a valid IPv4 address in byte format. + * If the field value doesn't meet this rule, an error message is generated. + * + * ```proto + * message MyBytes { + * // must be a valid IPv4 address + * optional bytes value = 1 [(buf.validate.field).bytes.ipv4 = true]; + * } + * ``` + * + * @generated from field: bool ipv4 = 11; + */ + value: boolean; + case: "ipv4"; + } | { + /** + * `ipv6` ensures that the field `value` is a valid IPv6 address in byte format. + * If the field value doesn't meet this rule, an error message is generated. + * ```proto + * message MyBytes { + * // must be a valid IPv6 address + * optional bytes value = 1 [(buf.validate.field).bytes.ipv6 = true]; + * } + * ``` + * + * @generated from field: bool ipv6 = 12; + */ + value: boolean; + case: "ipv6"; + } | { + /** + * `uuid` ensures that the field value encodes 128-bit UUID data as defined + * by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2). + * The field must contain exactly 16 bytes representing the UUID. If the + * field value isn't a valid UUID, an error message will be generated. + * + * ```proto + * message MyBytes { + * // must be a valid UUID + * optional bytes value = 1 [(buf.validate.field).bytes.uuid = true]; + * } + * ``` + * + * @generated from field: bool uuid = 15; + */ + value: boolean; + case: "uuid"; + } | { case: undefined; value?: undefined }; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyBytes { + * bytes value = 1 [ + * (buf.validate.field).bytes.example = "\x01\x02", + * (buf.validate.field).bytes.example = "\x02\x03" + * ]; + * } + * ``` + * + * @generated from field: repeated bytes example = 14; + */ + example: Uint8Array[]; +}; + +/** + * Describes the message buf.validate.BytesRules. + * Use `create(BytesRulesSchema)` to create a new message. + */ +export const BytesRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 20); + +/** + * EnumRules describe the rules applied to `enum` values. + * + * @generated from message buf.validate.EnumRules + */ +export type EnumRules = Message<"buf.validate.EnumRules"> & { + /** + * `const` requires the field value to exactly match the specified enum value. + * If the field value doesn't match, an error message is generated. + * + * ```proto + * enum MyEnum { + * MY_ENUM_UNSPECIFIED = 0; + * MY_ENUM_VALUE1 = 1; + * MY_ENUM_VALUE2 = 2; + * } + * + * message MyMessage { + * // The field `value` must be exactly MY_ENUM_VALUE1. + * MyEnum value = 1 [(buf.validate.field).enum.const = 1]; + * } + * ``` + * + * @generated from field: optional int32 const = 1; + */ + const: number; + + /** + * `defined_only` requires the field value to be one of the defined values for + * this enum, failing on any undefined value. + * + * ```proto + * enum MyEnum { + * MY_ENUM_UNSPECIFIED = 0; + * MY_ENUM_VALUE1 = 1; + * MY_ENUM_VALUE2 = 2; + * } + * + * message MyMessage { + * // The field `value` must be a defined value of MyEnum. + * MyEnum value = 1 [(buf.validate.field).enum.defined_only = true]; + * } + * ``` + * + * @generated from field: optional bool defined_only = 2; + */ + definedOnly: boolean; + + /** + * `in` requires the field value to be equal to one of the + * specified enum values. If the field value doesn't match any of the + * specified values, an error message is generated. + * + * ```proto + * enum MyEnum { + * MY_ENUM_UNSPECIFIED = 0; + * MY_ENUM_VALUE1 = 1; + * MY_ENUM_VALUE2 = 2; + * } + * + * message MyMessage { + * // The field `value` must be equal to one of the specified values. + * MyEnum value = 1 [(buf.validate.field).enum = { in: [1, 2]}]; + * } + * ``` + * + * @generated from field: repeated int32 in = 3; + */ + in: number[]; + + /** + * `not_in` requires the field value to be not equal to any of the + * specified enum values. If the field value matches one of the specified + * values, an error message is generated. + * + * ```proto + * enum MyEnum { + * MY_ENUM_UNSPECIFIED = 0; + * MY_ENUM_VALUE1 = 1; + * MY_ENUM_VALUE2 = 2; + * } + * + * message MyMessage { + * // The field `value` must not be equal to any of the specified values. + * MyEnum value = 1 [(buf.validate.field).enum = { not_in: [1, 2]}]; + * } + * ``` + * + * @generated from field: repeated int32 not_in = 4; + */ + notIn: number[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * enum MyEnum { + * MY_ENUM_UNSPECIFIED = 0; + * MY_ENUM_VALUE1 = 1; + * MY_ENUM_VALUE2 = 2; + * } + * + * message MyMessage { + * MyEnum value = 1 [ + * (buf.validate.field).enum.example = 1, + * (buf.validate.field).enum.example = 2 + * ]; + * } + * ``` + * + * @generated from field: repeated int32 example = 5; + */ + example: number[]; +}; + +/** + * Describes the message buf.validate.EnumRules. + * Use `create(EnumRulesSchema)` to create a new message. + */ +export const EnumRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 21); + +/** + * RepeatedRules describe the rules applied to `repeated` values. + * + * @generated from message buf.validate.RepeatedRules + */ +export type RepeatedRules = Message<"buf.validate.RepeatedRules"> & { + /** + * `min_items` requires that this field must contain at least the specified + * minimum number of items. + * + * Note that `min_items = 1` is equivalent to setting a field as `required`. + * + * ```proto + * message MyRepeated { + * // value must contain at least 2 items + * repeated string value = 1 [(buf.validate.field).repeated.min_items = 2]; + * } + * ``` + * + * @generated from field: optional uint64 min_items = 1; + */ + minItems: bigint; + + /** + * `max_items` denotes that this field must not exceed a + * certain number of items as the upper limit. If the field contains more + * items than specified, an error message will be generated, requiring the + * field to maintain no more than the specified number of items. + * + * ```proto + * message MyRepeated { + * // value must contain no more than 3 item(s) + * repeated string value = 1 [(buf.validate.field).repeated.max_items = 3]; + * } + * ``` + * + * @generated from field: optional uint64 max_items = 2; + */ + maxItems: bigint; + + /** + * `unique` indicates that all elements in this field must + * be unique. This rule is strictly applicable to scalar and enum + * types, with message types not being supported. + * + * ```proto + * message MyRepeated { + * // repeated value must contain unique items + * repeated string value = 1 [(buf.validate.field).repeated.unique = true]; + * } + * ``` + * + * @generated from field: optional bool unique = 3; + */ + unique: boolean; + + /** + * `items` details the rules to be applied to each item + * in the field. Even for repeated message fields, validation is executed + * against each item unless `ignore` is specified. + * + * ```proto + * message MyRepeated { + * // The items in the field `value` must follow the specified rules. + * repeated string value = 1 [(buf.validate.field).repeated.items = { + * string: { + * min_len: 3 + * max_len: 10 + * } + * }]; + * } + * ``` + * + * Note that the `required` rule does not apply. Repeated items + * cannot be unset. + * + * @generated from field: optional buf.validate.FieldRules items = 4; + */ + items?: FieldRules; +}; + +/** + * Describes the message buf.validate.RepeatedRules. + * Use `create(RepeatedRulesSchema)` to create a new message. + */ +export const RepeatedRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 22); + +/** + * MapRules describe the rules applied to `map` values. + * + * @generated from message buf.validate.MapRules + */ +export type MapRules = Message<"buf.validate.MapRules"> & { + /** + * Specifies the minimum number of key-value pairs allowed. If the field has + * fewer key-value pairs than specified, an error message is generated. + * + * ```proto + * message MyMap { + * // The field `value` must have at least 2 key-value pairs. + * map value = 1 [(buf.validate.field).map.min_pairs = 2]; + * } + * ``` + * + * @generated from field: optional uint64 min_pairs = 1; + */ + minPairs: bigint; + + /** + * Specifies the maximum number of key-value pairs allowed. If the field has + * more key-value pairs than specified, an error message is generated. + * + * ```proto + * message MyMap { + * // The field `value` must have at most 3 key-value pairs. + * map value = 1 [(buf.validate.field).map.max_pairs = 3]; + * } + * ``` + * + * @generated from field: optional uint64 max_pairs = 2; + */ + maxPairs: bigint; + + /** + * Specifies the rules to be applied to each key in the field. + * + * ```proto + * message MyMap { + * // The keys in the field `value` must follow the specified rules. + * map value = 1 [(buf.validate.field).map.keys = { + * string: { + * min_len: 3 + * max_len: 10 + * } + * }]; + * } + * ``` + * + * Note that the `required` rule does not apply. Map keys cannot be unset. + * + * @generated from field: optional buf.validate.FieldRules keys = 4; + */ + keys?: FieldRules; + + /** + * Specifies the rules to be applied to the value of each key in the + * field. Message values will still have their validations evaluated unless + * `ignore` is specified. + * + * ```proto + * message MyMap { + * // The values in the field `value` must follow the specified rules. + * map value = 1 [(buf.validate.field).map.values = { + * string: { + * min_len: 5 + * max_len: 20 + * } + * }]; + * } + * ``` + * Note that the `required` rule does not apply. Map values cannot be unset. + * + * @generated from field: optional buf.validate.FieldRules values = 5; + */ + values?: FieldRules; +}; + +/** + * Describes the message buf.validate.MapRules. + * Use `create(MapRulesSchema)` to create a new message. + */ +export const MapRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 23); + +/** + * AnyRules describe rules applied exclusively to the `google.protobuf.Any` well-known type. + * + * @generated from message buf.validate.AnyRules + */ +export type AnyRules = Message<"buf.validate.AnyRules"> & { + /** + * `in` requires the field's `type_url` to be equal to one of the + * specified values. If it doesn't match any of the specified values, an error + * message is generated. + * + * ```proto + * message MyAny { + * // The `value` field must have a `type_url` equal to one of the specified values. + * google.protobuf.Any value = 1 [(buf.validate.field).any = { + * in: ["type.googleapis.com/MyType1", "type.googleapis.com/MyType2"] + * }]; + * } + * ``` + * + * @generated from field: repeated string in = 2; + */ + in: string[]; + + /** + * `not_in` requires the field's type_url to be not equal to any of the specified values. If it matches any of the specified values, an error message is generated. + * + * ```proto + * message MyAny { + * // The `value` field must not have a `type_url` equal to any of the specified values. + * google.protobuf.Any value = 1 [(buf.validate.field).any = { + * not_in: ["type.googleapis.com/ForbiddenType1", "type.googleapis.com/ForbiddenType2"] + * }]; + * } + * ``` + * + * @generated from field: repeated string not_in = 3; + */ + notIn: string[]; +}; + +/** + * Describes the message buf.validate.AnyRules. + * Use `create(AnyRulesSchema)` to create a new message. + */ +export const AnyRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 24); + +/** + * DurationRules describe the rules applied exclusively to the `google.protobuf.Duration` well-known type. + * + * @generated from message buf.validate.DurationRules + */ +export type DurationRules = Message<"buf.validate.DurationRules"> & { + /** + * `const` dictates that the field must match the specified value of the `google.protobuf.Duration` type exactly. + * If the field's value deviates from the specified value, an error message + * will be generated. + * + * ```proto + * message MyDuration { + * // value must equal 5s + * google.protobuf.Duration value = 1 [(buf.validate.field).duration.const = { seconds: 5 }]; + * } + * ``` + * + * @generated from field: optional google.protobuf.Duration const = 2; + */ + const?: Duration; + + /** + * @generated from oneof buf.validate.DurationRules.less_than + */ + lessThan: { + /** + * `lt` stipulates that the field must be less than the specified value of the `google.protobuf.Duration` type, + * exclusive. If the field's value is greater than or equal to the specified + * value, an error message will be generated. + * + * ```proto + * message MyDuration { + * // must be less than 5s + * google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = { seconds: 5 }]; + * } + * ``` + * + * @generated from field: google.protobuf.Duration lt = 3; + */ + value: Duration; + case: "lt"; + } | { + /** + * `lte` indicates that the field must be less than or equal to the specified + * value of the `google.protobuf.Duration` type, inclusive. If the field's value is greater than the specified value, + * an error message will be generated. + * + * ```proto + * message MyDuration { + * // must be less than or equal to 10s + * google.protobuf.Duration value = 1 [(buf.validate.field).duration.lte = { seconds: 10 }]; + * } + * ``` + * + * @generated from field: google.protobuf.Duration lte = 4; + */ + value: Duration; + case: "lte"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.DurationRules.greater_than + */ + greaterThan: { + /** + * `gt` requires the duration field value to be greater than the specified + * value (exclusive). If the value of `gt` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyDuration { + * // duration must be greater than 5s [duration.gt] + * google.protobuf.Duration value = 1 [(buf.validate.field).duration.gt = { seconds: 5 }]; + * + * // duration must be greater than 5s and less than 10s [duration.gt_lt] + * google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gt: { seconds: 5 }, lt: { seconds: 10 } }]; + * + * // duration must be greater than 10s or less than 5s [duration.gt_lt_exclusive] + * google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gt: { seconds: 10 }, lt: { seconds: 5 } }]; + * } + * ``` + * + * @generated from field: google.protobuf.Duration gt = 5; + */ + value: Duration; + case: "gt"; + } | { + /** + * `gte` requires the duration field value to be greater than or equal to the + * specified value (exclusive). If the value of `gte` is larger than a + * specified `lt` or `lte`, the range is reversed, and the field value must + * be outside the specified range. If the field value doesn't meet the + * required conditions, an error message is generated. + * + * ```proto + * message MyDuration { + * // duration must be greater than or equal to 5s [duration.gte] + * google.protobuf.Duration value = 1 [(buf.validate.field).duration.gte = { seconds: 5 }]; + * + * // duration must be greater than or equal to 5s and less than 10s [duration.gte_lt] + * google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gte: { seconds: 5 }, lt: { seconds: 10 } }]; + * + * // duration must be greater than or equal to 10s or less than 5s [duration.gte_lt_exclusive] + * google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gte: { seconds: 10 }, lt: { seconds: 5 } }]; + * } + * ``` + * + * @generated from field: google.protobuf.Duration gte = 6; + */ + value: Duration; + case: "gte"; + } | { case: undefined; value?: undefined }; + + /** + * `in` asserts that the field must be equal to one of the specified values of the `google.protobuf.Duration` type. + * If the field's value doesn't correspond to any of the specified values, + * an error message will be generated. + * + * ```proto + * message MyDuration { + * // must be in list [1s, 2s, 3s] + * google.protobuf.Duration value = 1 [(buf.validate.field).duration = { + * in: [{ seconds: 1 }, { seconds: 2 }, { seconds: 3 }] + * }]; + * } + * ``` + * + * @generated from field: repeated google.protobuf.Duration in = 7; + */ + in: Duration[]; + + /** + * `not_in` denotes that the field must not be equal to + * any of the specified values of the `google.protobuf.Duration` type. + * If the field's value matches any of these values, an error message will be + * generated. + * + * ```proto + * message MyDuration { + * // value must not be in list [1s, 2s, 3s] + * google.protobuf.Duration value = 1 [(buf.validate.field).duration = { + * not_in: [{ seconds: 1 }, { seconds: 2 }, { seconds: 3 }] + * }]; + * } + * ``` + * + * @generated from field: repeated google.protobuf.Duration not_in = 8; + */ + notIn: Duration[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyDuration { + * google.protobuf.Duration value = 1 [ + * (buf.validate.field).duration.example = { seconds: 1 }, + * (buf.validate.field).duration.example = { seconds: 2 } + * ]; + * } + * ``` + * + * @generated from field: repeated google.protobuf.Duration example = 9; + */ + example: Duration[]; +}; + +/** + * Describes the message buf.validate.DurationRules. + * Use `create(DurationRulesSchema)` to create a new message. + */ +export const DurationRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 25); + +/** + * FieldMaskRules describe rules applied exclusively to the `google.protobuf.FieldMask` well-known type. + * + * @generated from message buf.validate.FieldMaskRules + */ +export type FieldMaskRules = Message<"buf.validate.FieldMaskRules"> & { + /** + * `const` dictates that the field must match the specified value of the `google.protobuf.FieldMask` type exactly. + * If the field's value deviates from the specified value, an error message + * will be generated. + * + * ```proto + * message MyFieldMask { + * // value must equal ["a"] + * google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask.const = { + * paths: ["a"] + * }]; + * } + * ``` + * + * @generated from field: optional google.protobuf.FieldMask const = 1; + */ + const?: FieldMask; + + /** + * `in` requires the field value to only contain paths matching specified + * values or their subpaths. + * If any of the field value's paths doesn't match the rule, + * an error message is generated. + * See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask + * + * ```proto + * message MyFieldMask { + * // The `value` FieldMask must only contain paths listed in `in`. + * google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = { + * in: ["a", "b", "c.a"] + * }]; + * } + * ``` + * + * @generated from field: repeated string in = 2; + */ + in: string[]; + + /** + * `not_in` requires the field value to not contain paths matching specified + * values or their subpaths. + * If any of the field value's paths matches the rule, + * an error message is generated. + * See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask + * + * ```proto + * message MyFieldMask { + * // The `value` FieldMask shall not contain paths listed in `not_in`. + * google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = { + * not_in: ["forbidden", "immutable", "c.a"] + * }]; + * } + * ``` + * + * @generated from field: repeated string not_in = 3; + */ + notIn: string[]; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyFieldMask { + * google.protobuf.FieldMask value = 1 [ + * (buf.validate.field).field_mask.example = { paths: ["a", "b"] }, + * (buf.validate.field).field_mask.example = { paths: ["c.a", "d"] } + * ]; + * } + * ``` + * + * @generated from field: repeated google.protobuf.FieldMask example = 4; + */ + example: FieldMask[]; +}; + +/** + * Describes the message buf.validate.FieldMaskRules. + * Use `create(FieldMaskRulesSchema)` to create a new message. + */ +export const FieldMaskRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 26); + +/** + * TimestampRules describe the rules applied exclusively to the `google.protobuf.Timestamp` well-known type. + * + * @generated from message buf.validate.TimestampRules + */ +export type TimestampRules = Message<"buf.validate.TimestampRules"> & { + /** + * `const` dictates that this field, of the `google.protobuf.Timestamp` type, must exactly match the specified value. If the field value doesn't correspond to the specified timestamp, an error message will be generated. + * + * ```proto + * message MyTimestamp { + * // value must equal 2023-05-03T10:00:00Z + * google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.const = {seconds: 1727998800}]; + * } + * ``` + * + * @generated from field: optional google.protobuf.Timestamp const = 2; + */ + const?: Timestamp; + + /** + * @generated from oneof buf.validate.TimestampRules.less_than + */ + lessThan: { + /** + * `lt` requires the timestamp field value to be less than the specified value (field < value). If the field value doesn't meet the required conditions, an error message is generated. + * + * ```proto + * message MyTimestamp { + * // timestamp must be less than '2023-01-01T00:00:00Z' [timestamp.lt] + * google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.lt = { seconds: 1672444800 }]; + * } + * ``` + * + * @generated from field: google.protobuf.Timestamp lt = 3; + */ + value: Timestamp; + case: "lt"; + } | { + /** + * `lte` requires the timestamp field value to be less than or equal to the specified value (field <= value). If the field value doesn't meet the required conditions, an error message is generated. + * + * ```proto + * message MyTimestamp { + * // timestamp must be less than or equal to '2023-05-14T00:00:00Z' [timestamp.lte] + * google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.lte = { seconds: 1678867200 }]; + * } + * ``` + * + * @generated from field: google.protobuf.Timestamp lte = 4; + */ + value: Timestamp; + case: "lte"; + } | { + /** + * `lt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be less than the current time. `lt_now` can only be used with the `within` rule. + * + * ```proto + * message MyTimestamp { + * // must be less than now + * google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true]; + * } + * ``` + * + * @generated from field: bool lt_now = 7; + */ + value: boolean; + case: "ltNow"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof buf.validate.TimestampRules.greater_than + */ + greaterThan: { + /** + * `gt` requires the timestamp field value to be greater than the specified + * value (exclusive). If the value of `gt` is larger than a specified `lt` + * or `lte`, the range is reversed, and the field value must be outside the + * specified range. If the field value doesn't meet the required conditions, + * an error message is generated. + * + * ```proto + * message MyTimestamp { + * // timestamp must be greater than '2023-01-01T00:00:00Z' [timestamp.gt] + * google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gt = { seconds: 1672444800 }]; + * + * // timestamp must be greater than '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gt_lt] + * google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gt: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }]; + * + * // timestamp must be greater than '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gt_lt_exclusive] + * google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gt: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }]; + * } + * ``` + * + * @generated from field: google.protobuf.Timestamp gt = 5; + */ + value: Timestamp; + case: "gt"; + } | { + /** + * `gte` requires the timestamp field value to be greater than or equal to the + * specified value (exclusive). If the value of `gte` is larger than a + * specified `lt` or `lte`, the range is reversed, and the field value + * must be outside the specified range. If the field value doesn't meet + * the required conditions, an error message is generated. + * + * ```proto + * message MyTimestamp { + * // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' [timestamp.gte] + * google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gte = { seconds: 1672444800 }]; + * + * // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gte_lt] + * google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gte: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }]; + * + * // timestamp must be greater than or equal to '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gte_lt_exclusive] + * google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gte: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }]; + * } + * ``` + * + * @generated from field: google.protobuf.Timestamp gte = 6; + */ + value: Timestamp; + case: "gte"; + } | { + /** + * `gt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be greater than the current time. `gt_now` can only be used with the `within` rule. + * + * ```proto + * message MyTimestamp { + * // must be greater than now + * google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.gt_now = true]; + * } + * ``` + * + * @generated from field: bool gt_now = 8; + */ + value: boolean; + case: "gtNow"; + } | { case: undefined; value?: undefined }; + + /** + * `within` specifies that this field, of the `google.protobuf.Timestamp` type, must be within the specified duration of the current time. If the field value isn't within the duration, an error message is generated. + * + * ```proto + * message MyTimestamp { + * // must be within 1 hour of now + * google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.within = {seconds: 3600}]; + * } + * ``` + * + * @generated from field: optional google.protobuf.Duration within = 9; + */ + within?: Duration; + + /** + * `example` specifies values that the field may have. These values SHOULD + * conform to other rules. `example` values will not impact validation + * but may be used as helpful guidance on how to populate the given field. + * + * ```proto + * message MyTimestamp { + * google.protobuf.Timestamp value = 1 [ + * (buf.validate.field).timestamp.example = { seconds: 1672444800 }, + * (buf.validate.field).timestamp.example = { seconds: 1672531200 } + * ]; + * } + * ``` + * + * @generated from field: repeated google.protobuf.Timestamp example = 10; + */ + example: Timestamp[]; +}; + +/** + * Describes the message buf.validate.TimestampRules. + * Use `create(TimestampRulesSchema)` to create a new message. + */ +export const TimestampRulesSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 27); + +/** + * `Violations` is a collection of `Violation` messages. This message type is returned by + * Protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules. + * Each individual violation is represented by a `Violation` message. + * + * @generated from message buf.validate.Violations + */ +export type Violations = Message<"buf.validate.Violations"> & { + /** + * `violations` is a repeated field that contains all the `Violation` messages corresponding to the violations detected. + * + * @generated from field: repeated buf.validate.Violation violations = 1; + */ + violations: Violation[]; +}; + +/** + * Describes the message buf.validate.Violations. + * Use `create(ViolationsSchema)` to create a new message. + */ +export const ViolationsSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 28); + +/** + * `Violation` represents a single instance where a validation rule, expressed + * as a `Rule`, was not met. It provides information about the field that + * caused the violation, the specific rule that wasn't fulfilled, and a + * human-readable error message. + * + * For example, consider the following message: + * + * ```proto + * message User { + * int32 age = 1 [(buf.validate.field).cel = { + * id: "user.age", + * expression: "this < 18 ? 'User must be at least 18 years old' : ''", + * }]; + * } + * ``` + * + * It could produce the following violation: + * + * ```json + * { + * "ruleId": "user.age", + * "message": "User must be at least 18 years old", + * "field": { + * "elements": [ + * { + * "fieldNumber": 1, + * "fieldName": "age", + * "fieldType": "TYPE_INT32" + * } + * ] + * }, + * "rule": { + * "elements": [ + * { + * "fieldNumber": 23, + * "fieldName": "cel", + * "fieldType": "TYPE_MESSAGE", + * "index": "0" + * } + * ] + * } + * } + * ``` + * + * @generated from message buf.validate.Violation + */ +export type Violation = Message<"buf.validate.Violation"> & { + /** + * `field` is a machine-readable path to the field that failed validation. + * This could be a nested field, in which case the path will include all the parent fields leading to the actual field that caused the violation. + * + * For example, consider the following message: + * + * ```proto + * message Message { + * bool a = 1 [(buf.validate.field).required = true]; + * } + * ``` + * + * It could produce the following violation: + * + * ```textproto + * violation { + * field { element { field_number: 1, field_name: "a", field_type: 8 } } + * ... + * } + * ``` + * + * @generated from field: optional buf.validate.FieldPath field = 5; + */ + field?: FieldPath; + + /** + * `rule` is a machine-readable path that points to the specific rule that failed validation. + * This will be a nested field starting from the FieldRules of the field that failed validation. + * For custom rules, this will provide the path of the rule, e.g. `cel[0]`. + * + * For example, consider the following message: + * + * ```proto + * message Message { + * bool a = 1 [(buf.validate.field).required = true]; + * bool b = 2 [(buf.validate.field).cel = { + * id: "custom_rule", + * expression: "!this ? 'b must be true': ''" + * }]; + * } + * ``` + * + * It could produce the following violations: + * + * ```textproto + * violation { + * rule { element { field_number: 25, field_name: "required", field_type: 8 } } + * ... + * } + * violation { + * rule { element { field_number: 23, field_name: "cel", field_type: 11, index: 0 } } + * ... + * } + * ``` + * + * @generated from field: optional buf.validate.FieldPath rule = 6; + */ + rule?: FieldPath; + + /** + * `rule_id` is the unique identifier of the `Rule` that was not fulfilled. + * This is the same `id` that was specified in the `Rule` message, allowing easy tracing of which rule was violated. + * + * @generated from field: optional string rule_id = 2; + */ + ruleId: string; + + /** + * `message` is a human-readable error message that describes the nature of the violation. + * This can be the default error message from the violated `Rule`, or it can be a custom message that gives more context about the violation. + * + * @generated from field: optional string message = 3; + */ + message: string; + + /** + * `for_key` indicates whether the violation was caused by a map key, rather than a value. + * + * @generated from field: optional bool for_key = 4; + */ + forKey: boolean; +}; + +/** + * Describes the message buf.validate.Violation. + * Use `create(ViolationSchema)` to create a new message. + */ +export const ViolationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 29); + +/** + * `FieldPath` provides a path to a nested protobuf field. + * + * This message provides enough information to render a dotted field path even without protobuf descriptors. + * It also provides enough information to resolve a nested field through unknown wire data. + * + * @generated from message buf.validate.FieldPath + */ +export type FieldPath = Message<"buf.validate.FieldPath"> & { + /** + * `elements` contains each element of the path, starting from the root and recursing downward. + * + * @generated from field: repeated buf.validate.FieldPathElement elements = 1; + */ + elements: FieldPathElement[]; +}; + +/** + * Describes the message buf.validate.FieldPath. + * Use `create(FieldPathSchema)` to create a new message. + */ +export const FieldPathSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 30); + +/** + * `FieldPathElement` provides enough information to nest through a single protobuf field. + * + * If the selected field is a map or repeated field, the `subscript` value selects a specific element from it. + * A path that refers to a value nested under a map key or repeated field index will have a `subscript` value. + * The `field_type` field allows unambiguous resolution of a field even if descriptors are not available. + * + * @generated from message buf.validate.FieldPathElement + */ +export type FieldPathElement = Message<"buf.validate.FieldPathElement"> & { + /** + * `field_number` is the field number this path element refers to. + * + * @generated from field: optional int32 field_number = 1; + */ + fieldNumber: number; + + /** + * `field_name` contains the field name this path element refers to. + * This can be used to display a human-readable path even if the field number is unknown. + * + * @generated from field: optional string field_name = 2; + */ + fieldName: string; + + /** + * `field_type` specifies the type of this field. When using reflection, this value is not needed. + * + * This value is provided to make it possible to traverse unknown fields through wire data. + * When traversing wire data, be mindful of both packed[1] and delimited[2] encoding schemes. + * + * [1]: https://protobuf.dev/programming-guides/encoding/#packed + * [2]: https://protobuf.dev/programming-guides/encoding/#groups + * + * N.B.: Although groups are deprecated, the corresponding delimited encoding scheme is not, and + * can be explicitly used in Protocol Buffers 2023 Edition. + * + * @generated from field: optional google.protobuf.FieldDescriptorProto.Type field_type = 3; + */ + fieldType: FieldDescriptorProto_Type; + + /** + * `key_type` specifies the map key type of this field. This value is useful when traversing + * unknown fields through wire data: specifically, it allows handling the differences between + * different integer encodings. + * + * @generated from field: optional google.protobuf.FieldDescriptorProto.Type key_type = 4; + */ + keyType: FieldDescriptorProto_Type; + + /** + * `value_type` specifies map value type of this field. This is useful if you want to display a + * value inside unknown fields through wire data. + * + * @generated from field: optional google.protobuf.FieldDescriptorProto.Type value_type = 5; + */ + valueType: FieldDescriptorProto_Type; + + /** + * `subscript` contains a repeated index or map key, if this path element nests into a repeated or map field. + * + * @generated from oneof buf.validate.FieldPathElement.subscript + */ + subscript: { + /** + * `index` specifies a 0-based index into a repeated field. + * + * @generated from field: uint64 index = 6; + */ + value: bigint; + case: "index"; + } | { + /** + * `bool_key` specifies a map key of type bool. + * + * @generated from field: bool bool_key = 7; + */ + value: boolean; + case: "boolKey"; + } | { + /** + * `int_key` specifies a map key of type int32, int64, sint32, sint64, sfixed32 or sfixed64. + * + * @generated from field: int64 int_key = 8; + */ + value: bigint; + case: "intKey"; + } | { + /** + * `uint_key` specifies a map key of type uint32, uint64, fixed32 or fixed64. + * + * @generated from field: uint64 uint_key = 9; + */ + value: bigint; + case: "uintKey"; + } | { + /** + * `string_key` specifies a map key of type string. + * + * @generated from field: string string_key = 10; + */ + value: string; + case: "stringKey"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message buf.validate.FieldPathElement. + * Use `create(FieldPathElementSchema)` to create a new message. + */ +export const FieldPathElementSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_buf_validate_validate, 31); + +/** + * Specifies how `FieldRules.ignore` behaves, depending on the field's value, and + * whether the field tracks presence. + * + * @generated from enum buf.validate.Ignore + */ +export enum Ignore { + /** + * Ignore rules if the field tracks presence and is unset. This is the default + * behavior. + * + * In proto3, only message fields, members of a Protobuf `oneof`, and fields + * with the `optional` label track presence. Consequently, the following fields + * are always validated, whether a value is set or not: + * + * ```proto + * syntax="proto3"; + * + * message RulesApply { + * string email = 1 [ + * (buf.validate.field).string.email = true + * ]; + * int32 age = 2 [ + * (buf.validate.field).int32.gt = 0 + * ]; + * repeated string labels = 3 [ + * (buf.validate.field).repeated.min_items = 1 + * ]; + * } + * ``` + * + * In contrast, the following fields track presence, and are only validated if + * a value is set: + * + * ```proto + * syntax="proto3"; + * + * message RulesApplyIfSet { + * optional string email = 1 [ + * (buf.validate.field).string.email = true + * ]; + * oneof ref { + * string reference = 2 [ + * (buf.validate.field).string.uuid = true + * ]; + * string name = 3 [ + * (buf.validate.field).string.min_len = 4 + * ]; + * } + * SomeMessage msg = 4 [ + * (buf.validate.field).cel = {/* ... *\/} + * ]; + * } + * ``` + * + * To ensure that such a field is set, add the `required` rule. + * + * To learn which fields track presence, see the + * [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat). + * + * @generated from enum value: IGNORE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * Ignore rules if the field is unset, or set to the zero value. + * + * The zero value depends on the field type: + * - For strings, the zero value is the empty string. + * - For bytes, the zero value is empty bytes. + * - For bool, the zero value is false. + * - For numeric types, the zero value is zero. + * - For enums, the zero value is the first defined enum value. + * - For repeated fields, the zero is an empty list. + * - For map fields, the zero is an empty map. + * - For message fields, absence of the message (typically a null-value) is considered zero value. + * + * For fields that track presence (e.g. adding the `optional` label in proto3), + * this a no-op and behavior is the same as the default `IGNORE_UNSPECIFIED`. + * + * @generated from enum value: IGNORE_IF_ZERO_VALUE = 1; + */ + IF_ZERO_VALUE = 1, + + /** + * Always ignore rules, including the `required` rule. + * + * This is useful for ignoring the rules of a referenced message, or to + * temporarily ignore rules during development. + * + * ```proto + * message MyMessage { + * // The field's rules will always be ignored, including any validations + * // on value's fields. + * MyOtherMessage value = 1 [ + * (buf.validate.field).ignore = IGNORE_ALWAYS + * ]; + * } + * ``` + * + * @generated from enum value: IGNORE_ALWAYS = 3; + */ + ALWAYS = 3, +} + +/** + * Describes the enum buf.validate.Ignore. + */ +export const IgnoreSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_buf_validate_validate, 0); + +/** + * KnownRegex contains some well-known patterns. + * + * @generated from enum buf.validate.KnownRegex + */ +export enum KnownRegex { + /** + * @generated from enum value: KNOWN_REGEX_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2). + * + * @generated from enum value: KNOWN_REGEX_HTTP_HEADER_NAME = 1; + */ + HTTP_HEADER_NAME = 1, + + /** + * HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4). + * + * @generated from enum value: KNOWN_REGEX_HTTP_HEADER_VALUE = 2; + */ + HTTP_HEADER_VALUE = 2, +} + +/** + * Describes the enum buf.validate.KnownRegex. + */ +export const KnownRegexSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_buf_validate_validate, 1); + +/** + * Rules specify the validations to be performed on this message. By default, + * no validation is performed against a message. + * + * @generated from extension: optional buf.validate.MessageRules message = 1159; + */ +export const message: GenExtension = /*@__PURE__*/ + extDesc(file_buf_validate_validate, 0); + +/** + * Rules specify the validations to be performed on this oneof. By default, + * no validation is performed against a oneof. + * + * @generated from extension: optional buf.validate.OneofRules oneof = 1159; + */ +export const oneof: GenExtension = /*@__PURE__*/ + extDesc(file_buf_validate_validate, 1); + +/** + * Rules specify the validations to be performed on this field. By default, + * no validation is performed against a field. + * + * @generated from extension: optional buf.validate.FieldRules field = 1159; + */ +export const field: GenExtension = /*@__PURE__*/ + extDesc(file_buf_validate_validate, 2); + +/** + * Specifies predefined rules. When extending a standard rule message, + * this adds additional CEL expressions that apply when the extension is used. + * + * ```proto + * extend buf.validate.Int32Rules { + * bool is_zero = 1001 [(buf.validate.predefined).cel = { + * id: "int32.is_zero", + * message: "must be zero", + * expression: "!rule || this == 0", + * }]; + * } + * + * message Foo { + * int32 reserved = 1 [(buf.validate.field).int32.(is_zero) = true]; + * } + * ``` + * + * @generated from extension: optional buf.validate.PredefinedRules predefined = 1160; + */ +export const predefined: GenExtension = /*@__PURE__*/ + extDesc(file_buf_validate_validate, 3); + diff --git a/libraries/authority-client/typescript/src/gen/google/api/annotations_pb.ts b/libraries/authority-client/typescript/src/gen/google/api/annotations_pb.ts new file mode 100644 index 00000000..2e9f31d0 --- /dev/null +++ b/libraries/authority-client/typescript/src/gen/google/api/annotations_pb.ts @@ -0,0 +1,39 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=js" +// @generated from file google/api/annotations.proto (package google.api, syntax proto3) +/* eslint-disable */ + +import type { GenExtension, GenFile } from "@bufbuild/protobuf/codegenv2"; +import { extDesc, fileDesc } from "@bufbuild/protobuf/codegenv2"; +import type { HttpRule } from "./http_pb.js"; +import { file_google_api_http } from "./http_pb.js"; +import type { MethodOptions } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_descriptor } from "@bufbuild/protobuf/wkt"; + +/** + * Describes the file google/api/annotations.proto. + */ +export const file_google_api_annotations: GenFile = /*@__PURE__*/ + fileDesc("Chxnb29nbGUvYXBpL2Fubm90YXRpb25zLnByb3RvEgpnb29nbGUuYXBpOksKBGh0dHASHi5nb29nbGUucHJvdG9idWYuTWV0aG9kT3B0aW9ucxiwyrwiIAEoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGVSBGh0dHBCbgoOY29tLmdvb2dsZS5hcGlCEEFubm90YXRpb25zUHJvdG9QAVpBZ29vZ2xlLmdvbGFuZy5vcmcvZ2VucHJvdG8vZ29vZ2xlYXBpcy9hcGkvYW5ub3RhdGlvbnM7YW5ub3RhdGlvbnOiAgRHQVBJYgZwcm90bzM", [file_google_api_http, file_google_protobuf_descriptor]); + +/** + * See `HttpRule`. + * + * @generated from extension: google.api.HttpRule http = 72295728; + */ +export const http: GenExtension = /*@__PURE__*/ + extDesc(file_google_api_annotations, 0); + diff --git a/libraries/authority-client/typescript/src/gen/google/api/http_pb.ts b/libraries/authority-client/typescript/src/gen/google/api/http_pb.ts new file mode 100644 index 00000000..2a6664e0 --- /dev/null +++ b/libraries/authority-client/typescript/src/gen/google/api/http_pb.ts @@ -0,0 +1,474 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=js" +// @generated from file google/api/http.proto (package google.api, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file google/api/http.proto. + */ +export const file_google_api_http: GenFile = /*@__PURE__*/ + fileDesc("ChVnb29nbGUvYXBpL2h0dHAucHJvdG8SCmdvb2dsZS5hcGkiVAoESHR0cBIjCgVydWxlcxgBIAMoCzIULmdvb2dsZS5hcGkuSHR0cFJ1bGUSJwofZnVsbHlfZGVjb2RlX3Jlc2VydmVkX2V4cGFuc2lvbhgCIAEoCCKBAgoISHR0cFJ1bGUSEAoIc2VsZWN0b3IYASABKAkSDQoDZ2V0GAIgASgJSAASDQoDcHV0GAMgASgJSAASDgoEcG9zdBgEIAEoCUgAEhAKBmRlbGV0ZRgFIAEoCUgAEg8KBXBhdGNoGAYgASgJSAASLwoGY3VzdG9tGAggASgLMh0uZ29vZ2xlLmFwaS5DdXN0b21IdHRwUGF0dGVybkgAEgwKBGJvZHkYByABKAkSFQoNcmVzcG9uc2VfYm9keRgMIAEoCRIxChNhZGRpdGlvbmFsX2JpbmRpbmdzGAsgAygLMhQuZ29vZ2xlLmFwaS5IdHRwUnVsZUIJCgdwYXR0ZXJuIi8KEUN1c3RvbUh0dHBQYXR0ZXJuEgwKBGtpbmQYASABKAkSDAoEcGF0aBgCIAEoCUJnCg5jb20uZ29vZ2xlLmFwaUIJSHR0cFByb3RvUAFaQWdvb2dsZS5nb2xhbmcub3JnL2dlbnByb3RvL2dvb2dsZWFwaXMvYXBpL2Fubm90YXRpb25zO2Fubm90YXRpb25zogIER0FQSWIGcHJvdG8z"); + +/** + * Defines the HTTP configuration for an API service. It contains a list of + * [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method + * to one or more HTTP REST API methods. + * + * @generated from message google.api.Http + */ +export type Http = Message<"google.api.Http"> & { + /** + * A list of HTTP configuration rules that apply to individual API methods. + * + * **NOTE:** All service configuration rules follow "last one wins" order. + * + * @generated from field: repeated google.api.HttpRule rules = 1; + */ + rules: HttpRule[]; + + /** + * When set to true, URL path parameters will be fully URI-decoded except in + * cases of single segment matches in reserved expansion, where "%2F" will be + * left encoded. + * + * The default behavior is to not decode RFC 6570 reserved characters in multi + * segment matches. + * + * @generated from field: bool fully_decode_reserved_expansion = 2; + */ + fullyDecodeReservedExpansion: boolean; +}; + +/** + * Describes the message google.api.Http. + * Use `create(HttpSchema)` to create a new message. + */ +export const HttpSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_api_http, 0); + +/** + * gRPC Transcoding + * + * gRPC Transcoding is a feature for mapping between a gRPC method and one or + * more HTTP REST endpoints. It allows developers to build a single API service + * that supports both gRPC APIs and REST APIs. Many systems, including [Google + * APIs](https://github.com/googleapis/googleapis), + * [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC + * Gateway](https://github.com/grpc-ecosystem/grpc-gateway), + * and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature + * and use it for large scale production services. + * + * `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies + * how different portions of the gRPC request message are mapped to the URL + * path, URL query parameters, and HTTP request body. It also controls how the + * gRPC response message is mapped to the HTTP response body. `HttpRule` is + * typically specified as an `google.api.http` annotation on the gRPC method. + * + * Each mapping specifies a URL path template and an HTTP method. The path + * template may refer to one or more fields in the gRPC request message, as long + * as each field is a non-repeated field with a primitive (non-message) type. + * The path template controls how fields of the request message are mapped to + * the URL path. + * + * Example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get: "/v1/{name=messages/*}" + * }; + * } + * } + * message GetMessageRequest { + * string name = 1; // Mapped to URL path. + * } + * message Message { + * string text = 1; // The resource content. + * } + * + * This enables an HTTP REST to gRPC mapping as below: + * + * - HTTP: `GET /v1/messages/123456` + * - gRPC: `GetMessage(name: "messages/123456")` + * + * Any fields in the request message which are not bound by the path template + * automatically become HTTP query parameters if there is no HTTP request body. + * For example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get:"/v1/messages/{message_id}" + * }; + * } + * } + * message GetMessageRequest { + * message SubMessage { + * string subfield = 1; + * } + * string message_id = 1; // Mapped to URL path. + * int64 revision = 2; // Mapped to URL query parameter `revision`. + * SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. + * } + * + * This enables a HTTP JSON to RPC mapping as below: + * + * - HTTP: `GET /v1/messages/123456?revision=2&sub.subfield=foo` + * - gRPC: `GetMessage(message_id: "123456" revision: 2 sub: + * SubMessage(subfield: "foo"))` + * + * Note that fields which are mapped to URL query parameters must have a + * primitive type or a repeated primitive type or a non-repeated message type. + * In the case of a repeated type, the parameter can be repeated in the URL + * as `...?param=A¶m=B`. In the case of a message type, each field of the + * message is mapped to a separate parameter, such as + * `...?foo.a=A&foo.b=B&foo.c=C`. + * + * For HTTP methods that allow a request body, the `body` field + * specifies the mapping. Consider a REST update method on the + * message resource collection: + * + * service Messaging { + * rpc UpdateMessage(UpdateMessageRequest) returns (Message) { + * option (google.api.http) = { + * patch: "/v1/messages/{message_id}" + * body: "message" + * }; + * } + * } + * message UpdateMessageRequest { + * string message_id = 1; // mapped to the URL + * Message message = 2; // mapped to the body + * } + * + * The following HTTP JSON to RPC mapping is enabled, where the + * representation of the JSON in the request body is determined by + * protos JSON encoding: + * + * - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` + * - gRPC: `UpdateMessage(message_id: "123456" message { text: "Hi!" })` + * + * The special name `*` can be used in the body mapping to define that + * every field not bound by the path template should be mapped to the + * request body. This enables the following alternative definition of + * the update method: + * + * service Messaging { + * rpc UpdateMessage(Message) returns (Message) { + * option (google.api.http) = { + * patch: "/v1/messages/{message_id}" + * body: "*" + * }; + * } + * } + * message Message { + * string message_id = 1; + * string text = 2; + * } + * + * + * The following HTTP JSON to RPC mapping is enabled: + * + * - HTTP: `PATCH /v1/messages/123456 { "text": "Hi!" }` + * - gRPC: `UpdateMessage(message_id: "123456" text: "Hi!")` + * + * Note that when using `*` in the body mapping, it is not possible to + * have HTTP parameters, as all fields not bound by the path end in + * the body. This makes this option more rarely used in practice when + * defining REST APIs. The common usage of `*` is in custom methods + * which don't use the URL at all for transferring data. + * + * It is possible to define multiple HTTP methods for one RPC by using + * the `additional_bindings` option. Example: + * + * service Messaging { + * rpc GetMessage(GetMessageRequest) returns (Message) { + * option (google.api.http) = { + * get: "/v1/messages/{message_id}" + * additional_bindings { + * get: "/v1/users/{user_id}/messages/{message_id}" + * } + * }; + * } + * } + * message GetMessageRequest { + * string message_id = 1; + * string user_id = 2; + * } + * + * This enables the following two alternative HTTP JSON to RPC mappings: + * + * - HTTP: `GET /v1/messages/123456` + * - gRPC: `GetMessage(message_id: "123456")` + * + * - HTTP: `GET /v1/users/me/messages/123456` + * - gRPC: `GetMessage(user_id: "me" message_id: "123456")` + * + * Rules for HTTP mapping + * + * 1. Leaf request fields (recursive expansion nested messages in the request + * message) are classified into three categories: + * - Fields referred by the path template. They are passed via the URL path. + * - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They + * are passed via the HTTP + * request body. + * - All other fields are passed via the URL query parameters, and the + * parameter name is the field path in the request message. A repeated + * field can be represented as multiple query parameters under the same + * name. + * 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL + * query parameter, all fields + * are passed via URL path and HTTP request body. + * 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP + * request body, all + * fields are passed via URL path and URL query parameters. + * + * Path template syntax + * + * Template = "/" Segments [ Verb ] ; + * Segments = Segment { "/" Segment } ; + * Segment = "*" | "**" | LITERAL | Variable ; + * Variable = "{" FieldPath [ "=" Segments ] "}" ; + * FieldPath = IDENT { "." IDENT } ; + * Verb = ":" LITERAL ; + * + * The syntax `*` matches a single URL path segment. The syntax `**` matches + * zero or more URL path segments, which must be the last part of the URL path + * except the `Verb`. + * + * The syntax `Variable` matches part of the URL path as specified by its + * template. A variable template must not contain other variables. If a variable + * matches a single path segment, its template may be omitted, e.g. `{var}` + * is equivalent to `{var=*}`. + * + * The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` + * contains any reserved character, such characters should be percent-encoded + * before the matching. + * + * If a variable contains exactly one path segment, such as `"{var}"` or + * `"{var=*}"`, when such a variable is expanded into a URL path on the client + * side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The + * server side does the reverse decoding. Such variables show up in the + * [Discovery + * Document](https://developers.google.com/discovery/v1/reference/apis) as + * `{var}`. + * + * If a variable contains multiple path segments, such as `"{var=foo/*}"` + * or `"{var=**}"`, when such a variable is expanded into a URL path on the + * client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. + * The server side does the reverse decoding, except "%2F" and "%2f" are left + * unchanged. Such variables show up in the + * [Discovery + * Document](https://developers.google.com/discovery/v1/reference/apis) as + * `{+var}`. + * + * Using gRPC API Service Configuration + * + * gRPC API Service Configuration (service config) is a configuration language + * for configuring a gRPC service to become a user-facing product. The + * service config is simply the YAML representation of the `google.api.Service` + * proto message. + * + * As an alternative to annotating your proto file, you can configure gRPC + * transcoding in your service config YAML files. You do this by specifying a + * `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same + * effect as the proto annotation. This can be particularly useful if you + * have a proto that is reused in multiple services. Note that any transcoding + * specified in the service config will override any matching transcoding + * configuration in the proto. + * + * The following example selects a gRPC method and applies an `HttpRule` to it: + * + * http: + * rules: + * - selector: example.v1.Messaging.GetMessage + * get: /v1/messages/{message_id}/{sub.subfield} + * + * Special notes + * + * When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the + * proto to JSON conversion must follow the [proto3 + * specification](https://developers.google.com/protocol-buffers/docs/proto3#json). + * + * While the single segment variable follows the semantics of + * [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String + * Expansion, the multi segment variable **does not** follow RFC 6570 Section + * 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion + * does not expand special characters like `?` and `#`, which would lead + * to invalid URLs. As the result, gRPC Transcoding uses a custom encoding + * for multi segment variables. + * + * The path variables **must not** refer to any repeated or mapped field, + * because client libraries are not capable of handling such variable expansion. + * + * The path variables **must not** capture the leading "/" character. The reason + * is that the most common use case "{var}" does not capture the leading "/" + * character. For consistency, all path variables must share the same behavior. + * + * Repeated message fields must not be mapped to URL query parameters, because + * no client library can support such complicated mapping. + * + * If an API needs to use a JSON array for request or response body, it can map + * the request or response body to a repeated field. However, some gRPC + * Transcoding implementations may not support this feature. + * + * @generated from message google.api.HttpRule + */ +export type HttpRule = Message<"google.api.HttpRule"> & { + /** + * Selects a method to which this rule applies. + * + * Refer to [selector][google.api.DocumentationRule.selector] for syntax + * details. + * + * @generated from field: string selector = 1; + */ + selector: string; + + /** + * Determines the URL pattern is matched by this rules. This pattern can be + * used with any of the {get|put|post|delete|patch} methods. A custom method + * can be defined using the 'custom' field. + * + * @generated from oneof google.api.HttpRule.pattern + */ + pattern: { + /** + * Maps to HTTP GET. Used for listing and getting information about + * resources. + * + * @generated from field: string get = 2; + */ + value: string; + case: "get"; + } | { + /** + * Maps to HTTP PUT. Used for replacing a resource. + * + * @generated from field: string put = 3; + */ + value: string; + case: "put"; + } | { + /** + * Maps to HTTP POST. Used for creating a resource or performing an action. + * + * @generated from field: string post = 4; + */ + value: string; + case: "post"; + } | { + /** + * Maps to HTTP DELETE. Used for deleting a resource. + * + * @generated from field: string delete = 5; + */ + value: string; + case: "delete"; + } | { + /** + * Maps to HTTP PATCH. Used for updating a resource. + * + * @generated from field: string patch = 6; + */ + value: string; + case: "patch"; + } | { + /** + * The custom pattern is used for specifying an HTTP method that is not + * included in the `pattern` field, such as HEAD, or "*" to leave the + * HTTP method unspecified for this rule. The wild-card rule is useful + * for services that provide content to Web (HTML) clients. + * + * @generated from field: google.api.CustomHttpPattern custom = 8; + */ + value: CustomHttpPattern; + case: "custom"; + } | { case: undefined; value?: undefined }; + + /** + * The name of the request field whose value is mapped to the HTTP request + * body, or `*` for mapping all request fields not captured by the path + * pattern to the HTTP body, or omitted for not having any HTTP request body. + * + * NOTE: the referred field must be present at the top-level of the request + * message type. + * + * @generated from field: string body = 7; + */ + body: string; + + /** + * Optional. The name of the response field whose value is mapped to the HTTP + * response body. When omitted, the entire response message will be used + * as the HTTP response body. + * + * NOTE: The referred field must be present at the top-level of the response + * message type. + * + * @generated from field: string response_body = 12; + */ + responseBody: string; + + /** + * Additional HTTP bindings for the selector. Nested bindings must + * not contain an `additional_bindings` field themselves (that is, + * the nesting may only be one level deep). + * + * @generated from field: repeated google.api.HttpRule additional_bindings = 11; + */ + additionalBindings: HttpRule[]; +}; + +/** + * Describes the message google.api.HttpRule. + * Use `create(HttpRuleSchema)` to create a new message. + */ +export const HttpRuleSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_api_http, 1); + +/** + * A custom pattern is used for defining custom HTTP verb. + * + * @generated from message google.api.CustomHttpPattern + */ +export type CustomHttpPattern = Message<"google.api.CustomHttpPattern"> & { + /** + * The name of this custom HTTP verb. + * + * @generated from field: string kind = 1; + */ + kind: string; + + /** + * The path matched by this custom verb. + * + * @generated from field: string path = 2; + */ + path: string; +}; + +/** + * Describes the message google.api.CustomHttpPattern. + * Use `create(CustomHttpPatternSchema)` to create a new message. + */ +export const CustomHttpPatternSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_google_api_http, 2); + diff --git a/libraries/authority-client/typescript/src/gen/host/artifact/v1/artifact_pb.ts b/libraries/authority-client/typescript/src/gen/host/artifact/v1/artifact_pb.ts new file mode 100644 index 00000000..16869eb3 --- /dev/null +++ b/libraries/authority-client/typescript/src/gen/host/artifact/v1/artifact_pb.ts @@ -0,0 +1,276 @@ +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=js" +// @generated from file host/artifact/v1/artifact.proto (package host.artifact.v1, syntax proto3) +/* eslint-disable */ + +// The artifact interface: a person's consent to an exact authored artifact a +// module runs (an agent's configuration, an intent version), for one +// installation, under one policy. A policy is a resource kind: approving +// requires its `activate` action at the module, running requires its `run`. +// Consent is given in person and read back at every run; it is never a +// credential to execute or read anything. + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file host/artifact/v1/artifact.proto. + */ +export const file_host_artifact_v1_artifact: GenFile = /*@__PURE__*/ + fileDesc("Ch9ob3N0L2FydGlmYWN0L3YxL2FydGlmYWN0LnByb3RvEhBob3N0LmFydGlmYWN0LnYxIjYKCENvbnRyYWN0EgwKBGtpbmQYASABKAkSDAoEbmFtZRgCIAEoCRIOCgZkaWdlc3QYAyABKAkihQEKCElkZW50aXR5Eg4KBnNjaGVtYRgBIAEoCRIOCgZzb3VyY2UYAiABKAkSDwoHc3ViamVjdBgDIAEoDBItCgljb250cmFjdHMYBCADKAsyGi5ob3N0LmFydGlmYWN0LnYxLkNvbnRyYWN0EhkKEWV4cGVjdGVkX3JldmlzaW9uGAUgASgDIpgBCg9BcnRpZmFjdFJlcXVlc3QSFAoMcGFyZW50X3Rva2VuGAEgASgJEhgKEGFjdG9yX2NyZWRlbnRpYWwYAiABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAMgASgJEg4KBnBvbGljeRgEIAEoCRIsCghpZGVudGl0eRgFIAEoCzIaLmhvc3QuYXJ0aWZhY3QudjEuSWRlbnRpdHkifQoNUmV2b2tlUmVxdWVzdBIUCgxwYXJlbnRfdG9rZW4YASABKAkSGAoQYWN0b3JfY3JlZGVudGlhbBgCIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAyABKAkSDgoGcG9saWN5GAQgASgJEhMKC2FwcHJvdmFsX2lkGAUgASgJIk0KCEFwcHJvdmFsEhYKDnF1YWxpZmllZF9uYW1lGAEgASgJEg4KBmRpZ2VzdBgCIAEoCRIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoAzLuAQoPQXJ0aWZhY3RTZXJ2aWNlEkgKB0FwcHJvdmUSIS5ob3N0LmFydGlmYWN0LnYxLkFydGlmYWN0UmVxdWVzdBoaLmhvc3QuYXJ0aWZhY3QudjEuQXBwcm92YWwSSgoJQXV0aG9yaXplEiEuaG9zdC5hcnRpZmFjdC52MS5BcnRpZmFjdFJlcXVlc3QaGi5ob3N0LmFydGlmYWN0LnYxLkFwcHJvdmFsEkUKBlJldm9rZRIfLmhvc3QuYXJ0aWZhY3QudjEuUmV2b2tlUmVxdWVzdBoaLmhvc3QuYXJ0aWZhY3QudjEuQXBwcm92YWxCLVorZ2F0ZXdheS9wa2cvZ2VuL2hvc3QvYXJ0aWZhY3QvdjE7YXJ0aWZhY3R2MWIGcHJvdG8z"); + +/** + * Contract is one contract the artifact is compiled against. + * + * @generated from message host.artifact.v1.Contract + */ +export type Contract = Message<"host.artifact.v1.Contract"> & { + /** + * @generated from field: string kind = 1; + */ + kind: string; + + /** + * @generated from field: string name = 2; + */ + name: string; + + /** + * @generated from field: string digest = 3; + */ + digest: string; +}; + +/** + * Describes the message host.artifact.v1.Contract. + * Use `create(ContractSchema)` to create a new message. + */ +export const ContractSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_artifact_v1_artifact, 0); + +/** + * Identity is the artifact's complete canonical identity, as the module's + * compiler exports it: references and digests, never authored content. + * + * @generated from message host.artifact.v1.Identity + */ +export type Identity = Message<"host.artifact.v1.Identity"> & { + /** + * The schema the subject is written in, which owns its canonical bytes. + * + * @generated from field: string schema = 1; + */ + schema: string; + + /** + * The source the artifact belongs to. + * + * @generated from field: string source = 2; + */ + source: string; + + /** + * The canonical subject. + * + * @generated from field: bytes subject = 3; + */ + subject: Uint8Array; + + /** + * The contracts it is compiled against. + * + * @generated from field: repeated host.artifact.v1.Contract contracts = 4; + */ + contracts: Contract[]; + + /** + * The revision the module expects the approval at. + * + * @generated from field: int64 expected_revision = 5; + */ + expectedRevision: bigint; +}; + +/** + * Describes the message host.artifact.v1.Identity. + * Use `create(IdentitySchema)` to create a new message. + */ +export const IdentitySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_artifact_v1_artifact, 1); + +/** + * ArtifactRequest asks about one identity, for one installation, under one policy. + * + * @generated from message host.artifact.v1.ArtifactRequest + */ +export type ArtifactRequest = Message<"host.artifact.v1.ArtifactRequest"> & { + /** + * The Work Context the module was presented, addressed to it: whose consent + * this is, and what they hold there. + * + * @generated from field: string parent_token = 1; + */ + parentToken: string; + + /** + * The module asking, by its workload credential. + * + * @generated from field: string actor_credential = 2; + */ + actorCredential: string; + + /** + * The installation the artifact runs in. + * + * @generated from field: string installation_id = 3; + */ + installationId: string; + + /** + * The policy: a resource kind at the module. + * + * @generated from field: string policy = 4; + */ + policy: string; + + /** + * The artifact. + * + * @generated from field: host.artifact.v1.Identity identity = 5; + */ + identity?: Identity; +}; + +/** + * Describes the message host.artifact.v1.ArtifactRequest. + * Use `create(ArtifactRequestSchema)` to create a new message. + */ +export const ArtifactRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_artifact_v1_artifact, 2); + +/** + * RevokeRequest withdraws one approval. + * + * @generated from message host.artifact.v1.RevokeRequest + */ +export type RevokeRequest = Message<"host.artifact.v1.RevokeRequest"> & { + /** + * @generated from field: string parent_token = 1; + */ + parentToken: string; + + /** + * @generated from field: string actor_credential = 2; + */ + actorCredential: string; + + /** + * @generated from field: string installation_id = 3; + */ + installationId: string; + + /** + * The policy the approval was given under. + * + * @generated from field: string policy = 4; + */ + policy: string; + + /** + * The approval, by its id. + * + * @generated from field: string approval_id = 5; + */ + approvalId: string; +}; + +/** + * Describes the message host.artifact.v1.RevokeRequest. + * Use `create(RevokeRequestSchema)` to create a new message. + */ +export const RevokeRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_artifact_v1_artifact, 3); + +/** + * Approval is the host's exact consent reference. + * + * @generated from message host.artifact.v1.Approval + */ +export type Approval = Message<"host.artifact.v1.Approval"> & { + /** + * host/approved-artifacts/. + * + * @generated from field: string qualified_name = 1; + */ + qualifiedName: string; + + /** + * sha256: of the identity approved. + * + * @generated from field: string digest = 2; + */ + digest: string; + + /** + * The revision the approval was given at. + * + * @generated from field: int64 expected_revision = 3; + */ + expectedRevision: bigint; +}; + +/** + * Describes the message host.artifact.v1.Approval. + * Use `create(ApprovalSchema)` to create a new message. + */ +export const ApprovalSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_artifact_v1_artifact, 4); + +/** + * ArtifactService is implemented by the host and by the authority gateway. + * + * @generated from service host.artifact.v1.ArtifactService + */ +export const ArtifactService: GenService<{ + /** + * Approve records the person's consent to the identity for the installation + * under the policy. The person is present: the parent is theirs, addressed + * to the asking module, with no module acting between, and holds the policy + * kind's activate action there. An identity already approved answers the + * same approval; one whose approval was revoked is refused. Consent is + * durable: the host keeps it in its store, so it outlives the process that + * recorded it and answers every run until it is revoked. + * + * @generated from rpc host.artifact.v1.ArtifactService.Approve + */ + approve: { + methodKind: "unary"; + input: typeof ArtifactRequestSchema; + output: typeof ApprovalSchema; + }, + /** + * Authorize answers the standing approval of the identity, for a run: the + * parent may be delegated and holds the policy kind's run action. It never + * creates consent. + * + * @generated from rpc host.artifact.v1.ArtifactService.Authorize + */ + authorize: { + methodKind: "unary"; + input: typeof ArtifactRequestSchema; + output: typeof ApprovalSchema; + }, + /** + * Revoke withdraws an approval, permanently, for a person present holding + * the policy kind's activate action. + * + * @generated from rpc host.artifact.v1.ArtifactService.Revoke + */ + revoke: { + methodKind: "unary"; + input: typeof RevokeRequestSchema; + output: typeof ApprovalSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_host_artifact_v1_artifact, 0); + diff --git a/libraries/authority-client/typescript/src/gen/host/authority/v1/authority_pb.ts b/libraries/authority-client/typescript/src/gen/host/authority/v1/authority_pb.ts new file mode 100644 index 00000000..e225b5a4 --- /dev/null +++ b/libraries/authority-client/typescript/src/gen/host/authority/v1/authority_pb.ts @@ -0,0 +1,787 @@ +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=js" +// @generated from file host/authority/v1/authority.proto (package host.authority.v1, syntax proto3) +/* eslint-disable */ + +// The authority interface: what a module or solution calls on its host to be +// issued a Work Context, to have one checked, and to learn the keys it was +// signed with, and to ask which resources of a kind it covers. Every former spelling (start task, start +// installation task, exchange audience, start child, renew, the module broker's +// mints) is Issue with a different `as`. + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_google_api_annotations } from "../../../google/api/annotations_pb.js"; +import type { WorkContextV1, WorkScopeV1 } from "./work_context_pb.js"; +import { file_host_authority_v1_work_context } from "./work_context_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file host/authority/v1/authority.proto. + */ +export const file_host_authority_v1_authority: GenFile = /*@__PURE__*/ + fileDesc("CiFob3N0L2F1dGhvcml0eS92MS9hdXRob3JpdHkucHJvdG8SEWhvc3QuYXV0aG9yaXR5LnYxIuYDCgxJc3N1ZVJlcXVlc3QSMgoGcGVyc29uGAEgASgLMiAuaG9zdC5hdXRob3JpdHkudjEuUGVyc29uU2Vzc2lvbkgAEjkKCHdvcmtsb2FkGAIgASgLMiUuaG9zdC5hdXRob3JpdHkudjEuV29ya2xvYWRDcmVkZW50aWFsSAASOgoMaW5zdGFsbGF0aW9uGAMgASgLMiIuaG9zdC5hdXRob3JpdHkudjEuSW5zdGFsbGF0aW9uUmVmSAASFgoMcGFyZW50X3Rva2VuGAQgASgJSAASNgoKZGVsZWdhdGlvbhgLIAEoCzIgLmhvc3QuYXV0aG9yaXR5LnYxLkRlbGVnYXRpb25SZWZIABIQCghhdWRpZW5jZRgFIAEoCRIuCgZzY29wZXMYBiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRITCgt0dGxfc2Vjb25kcxgHIAEoAxIcChRvcGVyYXRpb25fYmluZGluZ19pZBgIIAEoCRIXCg9vcmdhbml6YXRpb25faWQYCSABKAkSNAoFYWN0b3IYCiABKAsyJS5ob3N0LmF1dGhvcml0eS52MS5Xb3JrbG9hZENyZWRlbnRpYWwSEQoJb3BlcmF0aW9uGAwgASgJQgQKAmFzIiYKDURlbGVnYXRpb25SZWYSFQoNZGVsZWdhdGlvbl9pZBgBIAEoCSJiCg9EZWxlZ2F0ZVJlcXVlc3QSDgoGbW9kdWxlGAEgASgJEi4KBnNjb3BlcxgCIAMoCzIeLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtTY29wZVYxEg8KB3B1cnBvc2UYAyABKAkirgEKCkRlbGVnYXRpb24SFQoNZGVsZWdhdGlvbl9pZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDwoHZ3JhbnRvchgDIAEoCRIOCgZtb2R1bGUYBCABKAkSLgoGc2NvcGVzGAUgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESDwoHcHVycG9zZRgGIAEoCRIOCgZhY3RpdmUYByABKAgiYwoUR2V0RGVsZWdhdGlvblJlcXVlc3QSFQoNZGVsZWdhdGlvbl9pZBgBIAEoCRI0CgVhY3RvchgCIAEoCzIlLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtsb2FkQ3JlZGVudGlhbCIwChdSZXZva2VEZWxlZ2F0aW9uUmVxdWVzdBIVCg1kZWxlZ2F0aW9uX2lkGAEgASgJIk0KGFJldm9rZURlbGVnYXRpb25SZXNwb25zZRIxCgpkZWxlZ2F0aW9uGAEgASgLMh0uaG9zdC5hdXRob3JpdHkudjEuRGVsZWdhdGlvbiIhCg1QZXJzb25TZXNzaW9uSgQIARACUgpzZXNzaW9uX2lkIigKEldvcmtsb2FkQ3JlZGVudGlhbBISCgpjcmVkZW50aWFsGAEgASgJIioKD0luc3RhbGxhdGlvblJlZhIXCg9pbnN0YWxsYXRpb25faWQYASABKAkiagoNSXNzdWVSZXNwb25zZRINCgV0b2tlbhgBIAEoCRIXCg9leHBpcmVzX2F0X3VuaXgYAiABKAMSMQoHY29udGV4dBgDIAEoCzIgLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtDb250ZXh0VjEiUQoMQ2hlY2tSZXF1ZXN0Eg0KBXRva2VuGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEg4KBm1ldGhvZBgDIAEoCRIQCghhZHZpc29yeRgEIAEoCCJzCg1DaGVja1Jlc3BvbnNlEjEKB2NvbnRleHQYASABKAsyIC5ob3N0LmF1dGhvcml0eS52MS5Xb3JrQ29udGV4dFYxEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YAiABKAQSDwoHcmVmdXNhbBgDIAEoCSINCgtLZXlzUmVxdWVzdCJKCgxLZXlzUmVzcG9uc2USDgoGaXNzdWVyGAEgASgJEioKBGtleXMYAiADKAsyHC5ob3N0LmF1dGhvcml0eS52MS5QdWJsaWNLZXkiQgoJUHVibGljS2V5Eg4KBmtleV9pZBgBIAEoCRIRCglhbGdvcml0aG0YAiABKAkSEgoKcHVibGljX2tleRgDIAEoDCJeChRMaXN0UmVzb3VyY2VzUmVxdWVzdBINCgV0b2tlbhgBIAEoCRIQCghhdWRpZW5jZRgCIAEoCRIVCg1yZXNvdXJjZV9raW5kGAMgASgJEg4KBmFjdGlvbhgEIAEoCSJJChVMaXN0UmVzb3VyY2VzUmVzcG9uc2USMAoGZ3JhbnRzGAEgAygLMiAuaG9zdC5hdXRob3JpdHkudjEuUmVzb3VyY2VHcmFudCJaCg1SZXNvdXJjZUdyYW50Eg0KA2FsbBgBIAEoCEgAEgwKAmlkGAIgASgJSAASEAoGcHJlZml4GAMgASgJSAASEQoHcGF0dGVybhgEIAEoCUgAQgcKBW1hdGNoMqwFChBBdXRob3JpdHlTZXJ2aWNlEmoKBUlzc3VlEh8uaG9zdC5hdXRob3JpdHkudjEuSXNzdWVSZXF1ZXN0GiAuaG9zdC5hdXRob3JpdHkudjEuSXNzdWVSZXNwb25zZSIegtPkkwIYOgEqIhMvdjEvYXV0aG9yaXR5Omlzc3VlEmoKBUNoZWNrEh8uaG9zdC5hdXRob3JpdHkudjEuQ2hlY2tSZXF1ZXN0GiAuaG9zdC5hdXRob3JpdHkudjEuQ2hlY2tSZXNwb25zZSIegtPkkwIYOgEqIhMvdjEvYXV0aG9yaXR5OmNoZWNrEkcKBEtleXMSHi5ob3N0LmF1dGhvcml0eS52MS5LZXlzUmVxdWVzdBofLmhvc3QuYXV0aG9yaXR5LnYxLktleXNSZXNwb25zZRJiCg1MaXN0UmVzb3VyY2VzEicuaG9zdC5hdXRob3JpdHkudjEuTGlzdFJlc291cmNlc1JlcXVlc3QaKC5ob3N0LmF1dGhvcml0eS52MS5MaXN0UmVzb3VyY2VzUmVzcG9uc2USTQoIRGVsZWdhdGUSIi5ob3N0LmF1dGhvcml0eS52MS5EZWxlZ2F0ZVJlcXVlc3QaHS5ob3N0LmF1dGhvcml0eS52MS5EZWxlZ2F0aW9uEmsKEFJldm9rZURlbGVnYXRpb24SKi5ob3N0LmF1dGhvcml0eS52MS5SZXZva2VEZWxlZ2F0aW9uUmVxdWVzdBorLmhvc3QuYXV0aG9yaXR5LnYxLlJldm9rZURlbGVnYXRpb25SZXNwb25zZRJXCg1HZXREZWxlZ2F0aW9uEicuaG9zdC5hdXRob3JpdHkudjEuR2V0RGVsZWdhdGlvblJlcXVlc3QaHS5ob3N0LmF1dGhvcml0eS52MS5EZWxlZ2F0aW9uQi9aLWdhdGV3YXkvcGtnL2dlbi9ob3N0L2F1dGhvcml0eS92MTthdXRob3JpdHl2MWIGcHJvdG8z", [file_google_api_annotations, file_host_authority_v1_work_context]); + +/** + * IssueRequest names on whose behalf the capability is issued. + * + * @generated from message host.authority.v1.IssueRequest + */ +export type IssueRequest = Message<"host.authority.v1.IssueRequest"> & { + /** + * Who the capability is for; exactly one. + * + * @generated from oneof host.authority.v1.IssueRequest.as + */ + as: { + /** + * The signed-in person making this call, identified by the bearer the call + * carries; the host opened that session at sign-in. + * + * @generated from field: host.authority.v1.PersonSession person = 1; + */ + value: PersonSession; + case: "person"; + } | { + /** + * A module's own workload, proving which module it is. + * + * @generated from field: host.authority.v1.WorkloadCredential workload = 2; + */ + value: WorkloadCredential; + case: "workload"; + } | { + /** + * An installation acting for itself, with the scope the organisation consented to. + * + * @generated from field: host.authority.v1.InstallationRef installation = 3; + */ + value: InstallationRef; + case: "installation"; + } | { + /** + * One more hop from an existing capability: a child, an exchange or a renewal. + * + * @generated from field: string parent_token = 4; + */ + value: string; + case: "parentToken"; + } | { + /** + * A delegation a person granted the module acting (actor): the capability + * is owned by that person and carries the delegation on the module's hop. + * No live capability is presented: the authority re-checks the delegation + * and what its grantor holds now, so a revoked delegation or a lost role + * stops the next issue, and renewing is issuing again. + * + * @generated from field: host.authority.v1.DelegationRef delegation = 11; + */ + value: DelegationRef; + case: "delegation"; + } | { case: undefined; value?: undefined }; + + /** + * The module principal that may consume the capability. + * + * @generated from field: string audience = 5; + */ + audience: string; + + /** + * The scopes asked for; empty means everything the owner holds at the audience. + * + * @generated from field: repeated host.authority.v1.WorkScopeV1 scopes = 6; + */ + scopes: WorkScopeV1[]; + + /** + * Requested lifetime in seconds; clamped to the issuer's ceiling. + * + * @generated from field: int64 ttl_seconds = 7; + */ + ttlSeconds: bigint; + + /** + * The declared binding this capability exercises, when the owner is a workload + * or a child acts through one. + * + * @generated from field: string operation_binding_id = 8; + */ + operationBindingId: string; + + /** + * The organisation the work runs in, for an owner shape that does not carry one. + * + * @generated from field: string organization_id = 9; + */ + organizationId: string; + + /** + * The actor a child hop delegates to, as a workload credential; required with + * parent_token and with delegation. + * + * @generated from field: host.authority.v1.WorkloadCredential actor = 10; + */ + actor?: WorkloadCredential; + + /** + * Which of the binding's operations the capability is for: "invoke" (the + * default for a person, a child or a delegation), "headless" (the default for + * a workload) or "lookup": the binding's lookup scopes alone, to read a + * receipt without the authority to repeat the effect. + * + * @generated from field: string operation = 12; + */ + operation: string; +}; + +/** + * Describes the message host.authority.v1.IssueRequest. + * Use `create(IssueRequestSchema)` to create a new message. + */ +export const IssueRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 0); + +/** + * DelegationRef names one delegation. + * + * @generated from message host.authority.v1.DelegationRef + */ +export type DelegationRef = Message<"host.authority.v1.DelegationRef"> & { + /** + * The delegation's id, as Delegate answered it. + * + * @generated from field: string delegation_id = 1; + */ + delegationId: string; +}; + +/** + * Describes the message host.authority.v1.DelegationRef. + * Use `create(DelegationRefSchema)` to create a new message. + */ +export const DelegationRefSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 1); + +/** + * DelegateRequest is a signed-in person granting a module the right to act for + * them beyond any session: on scopes they hold, through the module's bindings. + * + * @generated from message host.authority.v1.DelegateRequest + */ +export type DelegateRequest = Message<"host.authority.v1.DelegateRequest"> & { + /** + * The module granted, by principal. + * + * @generated from field: string module = 1; + */ + module: string; + + /** + * What the module may do for the person; within what the person holds. + * + * @generated from field: repeated host.authority.v1.WorkScopeV1 scopes = 2; + */ + scopes: WorkScopeV1[]; + + /** + * Why, as the person was told (a connected source, a scheduled job). + * + * @generated from field: string purpose = 3; + */ + purpose: string; +}; + +/** + * Describes the message host.authority.v1.DelegateRequest. + * Use `create(DelegateRequestSchema)` to create a new message. + */ +export const DelegateRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 2); + +/** + * Delegation is one grant from a person to a module. + * + * @generated from message host.authority.v1.Delegation + */ +export type Delegation = Message<"host.authority.v1.Delegation"> & { + /** + * @generated from field: string delegation_id = 1; + */ + delegationId: string; + + /** + * @generated from field: string organization_id = 2; + */ + organizationId: string; + + /** + * The person who granted it, and whom every capability issued from it is for. + * + * @generated from field: string grantor = 3; + */ + grantor: string; + + /** + * The module granted. + * + * @generated from field: string module = 4; + */ + module: string; + + /** + * @generated from field: repeated host.authority.v1.WorkScopeV1 scopes = 5; + */ + scopes: WorkScopeV1[]; + + /** + * @generated from field: string purpose = 6; + */ + purpose: string; + + /** + * False once revoked; a revoked delegation issues nothing. + * + * @generated from field: bool active = 7; + */ + active: boolean; +}; + +/** + * Describes the message host.authority.v1.Delegation. + * Use `create(DelegationSchema)` to create a new message. + */ +export const DelegationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 3); + +/** + * GetDelegationRequest names the delegation read; anyone but its module or its + * grantor is told it does not exist. + * + * @generated from message host.authority.v1.GetDelegationRequest + */ +export type GetDelegationRequest = Message<"host.authority.v1.GetDelegationRequest"> & { + /** + * @generated from field: string delegation_id = 1; + */ + delegationId: string; + + /** + * The module asking, proving which module it is; absent when the grantor asks. + * + * @generated from field: host.authority.v1.WorkloadCredential actor = 2; + */ + actor?: WorkloadCredential; +}; + +/** + * Describes the message host.authority.v1.GetDelegationRequest. + * Use `create(GetDelegationRequestSchema)` to create a new message. + */ +export const GetDelegationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 4); + +/** + * RevokeDelegationRequest ends a delegation; only its grantor may. + * + * @generated from message host.authority.v1.RevokeDelegationRequest + */ +export type RevokeDelegationRequest = Message<"host.authority.v1.RevokeDelegationRequest"> & { + /** + * @generated from field: string delegation_id = 1; + */ + delegationId: string; +}; + +/** + * Describes the message host.authority.v1.RevokeDelegationRequest. + * Use `create(RevokeDelegationRequestSchema)` to create a new message. + */ +export const RevokeDelegationRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 5); + +/** + * RevokeDelegationResponse is the delegation as it now stands. + * + * @generated from message host.authority.v1.RevokeDelegationResponse + */ +export type RevokeDelegationResponse = Message<"host.authority.v1.RevokeDelegationResponse"> & { + /** + * @generated from field: host.authority.v1.Delegation delegation = 1; + */ + delegation?: Delegation; +}; + +/** + * Describes the message host.authority.v1.RevokeDelegationResponse. + * Use `create(RevokeDelegationResponseSchema)` to create a new message. + */ +export const RevokeDelegationResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 6); + +/** + * PersonSession names the caller's own session. It carries no field: the person + * is whoever the bearer on the call authenticates as, so a caller cannot ask for + * another person's authority by naming a session. + * + * @generated from message host.authority.v1.PersonSession + */ +export type PersonSession = Message<"host.authority.v1.PersonSession"> & { +}; + +/** + * Describes the message host.authority.v1.PersonSession. + * Use `create(PersonSessionSchema)` to create a new message. + */ +export const PersonSessionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 7); + +/** + * WorkloadCredential proves which module a running process is: the native + * launcher credential core's workloadidentity defines, signed by the one launcher + * the host was handed an anchor for. + * + * @generated from message host.authority.v1.WorkloadCredential + */ +export type WorkloadCredential = Message<"host.authority.v1.WorkloadCredential"> & { + /** + * The launcher-signed credential, as core's native issuer writes it. + * + * @generated from field: string credential = 1; + */ + credential: string; +}; + +/** + * Describes the message host.authority.v1.WorkloadCredential. + * Use `create(WorkloadCredentialSchema)` to create a new message. + */ +export const WorkloadCredentialSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 8); + +/** + * InstallationRef names an installation acting for itself. + * + * @generated from message host.authority.v1.InstallationRef + */ +export type InstallationRef = Message<"host.authority.v1.InstallationRef"> & { + /** + * The installation id. + * + * @generated from field: string installation_id = 1; + */ + installationId: string; +}; + +/** + * Describes the message host.authority.v1.InstallationRef. + * Use `create(InstallationRefSchema)` to create a new message. + */ +export const InstallationRefSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 9); + +/** + * IssueResponse carries the signed capability and its decoded form. + * + * @generated from message host.authority.v1.IssueResponse + */ +export type IssueResponse = Message<"host.authority.v1.IssueResponse"> & { + /** + * The signed capability, as a caller presents it. + * + * @generated from field: string token = 1; + */ + token: string; + + /** + * When the capability expires. + * + * @generated from field: int64 expires_at_unix = 2; + */ + expiresAtUnix: bigint; + + /** + * The decoded capability, for the holder to read its own claims. + * + * @generated from field: host.authority.v1.WorkContextV1 context = 3; + */ + context?: WorkContextV1; +}; + +/** + * Describes the message host.authority.v1.IssueResponse. + * Use `create(IssueResponseSchema)` to create a new message. + */ +export const IssueResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 10); + +/** + * CheckRequest presents a capability for verification. + * + * @generated from message host.authority.v1.CheckRequest + */ +export type CheckRequest = Message<"host.authority.v1.CheckRequest"> & { + /** + * The capability presented. + * + * @generated from field: string token = 1; + */ + token: string; + + /** + * The audience the caller is; a capability addressed elsewhere is refused. + * + * @generated from field: string audience = 2; + */ + audience: string; + + /** + * The method the capability is presented to: "/package.Service/Method" for a + * gRPC call, " " for an HTTP request (GET /v1/model/profiles), + * matched against the routes the module declares. When set, the check also + * applies what the audience module's delivery declares for that method, and + * refuses by name a call that does not hold it, a method that declares + * nothing, and a method the module does not serve. + * + * @generated from field: string method = 3; + */ + method: string; + + /** + * Advisory asks for the policy decision without its refusal: the capability + * is still verified and refused when it is not sound, but what the method + * declares is only reported, in CheckResponse.refusal, and the verified + * capability is answered. A hop in a composition that runs permissive + * (hop/ENFORCEMENT=warn) asks this way and logs the refusal it did not make. + * + * @generated from field: bool advisory = 4; + */ + advisory: boolean; +}; + +/** + * Describes the message host.authority.v1.CheckRequest. + * Use `create(CheckRequestSchema)` to create a new message. + */ +export const CheckRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 11); + +/** + * CheckResponse is the verified capability. + * + * @generated from message host.authority.v1.CheckResponse + */ +export type CheckResponse = Message<"host.authority.v1.CheckResponse"> & { + /** + * The verified capability. + * + * @generated from field: host.authority.v1.WorkContextV1 context = 1; + */ + context?: WorkContextV1; + + /** + * The owner's authorization revision as of this check. + * + * @generated from field: uint64 authorization_revision = 2; + */ + authorizationRevision: bigint; + + /** + * Refusal is what the method's policy would have refused, verbatim, when the + * check was advisory and the policy did not hold; empty when it held. + * + * @generated from field: string refusal = 3; + */ + refusal: string; +}; + +/** + * Describes the message host.authority.v1.CheckResponse. + * Use `create(CheckResponseSchema)` to create a new message. + */ +export const CheckResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 12); + +/** + * KeysRequest asks for the verification keys. + * + * @generated from message host.authority.v1.KeysRequest + */ +export type KeysRequest = Message<"host.authority.v1.KeysRequest"> & { +}; + +/** + * Describes the message host.authority.v1.KeysRequest. + * Use `create(KeysRequestSchema)` to create a new message. + */ +export const KeysRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 13); + +/** + * KeysResponse is the issuer and its keys. + * + * @generated from message host.authority.v1.KeysResponse + */ +export type KeysResponse = Message<"host.authority.v1.KeysResponse"> & { + /** + * The issuer every capability names. + * + * @generated from field: string issuer = 1; + */ + issuer: string; + + /** + * The public keys by key id. + * + * @generated from field: repeated host.authority.v1.PublicKey keys = 2; + */ + keys: PublicKey[]; +}; + +/** + * Describes the message host.authority.v1.KeysResponse. + * Use `create(KeysResponseSchema)` to create a new message. + */ +export const KeysResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 14); + +/** + * PublicKey is one verification key. + * + * @generated from message host.authority.v1.PublicKey + */ +export type PublicKey = Message<"host.authority.v1.PublicKey"> & { + /** + * The key id a capability names. + * + * @generated from field: string key_id = 1; + */ + keyId: string; + + /** + * The signature algorithm; Ed25519. + * + * @generated from field: string algorithm = 2; + */ + algorithm: string; + + /** + * The raw public key bytes. + * + * @generated from field: bytes public_key = 3; + */ + publicKey: Uint8Array; +}; + +/** + * Describes the message host.authority.v1.PublicKey. + * Use `create(PublicKeySchema)` to create a new message. + */ +export const PublicKeySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 15); + +/** + * ListResourcesRequest asks about one kind and one action. + * + * @generated from message host.authority.v1.ListResourcesRequest + */ +export type ListResourcesRequest = Message<"host.authority.v1.ListResourcesRequest"> & { + /** + * The capability presented. + * + * @generated from field: string token = 1; + */ + token: string; + + /** + * The audience the caller is; a capability addressed elsewhere is refused. + * + * @generated from field: string audience = 2; + */ + audience: string; + + /** + * The resource kind, as the audience module serves it. + * + * @generated from field: string resource_kind = 3; + */ + resourceKind: string; + + /** + * The action, in the kind's own vocabulary. + * + * @generated from field: string action = 4; + */ + action: string; +}; + +/** + * Describes the message host.authority.v1.ListResourcesRequest. + * Use `create(ListResourcesRequestSchema)` to create a new message. + */ +export const ListResourcesRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 16); + +/** + * ListResourcesResponse is every grant the capability holds on the kind for the + * action. A resource is covered when any grant matches its id; no grant means + * none. A capability that does not hold the action at all is refused instead. + * + * @generated from message host.authority.v1.ListResourcesResponse + */ +export type ListResourcesResponse = Message<"host.authority.v1.ListResourcesResponse"> & { + /** + * @generated from field: repeated host.authority.v1.ResourceGrant grants = 1; + */ + grants: ResourceGrant[]; +}; + +/** + * Describes the message host.authority.v1.ListResourcesResponse. + * Use `create(ListResourcesResponseSchema)` to create a new message. + */ +export const ListResourcesResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 17); + +/** + * ResourceGrant is one way of naming covered resources. Exactly one is set. + * + * @generated from message host.authority.v1.ResourceGrant + */ +export type ResourceGrant = Message<"host.authority.v1.ResourceGrant"> & { + /** + * @generated from oneof host.authority.v1.ResourceGrant.match + */ + match: { + /** + * Every resource of the kind. + * + * @generated from field: bool all = 1; + */ + value: boolean; + case: "all"; + } | { + /** + * Exactly this resource id. + * + * @generated from field: string id = 2; + */ + value: string; + case: "id"; + } | { + /** + * Every resource whose id starts with this. + * + * @generated from field: string prefix = 3; + */ + value: string; + case: "prefix"; + } | { + /** + * Every resource whose whole id matches this RE2 expression, at most 256 + * bytes; it is matched against the entire id, as if anchored. + * + * @generated from field: string pattern = 4; + */ + value: string; + case: "pattern"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message host.authority.v1.ResourceGrant. + * Use `create(ResourceGrantSchema)` to create a new message. + */ +export const ResourceGrantSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_authority, 18); + +/** + * AuthorityService is implemented by the host (accounts) and by the authority gateway. + * + * @generated from service host.authority.v1.AuthorityService + */ +export const AuthorityService: GenService<{ + /** + * Issue signs a Work Context for one owner shape, one audience and the scopes + * asked for, bounded by what the owner holds and what the audience declared. + * Served over REST too (POST /v1/authority:issue), for a module with no + * generated client of its own, as a Python service: the same request and + * answer, in JSON. + * + * @generated from rpc host.authority.v1.AuthorityService.Issue + */ + issue: { + methodKind: "unary"; + input: typeof IssueRequestSchema; + output: typeof IssueResponseSchema; + }, + /** + * Check verifies a presented Work Context for the audience the caller is, + * re-checking the authorization revision and the seal live. + * + * @generated from rpc host.authority.v1.AuthorityService.Check + */ + check: { + methodKind: "unary"; + input: typeof CheckRequestSchema; + output: typeof CheckResponseSchema; + }, + /** + * Keys returns the issuer and the public keys a verifier pins. + * + * @generated from rpc host.authority.v1.AuthorityService.Keys + */ + keys: { + methodKind: "unary"; + input: typeof KeysRequestSchema; + output: typeof KeysResponseSchema; + }, + /** + * ListResources answers which resources of one kind a checked capability may + * act on with one action, decided by the authority at the time of the call + * rather than carried in the capability. A module maps the answer onto its + * own resources; the authority knows them only by id. + * + * @generated from rpc host.authority.v1.AuthorityService.ListResources + */ + listResources: { + methodKind: "unary"; + input: typeof ListResourcesRequestSchema; + output: typeof ListResourcesResponseSchema; + }, + /** + * Delegate records a signed-in person's grant to a module (the caller's + * bearer is the person); Issue with a delegation acts on it. + * + * @generated from rpc host.authority.v1.AuthorityService.Delegate + */ + delegate: { + methodKind: "unary"; + input: typeof DelegateRequestSchema; + output: typeof DelegationSchema; + }, + /** + * RevokeDelegation ends a delegation; the next Issue from it is refused. + * + * @generated from rpc host.authority.v1.AuthorityService.RevokeDelegation + */ + revokeDelegation: { + methodKind: "unary"; + input: typeof RevokeDelegationRequestSchema; + output: typeof RevokeDelegationResponseSchema; + }, + /** + * GetDelegation reads one delegation for the module it is granted to (by its + * workload credential) or for its grantor (by the caller's bearer): whether it + * still stands. Work that outlives the capability that started it asks this + * between issues, so a revoke stops it even when it calls no other module. + * + * @generated from rpc host.authority.v1.AuthorityService.GetDelegation + */ + getDelegation: { + methodKind: "unary"; + input: typeof GetDelegationRequestSchema; + output: typeof DelegationSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_host_authority_v1_authority, 0); + diff --git a/libraries/authority-client/typescript/src/gen/host/authority/v1/work_context_pb.ts b/libraries/authority-client/typescript/src/gen/host/authority/v1/work_context_pb.ts new file mode 100644 index 00000000..85fa0b03 --- /dev/null +++ b/libraries/authority-client/typescript/src/gen/host/authority/v1/work_context_pb.ts @@ -0,0 +1,651 @@ +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=js" +// @generated from file host/authority/v1/work_context.proto (package host.authority.v1, syntax proto3) +/* eslint-disable */ + +// Moved from codefly-dev/core proto/codefly/base/v0/work_context.proto on 2026-10-21. +// Every message, field name and field number is unchanged, so the deterministic +// bytes a verifier reads are identical; only the package and the Go import path +// differ. The conformance fixtures prove it. A change in meaning is a V2 message +// beside the V1, never a field reinterpreted. + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_buf_validate_validate } from "../../../buf/validate/validate_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file host/authority/v1/work_context.proto. + */ +export const file_host_authority_v1_work_context: GenFile = /*@__PURE__*/ + fileDesc("CiRob3N0L2F1dGhvcml0eS92MS93b3JrX2NvbnRleHQucHJvdG8SEWhvc3QuYXV0aG9yaXR5LnYxItAJCg1Xb3JrQ29udGV4dFYxEisKA3R5cBgBIAEoCUIeukgbchkKF2NvZGVmbHkud29yay1jb250ZXh0L3YxEiEKCWFsZ29yaXRobRgCIAEoCUIOukgLcgkKB0VkMjU1MTkSGgoGa2V5X2lkGAMgASgJQgq6SAdyBRABGIABEhoKBmlzc3VlchgEIAEoCUIKukgHcgUQARiABBIcCghhdWRpZW5jZRgFIAEoCUIKukgHcgUQARiABBIXCg9ub3RfYmVmb3JlX3VuaXgYBiABKAMSFgoOaXNzdWVkX2F0X3VuaXgYByABKAMSFwoPZXhwaXJlc19hdF91bml4GAggASgDEhkKBW5vbmNlGAkgASgJQgq6SAdyBRABGIAEEh4KFmF1dGhvcml6YXRpb25fcmV2aXNpb24YCiABKAQSNAoNcmVwbGF5X3BvbGljeRgLIAEoCUIdukgachhSCmlkZW1wb3RlbnRSCnNpbmdsZS11c2USHQoJdGVuYW50X2lkGAwgASgJQgq6SAdyBRABGIAEEiYKEm93bmVyX3ByaW5jaXBhbF9pZBgNIAEoCUIKukgHcgUQARiABBIbCgd0YXNrX2lkGA4gASgJQgq6SAdyBRABGIAEEh4KCnNlc3Npb25faWQYDyABKAlCCrpIB3IFEAEYgAQSKgoRcGFyZW50X3Nlc3Npb25faWQYECABKAlCCrpIB3IFEAEYgARIAIgBARI4ChBhdXRob3JpdHlfc2NvcGVzGBEgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESMwoLYWN0b3JfY2hhaW4YEiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrQWN0b3JWMRIcChRhdHRyaWJ1dGlvbl90ZWFtX2lkcxgTIAMoCRIlCgx3b3Jrc3BhY2VfaWQYFCABKAlCCrpIB3IFEAEYgARIAYgBARIjCgpwcm9qZWN0X2lkGBUgASgJQgq6SAdyBRABGIAESAKIAQESLQoUb3duZXJfcHJpbmNpcGFsX2tpbmQYFiABKAlCCrpIB3IFEAEYgAFIA4gBARInCg5vd25lcl9hZ2VudF9pZBgXIAEoCUIKukgHcgUQARiABEgEiAEBEigKD29yZ2FuaXphdGlvbl9pZBgZIAEoCUIKukgHcgUQARiABEgFiAEBEjkKCWdyYW50X2hvcBgYIAEoCzIhLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtHcmFudEhvcFYxSAaIAQESMwoEc2VhbBgaIAEoCzIdLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtTZWFsVjFCBrpIA8gBARJJChFvcGVyYXRpb25fYmluZGluZxgbIAEoCzIpLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtPcGVyYXRpb25CaW5kaW5nVjFIB4gBAUIUChJfcGFyZW50X3Nlc3Npb25faWRCDwoNX3dvcmtzcGFjZV9pZEINCgtfcHJvamVjdF9pZEIXChVfb3duZXJfcHJpbmNpcGFsX2tpbmRCEQoPX293bmVyX2FnZW50X2lkQhIKEF9vcmdhbml6YXRpb25faWRCDAoKX2dyYW50X2hvcEIUChJfb3BlcmF0aW9uX2JpbmRpbmciqAEKCldvcmtTZWFsVjESIAoPcHJpbmNpcGFsX2Vwb2NoGAEgASgEQge6SAQyAigBEiMKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIKukgHcgUQARiABBImChVpbnN0YWxsYXRpb25fcmV2aXNpb24YAyABKARCB7pIBDICKAFKBAgEEAVKBAgFEAZSEWJ1aWxkX2luY2FybmF0aW9uUgxpbWFnZV9kaWdlc3QicQoWV29ya09wZXJhdGlvbkJpbmRpbmdWMRIeCgpiaW5kaW5nX2lkGAEgASgJQgq6SAdyBRABGIAEEhkKCHJldmlzaW9uGAIgASgEQge6SAQyAigBEhwKC2luY2FybmF0aW9uGAMgASgEQge6SAQyAigBIlcKC1dvcmtTY29wZVYxEiEKDXJlc291cmNlX2tpbmQYASABKAlCCrpIB3IFEAEYgAESDwoHYWN0aW9ucxgCIAMoCRIUCgxyZXNvdXJjZV9pZHMYAyADKAki7gIKC1dvcmtBY3RvclYxEiAKDHByaW5jaXBhbF9pZBgBIAEoCUIKukgHcgUQARiABBIiCg5wcmluY2lwYWxfa2luZBgCIAEoCUIKukgHcgUQARiAARIhCg1kZWxlZ2F0aW9uX2lkGAMgASgJQgq6SAdyBRABGIAEEjYKDmdyYW50ZWRfc2NvcGVzGAQgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESIQoIYWdlbnRfaWQYBSABKAlCCrpIB3IFEAEYgARIAIgBARIoCg9vcmdhbml6YXRpb25faWQYBiABKAlCCrpIB3IFEAEYgARIAYgBARIjCg9wcmluY2lwYWxfZXBvY2gYByABKARCCrpIB8gBATICKAFCCwoJX2FnZW50X2lkQhIKEF9vcmdhbml6YXRpb25faWRKBAgIEAlKBAgJEApSDGltYWdlX2RpZ2VzdFIRYnVpbGRfaW5jYXJuYXRpb24i8AEKDldvcmtHcmFudEhvcFYxEhwKCGdyYW50X2lkGAEgASgJQgq6SAdyBRABGIAEEkAKCWFwcHJvdmVycxgCIAMoCzIhLmhvc3QuYXV0aG9yaXR5LnYxLldvcmtBcHByb3ZlclYxQgq6SAeSAQQIARBAEj0KDWdyYW50ZWRfc2NvcGUYAyABKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMUIGukgDyAEBEhsKB3N1YmplY3QYBCABKAlCCrpIB3IFEAEYgAQSIgoOcmVxdWVzdF9kaWdlc3QYBSABKAlCCrpIB3IFEAEYgAQiVgoOV29ya0FwcHJvdmVyVjESIAoMcHJpbmNpcGFsX2lkGAEgASgJQgq6SAdyBRABGIAEEiIKDnByaW5jaXBhbF9raW5kGAIgASgJQgq6SAdyBRABGIABItQBChJBcHByb3ZhbFJlcXVpcmVkVjESHgoKcmVxdWVzdF9pZBgBIAEoCUIKukgHcgUQARiABBI/Cg9yZXF1ZXN0ZWRfc2NvcGUYAiABKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMUIGukgDyAEBEhsKB3N1YmplY3QYAyABKAlCCrpIB3IFEAEYgAQSIgoOcmVxdWVzdF9kaWdlc3QYBCABKAlCCrpIB3IFEAEYgAQSHAoIYXVkaWVuY2UYBSABKAlCCrpIB3IFEAEYgARCL1otZ2F0ZXdheS9wa2cvZ2VuL2hvc3QvYXV0aG9yaXR5L3YxO2F1dGhvcml0eXYxYgZwcm90bzM", [file_buf_validate_validate]); + +/** + * WorkContextV1 is the product-neutral identity and authority capability for + * one Task/Session hop. The signed wire representation is owned by Codefly + * SDKs; applications must never serialize this message into a bearer token or + * construct the transport header themselves. + * + * Task identity is the tuple (tenant_id, owner_principal_id, task_id). + * Starting a child agent keeps that tuple and exchanges the parent capability + * for a new child session capability. It never mutates an existing capability. + * + * @generated from message host.authority.v1.WorkContextV1 + */ +export type WorkContextV1 = Message<"host.authority.v1.WorkContextV1"> & { + /** + * typ is fixed to "codefly.work-context/v1". + * + * @generated from field: string typ = 1; + */ + typ: string; + + /** + * algorithm is fixed to "Ed25519". + * + * @generated from field: string algorithm = 2; + */ + algorithm: string; + + /** + * key_id selects one public verification key during rotation. + * + * @generated from field: string key_id = 3; + */ + keyId: string; + + /** + * issuer is the authority that verified identity and minted this context. + * + * @generated from field: string issuer = 4; + */ + issuer: string; + + /** + * audience is the exact service or trust boundary allowed to consume it. + * + * @generated from field: string audience = 5; + */ + audience: string; + + /** + * not_before_unix is the earliest instant a verifier may accept this + * capability. + * + * @generated from field: int64 not_before_unix = 6; + */ + notBeforeUnix: bigint; + + /** + * issued_at_unix is when the issuer minted it. + * + * @generated from field: int64 issued_at_unix = 7; + */ + issuedAtUnix: bigint; + + /** + * expires_at_unix is the instant it stops being accepted. Exchanging a + * parent capability for a child one never extends it. + * + * @generated from field: int64 expires_at_unix = 8; + */ + expiresAtUnix: bigint; + + /** + * nonce identifies the capability. Replay behavior is separately explicit. + * + * @generated from field: string nonce = 9; + */ + nonce: string; + + /** + * authorization_revision is the issuer's current membership/policy revision. + * + * @generated from field: uint64 authorization_revision = 10; + */ + authorizationRevision: bigint; + + /** + * replay_policy is "idempotent" or "single-use". A verifier still needs a + * durable replay store to enforce single-use consumption. + * + * @generated from field: string replay_policy = 11; + */ + replayPolicy: string; + + /** + * tenant_id is the first element of the task identity tuple and is carried + * unchanged across every hop. + * + * @generated from field: string tenant_id = 12; + */ + tenantId: string; + + /** + * owner_principal_id is the principal on whose authority the task runs. It + * is the delegating owner, not necessarily the current actor. + * + * @generated from field: string owner_principal_id = 13; + */ + ownerPrincipalId: string; + + /** + * task_id completes the task identity tuple and survives child agents. + * + * @generated from field: string task_id = 14; + */ + taskId: string; + + /** + * session_id identifies this hop. Starting a child agent mints a new one + * rather than mutating this capability. + * + * @generated from field: string session_id = 15; + */ + sessionId: string; + + /** + * parent_session_id is the session this one was exchanged from; absent on + * the first session of a task. + * + * @generated from field: optional string parent_session_id = 16; + */ + parentSessionId?: string; + + /** + * authority_scopes are the maximum scopes the owner delegated into this + * Work Context. Each actor hop's granted_scopes must be a subset of the + * preceding effective scopes. + * + * @generated from field: repeated host.authority.v1.WorkScopeV1 authority_scopes = 17; + */ + authorityScopes: WorkScopeV1[]; + + /** + * actor_chain is ordered from the first delegated actor to the current actor. + * The owner is not duplicated here merely to make a direct call look uniform. + * Every hop attenuates the one before it, except the final hop of a + * capability carrying grant_hop. + * + * @generated from field: repeated host.authority.v1.WorkActorV1 actor_chain = 18; + */ + actorChain: WorkActorV1[]; + + /** + * attribution_team_ids is an immutable verified attribution snapshot. It + * explains ownership; current authorization is still evaluated separately. + * + * @generated from field: repeated string attribution_team_ids = 19; + */ + attributionTeamIds: string[]; + + /** + * workspace_id is the workspace the task runs against, when the task is + * scoped to one. + * + * @generated from field: optional string workspace_id = 20; + */ + workspaceId?: string; + + /** + * project_id narrows workspace_id to a single project, when the task is + * scoped that far. + * + * @generated from field: optional string project_id = 21; + */ + projectId?: string; + + /** + * owner_principal_kind distinguishes the sort of principal the owner is, so + * a verifier can present the owner without resolving it against a directory. + * Optional on the wire: a capability minted before this field existed is + * still a valid capability, and an archived receipt embedding one still + * verifies. Deriving an identity from a capability that omits it is the + * error, raised where the identity is built. + * + * @generated from field: optional string owner_principal_kind = 22; + */ + ownerPrincipalKind?: string; + + /** + * owner_agent_id is the owner's agent manifest identity. Required when + * owner_principal_kind is "agent" and absent otherwise. + * + * @generated from field: optional string owner_agent_id = 23; + */ + ownerAgentId?: string; + + /** + * organization_id is the organization the task runs in. It is not the + * tenant: a tenant may hold several organizations, and authorization is + * scoped per organization. An actor hop may name a different one. + * + * @generated from field: optional string organization_id = 25; + */ + organizationId?: string; + + /** + * grant_hop is the approval grant this capability was minted under. It is + * the one audited exception to the attenuation rule: the final actor hop + * holds exactly granted_scope, which the preceding hop need not contain. + * Present only on a single-use child capability minted for one approved + * call; absent on every ordinary session. + * + * @generated from field: optional host.authority.v1.WorkGrantHopV1 grant_hop = 24; + */ + grantHop?: WorkGrantHopV1; + + /** + * seal binds this capability to one installation and one execution: the + * principal's epoch, the installation and revision it was minted for, and + * the build incarnation it was minted on. A verifier compares every field + * against the live value and refuses on any mismatch, which is what stops a + * capability outliving the installation or the build it was issued against. + * + * REQUIRED. An earlier version of this field was optional, with the stated + * reason that a schema rule would invalidate every archived capability and + * every receipt embedding one, so the requirement belonged in the verifier. + * Two independent reviews named that for what it is: a backward-compatibility + * hedge, which the rules in force forbid. It also meant every reader other + * than the verifier saw a capability's binding to its installation as + * optional, so the strongest check in the model was the easiest one for a + * consumer to not notice. + * + * Archived capabilities and receipts are historical DATA. If they must be + * read after this, they get a snapshot type of their own rather than keeping + * the live credential permanently weaker than it should be. + * + * @generated from field: host.authority.v1.WorkSealV1 seal = 26; + */ + seal?: WorkSealV1; + + /** + * operation_binding is the unit of authority this capability exercises, + * sealed by its id, revision and incarnation. Present on an operation + * context and absent on a session that exercises none. + * + * The id is what a verifier looks the binding up by, exactly. Nothing + * searches the bindings for one that contains the capability's scopes: a + * search is a predicate someone wrote, and a predicate one case too + * generous grants authority nobody reviewed. + * + * @generated from field: optional host.authority.v1.WorkOperationBindingV1 operation_binding = 27; + */ + operationBinding?: WorkOperationBindingV1; +}; + +/** + * Describes the message host.authority.v1.WorkContextV1. + * Use `create(WorkContextV1Schema)` to create a new message. + */ +export const WorkContextV1Schema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_work_context, 0); + +/** + * WorkSealV1 binds a capability to one installation and one execution. Its + * principal_epoch is the TASK OWNER'S; an actor hop carries its own in + * WorkActorV1.principal_epoch, because an actor's authority is narrowed + * independently of the owner's. Every + * field is a value the issuer holds live and the verifier compares exactly, so + * a capability minted before an installation was revised, a principal's + * authority was reset, or a build was replaced stops verifying at that moment + * rather than at its own expiry. + * + * @generated from message host.authority.v1.WorkSealV1 + */ +export type WorkSealV1 = Message<"host.authority.v1.WorkSealV1"> & { + /** + * principal_epoch is the principal's current epoch. Advancing it + * invalidates every capability minted for that principal, which is how a + * compromised principal is cut off without waiting for expiry. + * + * @generated from field: uint64 principal_epoch = 1; + */ + principalEpoch: bigint; + + /** + * installation_id identifies one organization's installation of one module. + * Authority is held through an installation, so a capability that names none + * is bound to nothing. + * + * @generated from field: string installation_id = 2; + */ + installationId: string; + + /** + * installation_revision is the installation's revision at mint time. A + * verifier holding a different revision refuses: the scope of an + * installation changes with its revision, so a capability sealed to an + * earlier one asks for authority under terms that no longer apply. + * + * @generated from field: uint64 installation_revision = 3; + */ + installationRevision: bigint; +}; + +/** + * Describes the message host.authority.v1.WorkSealV1. + * Use `create(WorkSealV1Schema)` to create a new message. + */ +export const WorkSealV1Schema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_work_context, 1); + +/** + * WorkOperationBindingV1 is the unit of authority an operation context + * exercises, sealed so that a verifier resolves exactly one binding and + * compares it rather than deciding which binding the capability's scopes fit. + * + * @generated from message host.authority.v1.WorkOperationBindingV1 + */ +export type WorkOperationBindingV1 = Message<"host.authority.v1.WorkOperationBindingV1"> & { + /** + * binding_id is the host's identifier for the binding. The verifier looks it + * up EXACTLY: nothing searches the bindings for one that fits a capability's + * scopes, because a search is a predicate somebody wrote and a predicate one + * case too generous grants authority nobody reviewed. + * + * This comment used to say the id "is never derived from the binding's + * contents, because a derived id is one a caller can compute for a binding + * it was never granted". I refuted a review finding against that sentence by + * reading it in isolation and judging it correct. The reviewer was right and + * I was wrong: solutionhost/authority.go says the opposite in the package + * that owns the id — deriving it deterministically is delivery's business + * and better than random, and "predictability costs nothing here: an id is + * neither a secret nor a capability". + * + * Predictability costs nothing because ENTITLEMENT is what refuses a binding + * a caller does not hold, not obscurity: the verifier requires the resolved + * binding to be granted to the exercising principal within the installation + * the capability is sealed to. Guessing an id gets you a refusal. Resting + * the rule on unguessability was the retracted reasoning, and leaving it + * here contradicted the package it describes. + * + * @generated from field: string binding_id = 1; + */ + bindingId: string; + + /** + * revision is the binding's revision at mint time. A verifier holding a + * different revision refuses. + * + * @generated from field: uint64 revision = 2; + */ + revision: bigint; + + /** + * incarnation is the binding's incarnation at mint time, which changes when + * the binding is re-established rather than merely revised. + * + * @generated from field: uint64 incarnation = 3; + */ + incarnation: bigint; +}; + +/** + * Describes the message host.authority.v1.WorkOperationBindingV1. + * Use `create(WorkOperationBindingV1Schema)` to create a new message. + */ +export const WorkOperationBindingV1Schema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_work_context, 2); + +/** + * WorkScopeV1 is a structured, product-neutral capability scope. Empty + * resource_ids means every resource of resource_kind; a child may narrow that + * wildcard to explicit IDs but may never widen an explicit parent set. + * + * @generated from message host.authority.v1.WorkScopeV1 + */ +export type WorkScopeV1 = Message<"host.authority.v1.WorkScopeV1"> & { + /** + * resource_kind is the class of resource the scope grants against. + * + * @generated from field: string resource_kind = 1; + */ + resourceKind: string; + + /** + * actions are the operations permitted on that kind. + * + * @generated from field: repeated string actions = 2; + */ + actions: string[]; + + /** + * resource_ids narrows the scope to specific resources. Empty means every + * resource of resource_kind. + * + * @generated from field: repeated string resource_ids = 3; + */ + resourceIds: string[]; +}; + +/** + * Describes the message host.authority.v1.WorkScopeV1. + * Use `create(WorkScopeV1Schema)` to create a new message. + */ +export const WorkScopeV1Schema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_work_context, 3); + +/** + * WorkActorV1 is one verified delegated actor. granted_scopes are the actor's + * effective scopes and must monotonically attenuate the previous hop. + * + * @generated from message host.authority.v1.WorkActorV1 + */ +export type WorkActorV1 = Message<"host.authority.v1.WorkActorV1"> & { + /** + * principal_id identifies the actor this hop delegated to. + * + * @generated from field: string principal_id = 1; + */ + principalId: string; + + /** + * principal_kind distinguishes the sort of principal, so a human and an + * agent bearing the same id are not conflated. + * + * @generated from field: string principal_kind = 2; + */ + principalKind: string; + + /** + * delegation_id identifies the delegation that produced this hop, so the + * chain stays auditable back to its grant. + * + * @generated from field: string delegation_id = 3; + */ + delegationId: string; + + /** + * granted_scopes are this actor's effective scopes, a subset of the + * preceding hop's. + * + * @generated from field: repeated host.authority.v1.WorkScopeV1 granted_scopes = 4; + */ + grantedScopes: WorkScopeV1[]; + + /** + * agent_id is the actor's agent manifest identity. Required when + * principal_kind is "agent" and absent otherwise. + * + * @generated from field: optional string agent_id = 5; + */ + agentId?: string; + + /** + * organization_id is the organization this actor belongs to, when it is not + * the one the task runs in. An org-bridge agent acts in an organization the + * owner does not belong to, and authorization is scoped to the actor's own. + * + * @generated from field: optional string organization_id = 6; + */ + organizationId?: string; + + /** + * principal_epoch is THIS actor's epoch, distinct from the owner's epoch in + * the seal. Advancing it invalidates every capability in which this + * principal is an actor, without touching the owner's. + * + * It exists because the seal's epoch is the task owner's, and an actor's + * authority can be narrowed independently of the owner's: a delegated + * operation context may have a person as its owner and a service principal + * as its actor, and narrowing that service principal must reach the + * capabilities it acts in. Without this field the only lever for a + * compromised actor is the tenant's authorization_revision, which cuts off + * every capability of the tenant. + * + * REQUIRED, for the reason seal gives. A hop with no epoch is a principal + * nothing can revoke, and leaving that expressible on the wire made it a + * shape a consumer could mint without noticing. + * + * @generated from field: uint64 principal_epoch = 7; + */ + principalEpoch: bigint; +}; + +/** + * Describes the message host.authority.v1.WorkActorV1. + * Use `create(WorkActorV1Schema)` to create a new message. + */ +export const WorkActorV1Schema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_work_context, 4); + +/** + * WorkGrantHopV1 records the approval that justifies a capability holding + * authority its delegating owner never held. It is minted only as a new child + * capability bound to one tool, one subject and one call, so the authority it + * adds is unusable anywhere else and expires with the grant window. + * + * @generated from message host.authority.v1.WorkGrantHopV1 + */ +export type WorkGrantHopV1 = Message<"host.authority.v1.WorkGrantHopV1"> & { + /** + * grant_id identifies the approval grant, so revoking the grant and + * auditing the call both reach the same record. + * + * @generated from field: string grant_id = 1; + */ + grantId: string; + + /** + * approvers are the principals whose decision produced the grant, every one + * of an N-of-M quorum, so the audit record is complete. + * + * @generated from field: repeated host.authority.v1.WorkApproverV1 approvers = 2; + */ + approvers: WorkApproverV1[]; + + /** + * granted_scope is exactly what the approval added: one resource_kind, one + * action and one resource_id. A grant never widens beyond that triple. + * + * @generated from field: host.authority.v1.WorkScopeV1 granted_scope = 3; + */ + grantedScope?: WorkScopeV1; + + /** + * subject is what the grant is pinned to, in the approvals engine's own + * vocabulary, so a grant approved for one subject cannot be spent on another. + * + * @generated from field: string subject = 4; + */ + subject: string; + + /** + * request_digest binds the grant to the exact call the approver saw. + * + * @generated from field: string request_digest = 5; + */ + requestDigest: string; +}; + +/** + * Describes the message host.authority.v1.WorkGrantHopV1. + * Use `create(WorkGrantHopV1Schema)` to create a new message. + */ +export const WorkGrantHopV1Schema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_work_context, 5); + +/** + * WorkApproverV1 is one principal whose decision produced a grant. It carries + * the kind as well as the id: a policy that requires a human approver cannot + * be written against bare identifiers. + * + * @generated from message host.authority.v1.WorkApproverV1 + */ +export type WorkApproverV1 = Message<"host.authority.v1.WorkApproverV1"> & { + /** + * principal_id identifies the approver. + * + * @generated from field: string principal_id = 1; + */ + principalId: string; + + /** + * principal_kind distinguishes the sort of principal the approver is. + * + * @generated from field: string principal_kind = 2; + */ + principalKind: string; +}; + +/** + * Describes the message host.authority.v1.WorkApproverV1. + * Use `create(WorkApproverV1Schema)` to create a new message. + */ +export const WorkApproverV1Schema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_work_context, 6); + +/** + * ApprovalRequiredV1 is the signal a policy decision point returns instead of + * a denial when the call is approvable: the authority is not held, but an + * approver may grant it. It names exactly what a WorkGrantHopV1 would have to + * carry, so a decision and the capability that satisfies it are the same shape. + * It is carried as a typed error detail on a FailedPrecondition status. + * + * @generated from message host.authority.v1.ApprovalRequiredV1 + */ +export type ApprovalRequiredV1 = Message<"host.authority.v1.ApprovalRequiredV1"> & { + /** + * request_id identifies the approval request the engine created, which the + * caller waits on and the resulting grant refers back to. + * + * @generated from field: string request_id = 1; + */ + requestId: string; + + /** + * requested_scope is the authority the call needs and the principal lacks. + * + * @generated from field: host.authority.v1.WorkScopeV1 requested_scope = 2; + */ + requestedScope?: WorkScopeV1; + + /** + * subject is what a grant would be pinned to. + * + * @generated from field: string subject = 3; + */ + subject: string; + + /** + * request_digest is the exact call put to the approver. + * + * @generated from field: string request_digest = 4; + */ + requestDigest: string; + + /** + * audience is the tool a granted capability must be minted for. + * + * @generated from field: string audience = 5; + */ + audience: string; +}; + +/** + * Describes the message host.authority.v1.ApprovalRequiredV1. + * Use `create(ApprovalRequiredV1Schema)` to create a new message. + */ +export const ApprovalRequiredV1Schema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_authority_v1_work_context, 7); + diff --git a/libraries/authority-client/typescript/src/gen/host/composition/v1/composition_pb.ts b/libraries/authority-client/typescript/src/gen/host/composition/v1/composition_pb.ts new file mode 100644 index 00000000..78d6194c --- /dev/null +++ b/libraries/authority-client/typescript/src/gen/host/composition/v1/composition_pb.ts @@ -0,0 +1,460 @@ +// @generated by protoc-gen-es v2.11.0 with parameter "target=ts,import_extension=js" +// @generated from file host/composition/v1/composition.proto (package host.composition.v1, syntax proto3) +/* eslint-disable */ + +// The composition interface: how the host learns what is composed. One +// operation. The render produces a manifest per module instance; the host +// derives from it the module's principal, its ceilings and its bindings. There +// is no envelope: the ceiling is the manifest. + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import type { WorkScopeV1 } from "../../authority/v1/work_context_pb.js"; +import { file_host_authority_v1_work_context } from "../../authority/v1/work_context_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file host/composition/v1/composition.proto. + */ +export const file_host_composition_v1_composition: GenFile = /*@__PURE__*/ + fileDesc("CiVob3N0L2NvbXBvc2l0aW9uL3YxL2NvbXBvc2l0aW9uLnByb3RvEhNob3N0LmNvbXBvc2l0aW9uLnYxItUBChRBcHBseU1hbmlmZXN0UmVxdWVzdBI3CghtYW5pZmVzdBgBIAEoCzIlLmhvc3QuY29tcG9zaXRpb24udjEuTW9kdWxlTWFuaWZlc3RWMRI2CgdjYXJyaWVyGAIgASgLMiUuaG9zdC5jb21wb3NpdGlvbi52MS5TaWduZWRNYW5pZmVzdFYxEhIKCmNyZWRlbnRpYWwYAyABKAkSOAoJYXV0aG9yaXR5GAQgASgLMiUuaG9zdC5jb21wb3NpdGlvbi52MS5TaWduZWRNYW5pZmVzdFYxIvEBChNBdXRob3JpdHlNYW5pZmVzdFYxEg4KBm1vZHVsZRgBIAEoCRISCgpnZW5lcmF0aW9uGAIgASgEEhwKFGNvcmVfbWFuaWZlc3RfZGlnZXN0GAMgASgJEjQKB21ldGhvZHMYBCADKAsyIy5ob3N0LmNvbXBvc2l0aW9uLnYxLk1ldGhvZFBvbGljeVYxEjYKDnNjb3BlX2NlaWxpbmdzGAUgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjESKgoFcm9sZXMYBiADKAsyGy5ob3N0LmNvbXBvc2l0aW9uLnYxLlJvbGVWMSJuCg5NZXRob2RQb2xpY3lWMRIOCgZtZXRob2QYASABKAkSMAoIcmVxdWlyZXMYAiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRIaChJhbnlfY2hlY2tlZF9jYWxsZXIYAyABKAgiNgoQU2lnbmVkTWFuaWZlc3RWMRIQCghkb2N1bWVudBgBIAEoDBIQCghlbnZlbG9wZRgCIAEoDCJYChVBcHBseU1hbmlmZXN0UmVzcG9uc2USEQoJcHJpbmNpcGFsGAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQSGAoQaW5zdGFsbGF0aW9uX2lkcxgDIAMoCSKdAgoQTW9kdWxlTWFuaWZlc3RWMRIOCgZtb2R1bGUYASABKAkSEQoJcHJpbmNpcGFsGAIgASgJEgwKBGtpbmQYAyABKAkSEgoKZ2VuZXJhdGlvbhgEIAEoBBIwCghiaW5kaW5ncxgFIAMoCzIeLmhvc3QuY29tcG9zaXRpb24udjEuQmluZGluZ1YxEjsKDnJlc291cmNlX2tpbmRzGAYgAygLMiMuaG9zdC5jb21wb3NpdGlvbi52MS5SZXNvdXJjZUtpbmRWMRIpCgRwYWdlGAcgASgLMhsuaG9zdC5jb21wb3NpdGlvbi52MS5QYWdlVjESKgoFcm9sZXMYCCADKAsyGy5ob3N0LmNvbXBvc2l0aW9uLnYxLlJvbGVWMSJGCgZSb2xlVjESDAoEbmFtZRgBIAEoCRIuCgZzY29wZXMYAiADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMSLQAQoJQmluZGluZ1YxEgoKAmlkGAEgASgJEhAKCGF1ZGllbmNlGAIgASgJEjUKDWludm9rZV9zY29wZXMYAyADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRI3Cg9oZWFkbGVzc19zY29wZXMYBCADKAsyHi5ob3N0LmF1dGhvcml0eS52MS5Xb3JrU2NvcGVWMRI1Cg1sb29rdXBfc2NvcGVzGAUgAygLMh4uaG9zdC5hdXRob3JpdHkudjEuV29ya1Njb3BlVjEiLwoOUmVzb3VyY2VLaW5kVjESDAoEa2luZBgBIAEoCRIPCgdhY3Rpb25zGAIgAygJIiYKBlBhZ2VWMRINCgV0aXRsZRgBIAEoCRINCgVvcmRlchgCIAEoBTJ8ChJDb21wb3NpdGlvblNlcnZpY2USZgoNQXBwbHlNYW5pZmVzdBIpLmhvc3QuY29tcG9zaXRpb24udjEuQXBwbHlNYW5pZmVzdFJlcXVlc3QaKi5ob3N0LmNvbXBvc2l0aW9uLnYxLkFwcGx5TWFuaWZlc3RSZXNwb25zZUIzWjFnYXRld2F5L3BrZy9nZW4vaG9zdC9jb21wb3NpdGlvbi92MTtjb21wb3NpdGlvbnYxYgZwcm90bzM", [file_host_authority_v1_work_context]); + +/** + * ApplyManifestRequest carries the manifest. + * + * @generated from message host.composition.v1.ApplyManifestRequest + */ +export type ApplyManifestRequest = Message<"host.composition.v1.ApplyManifestRequest"> & { + /** + * The sketch of a manifest, kept through the transition. An implementation + * that verifies deliveries ignores it when a carrier is present. + * + * @generated from field: host.composition.v1.ModuleManifestV1 manifest = 1; + */ + manifest?: ModuleManifestV1; + + /** + * The delivery as Codefly signs it: core's module manifest and the DSSE + * envelope over it. An implementation verifies the envelope against the release + * keys it pins and reads the manifest from the verified document, never from + * the request, so what composes a module is the same signed document on the + * authority gateway and on the host. + * + * @generated from field: host.composition.v1.SignedManifestV1 carrier = 2; + */ + carrier?: SignedManifestV1; + + /** + * The credential of the workload delivering the manifest, as that + * implementation attests workloads: the launcher's on the authority gateway, the + * delivery service account's on the host. A manifest is delivered by its own + * module's delivery workload and by nothing else. + * + * @generated from field: string credential = 3; + */ + credential: string; + + /** + * The authority's own document for this delivery, an AuthorityManifestV1: + * what the module declares that the authority enforces and core's manifest + * does not carry. Signed in the same envelope format by the same key as + * carrier, and naming carrier's digest. Absent means the module declares no + * method policy, and every Check that names one of its methods is refused. + * + * @generated from field: host.composition.v1.SignedManifestV1 authority = 4; + */ + authority?: SignedManifestV1; +}; + +/** + * Describes the message host.composition.v1.ApplyManifestRequest. + * Use `create(ApplyManifestRequestSchema)` to create a new message. + */ +export const ApplyManifestRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_composition_v1_composition, 0); + +/** + * AuthorityManifestV1 is the authority's document for one delivery. + * + * @generated from message host.composition.v1.AuthorityManifestV1 + */ +export type AuthorityManifestV1 = Message<"host.composition.v1.AuthorityManifestV1"> & { + /** + * The module, as the core manifest it travels with names it. + * + * @generated from field: string module = 1; + */ + module: string; + + /** + * The generation, equal to that core manifest's. + * + * @generated from field: uint64 generation = 2; + */ + generation: bigint; + + /** + * "sha256:" of the core manifest document delivered with it. + * + * @generated from field: string core_manifest_digest = 3; + */ + coreManifestDigest: string; + + /** + * Every method the module's published contracts serve, with what each + * declares. A method that declares nothing is listed with neither field set. + * + * @generated from field: repeated host.composition.v1.MethodPolicyV1 methods = 4; + */ + methods: MethodPolicyV1[]; + + /** + * The module's own resource kinds with the actions its contract declares on + * each (its scope ceilings). Core's manifest names the kinds alone, so without + * these a kind serves any action and a person's ask for everything they hold + * there cannot be enumerated. Each names a kind core's manifest declares. + * + * @generated from field: repeated host.authority.v1.WorkScopeV1 scope_ceilings = 5; + */ + scopeCeilings: WorkScopeV1[]; + + /** + * The roles a solution declares a person may hold on an installation of it, + * as ModuleManifestV1.roles declares them through the sketch: each within + * the solution's own kinds (its scope ceilings), one of them installer. + * Core's manifest carries no roles, so they travel here until it does. A + * module declares none; a module's delivery listing any is refused. + * + * @generated from field: repeated host.composition.v1.RoleV1 roles = 6; + */ + roles: RoleV1[]; +}; + +/** + * Describes the message host.composition.v1.AuthorityManifestV1. + * Use `create(AuthorityManifestV1Schema)` to create a new message. + */ +export const AuthorityManifestV1Schema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_composition_v1_composition, 1); + +/** + * MethodPolicyV1 is what a call to one method must hold, as the module's + * contract declares it with the authority.hop.v1.policy option. + * + * @generated from message host.composition.v1.MethodPolicyV1 + */ +export type MethodPolicyV1 = Message<"host.composition.v1.MethodPolicyV1"> & { + /** + * The method: "/package.Service/Method" for a gRPC method, or an HTTP route + * "