diff --git a/module/SOLUTION_REGISTRATION.md b/module/SOLUTION_REGISTRATION.md index c258ddf13..997fefd13 100644 --- a/module/SOLUTION_REGISTRATION.md +++ b/module/SOLUTION_REGISTRATION.md @@ -221,7 +221,20 @@ Four distinctions the projections keep: the caller's own organization (a platform super administrator, who passes every organization-admin check, is the inherited exception), unlike the internal `ListInstallations` the gateway reads for a projected tenant. It reads - the installations and their exposure on one repeatable-read snapshot. + the installations and their exposure on one repeatable-read snapshot. Each + installed target says whether its applied generation declares pages + (`has_pages`, from the registration's frontend half — the record navigation is + built from, never the kind). The administration page offers access on every + active row, and on a row without pages says once, muted, when a grant still + matters: a solution may call that component on a person's own session, and the + host then admits only a person with access there. The applied manifest does not + yet record that session consumption, so the page cannot tell which headless + components need a grant and does not hide the action. + A refusal from these RPCs carries the reason in words: a status wrapped beneath + the business error answers with its own message, never the chain's + `rpc error: code = … desc = …` text. A failed installation offers + `RetryInstallation` as "Retry installation", so recovering never needs an + uninstall. - An unreadable registry is still `503`, and a missing or malformed `client` still `400`. Neither ever renders as an empty list. diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 467ab5af0..592d233fa 100644 Binary files a/module/contracts/api/accounts/authority/contract.binpb and b/module/contracts/api/accounts/authority/contract.binpb differ diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/audit.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/audit.proto index ee8451f18..03e42a57c 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/audit.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/audit.proto @@ -34,6 +34,12 @@ message AuditEvent { // a call made from the host's own web session. It answers "what did they do // it through", which actor_id does not. string client_id = 15; + // actor_module names the module whose own principal is actor_id, by the + // prefix it is declared under in this host's module principal registry, so a + // reader can say "Module " instead of an unresolved id. Empty for every + // other actor, and for a module this host no longer declares. Read at query + // time; nothing about it is stored on the row. + string actor_module = 16; } message QueryAuditLogRequest { diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto index 7b8440c1b..8f272b8a9 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto @@ -685,6 +685,16 @@ message AvailableSolution { // unique index over (org_id, target_id) would refuse the second install anyway, // and an administrator should not have to discover that by being refused. bool installed = 9; + // True when the applied generation declares pages people open — the + // `frontend` block the host projects into its navigation. Read from the same + // applied record the navigation is built from, never from the kind: a module + // or a solution may declare pages, and either may be headless. It reports + // pages only: a grant on a headless installation can still matter, because a + // solution may call that component on a person's own session and the host + // then admits only a person with access there. The applied manifest does not + // record that session consumption yet, so this field alone does not decide + // whether access applies. + bool has_pages = 10; } message ListAvailableSolutionsRequest { diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 467ab5af0..592d233fa 100644 Binary files a/module/contracts/api/accounts/connect/contract.binpb and b/module/contracts/api/accounts/connect/contract.binpb differ diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/audit.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/audit.proto index ee8451f18..03e42a57c 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/audit.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/audit.proto @@ -34,6 +34,12 @@ message AuditEvent { // a call made from the host's own web session. It answers "what did they do // it through", which actor_id does not. string client_id = 15; + // actor_module names the module whose own principal is actor_id, by the + // prefix it is declared under in this host's module principal registry, so a + // reader can say "Module " instead of an unresolved id. Empty for every + // other actor, and for a module this host no longer declares. Read at query + // time; nothing about it is stored on the row. + string actor_module = 16; } message QueryAuditLogRequest { diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto index 7b8440c1b..8f272b8a9 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto @@ -685,6 +685,16 @@ message AvailableSolution { // unique index over (org_id, target_id) would refuse the second install anyway, // and an administrator should not have to discover that by being refused. bool installed = 9; + // True when the applied generation declares pages people open — the + // `frontend` block the host projects into its navigation. Read from the same + // applied record the navigation is built from, never from the kind: a module + // or a solution may declare pages, and either may be headless. It reports + // pages only: a grant on a headless installation can still matter, because a + // solution may call that component on a person's own session and the host + // then admits only a person with access there. The applied manifest does not + // record that session consumption yet, so this field alone does not decide + // whether access applies. + bool has_pages = 10; } message ListAvailableSolutionsRequest { diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 709b51a03..20a10f440 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:19b078707414a2946f30788ead4245bb1a4ed970789fd5aced5e5328702c259f", + "digest": "sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f", "services": [ { "name": "ModuleCapabilitiesService", @@ -65,7 +65,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:19b078707414a2946f30788ead4245bb1a4ed970789fd5aced5e5328702c259f", + "digest": "sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f", "services": [ { "name": "APIKeyService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 28fcb967a..4ffbf6f21 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:19b078707414a2946f30788ead4245bb1a4ed970789fd5aced5e5328702c259f + digest: sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:19b078707414a2946f30788ead4245bb1a4ed970789fd5aced5e5328702c259f + digest: sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index eb88298b0..c03039de8 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -24,6 +24,10 @@ Context exchanges below; no secret does. The principal directory lists stored identities only. Declared module authority is read by the capability checks and does not create synthetic directory rows. +`QueryAuditLog` instead names a module acting on its own on the row +(`actor_module`, the prefix the module principal registry declares it under) at +read time, so an audit reader shows "Module " rather than an unavailable +actor; a withdrawn module is no longer named. ## The one diagnostic the binary answers diff --git a/module/services/accounts/code/pkg/adapters/audit_actor_module_test.go b/module/services/accounts/code/pkg/adapters/audit_actor_module_test.go new file mode 100644 index 000000000..b9931f516 --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/audit_actor_module_test.go @@ -0,0 +1,23 @@ +package adapters + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "accounts/pkg/business" +) + +// A module acting on its own is recorded under its principal, which no +// directory row names; the audit read names it by the prefix the registry +// declares it under, and names no other actor. +func TestAuditActorModuleNamesOnlyADeclaredModulePrincipal(t *testing.T) { + modules := business.ModulePrincipalRegistry{ + business.ModulePrincipalID("example-chat"): {Prefix: "example-chat"}, + } + require.Equal(t, "example-chat", auditActorModule(modules, business.ModulePrincipalID("example-chat"))) + // A person, or a module this host no longer declares, is not a module here. + require.Empty(t, auditActorModule(modules, "11111111-1111-4111-8111-111111111111")) + require.Empty(t, auditActorModule(modules, business.ModulePrincipalID("example-withdrawn"))) + require.Empty(t, auditActorModule(modules, "")) +} diff --git a/module/services/accounts/code/pkg/adapters/installation_exposure_rpcs_test.go b/module/services/accounts/code/pkg/adapters/installation_exposure_rpcs_test.go index 180758e30..c119ed595 100644 --- a/module/services/accounts/code/pkg/adapters/installation_exposure_rpcs_test.go +++ b/module/services/accounts/code/pkg/adapters/installation_exposure_rpcs_test.go @@ -79,20 +79,29 @@ func TestInstallationExposureErrorMapping(t *testing.T) { name string err error want codes.Code + // The message the administrator reads; empty means the error's own text. + message string }{ // Another organization's installation id resolves to nothing under RLS. - {"unknown installation", business.NewStoreError(errors.New("installation x is not an active installation in this organization"), business.ErrTypeNotFound), codes.NotFound}, - {"team outside organization", fmt.Errorf("cannot expose installation to team: %w", business.ErrSubjectOutsideOrganization), codes.PermissionDenied}, - {"role exposes nothing", business.NewStoreError(business.ErrRoleDoesNotExposeSolution, business.ErrTypeValidation), codes.InvalidArgument}, + {"unknown installation", business.NewStoreError(errors.New("installation x is not an active installation in this organization"), business.ErrTypeNotFound), codes.NotFound, ""}, + {"team outside organization", fmt.Errorf("cannot expose installation to team: %w", business.ErrSubjectOutsideOrganization), codes.PermissionDenied, "subject does not belong to this organization"}, + {"role exposes nothing", business.NewStoreError(business.ErrRoleDoesNotExposeSolution, business.ErrTypeValidation), codes.InvalidArgument, ""}, // The host's own refusal of an unwitnessed narrowing keeps its words. - {"unwitnessed narrowing", fmt.Errorf("cannot revoke scope: %w: this host has no policy log, so authority cannot be narrowed", business.ErrPolicyLogUnreachable), codes.FailedPrecondition}, - {"store status", fmt.Errorf("cannot grant scope: %w", status.Error(codes.FailedPrecondition, `role "r" is not a role in this org`)), codes.FailedPrecondition}, + {"unwitnessed narrowing", fmt.Errorf("cannot revoke scope: %w: this host has no policy log, so authority cannot be narrowed", business.ErrPolicyLogUnreachable), codes.FailedPrecondition, ""}, + // A status carried beneath wrapping answers in its own words: the chain's + // text would read "cannot grant scope: rpc error: code = … desc = …". + {"store status", fmt.Errorf("cannot grant scope: %w", status.Error(codes.FailedPrecondition, `role "r" is not a role in this org`)), codes.FailedPrecondition, `role "r" is not a role in this org`}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { mapped := mapInstallationExposureError(tc.err) require.Equal(t, tc.want, status.Code(mapped)) - require.Equal(t, tc.err.Error(), status.Convert(mapped).Message()) + message := tc.message + if message == "" { + message = tc.err.Error() + } + require.Equal(t, message, status.Convert(mapped).Message()) + require.NotContains(t, status.Convert(mapped).Message(), "rpc error") }) } } diff --git a/module/services/accounts/code/pkg/adapters/installation_rpcs.go b/module/services/accounts/code/pkg/adapters/installation_rpcs.go index 2cea090e7..4c5c7746b 100644 --- a/module/services/accounts/code/pkg/adapters/installation_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/installation_rpcs.go @@ -191,6 +191,7 @@ func availableSolutionProto(entry *business.AvailableSolutionTarget) *gen.Availa OpenedGeneration: entry.OpenedGeneration, AppliedGeneration: entry.AppliedGeneration, Installed: entry.Installed, + HasPages: entry.HasPages, } } @@ -310,8 +311,11 @@ func mapInstallationExposureError(err error) error { if errors.Is(err, business.ErrPolicyLogUnreachable) { return status.Error(codes.FailedPrecondition, err.Error()) } - if st, ok := status.FromError(err); ok && st.Code() != codes.Unknown { - return status.Error(st.Code(), err.Error()) + // The status's own message, not the chain's text: a wrapped status reads as + // "cannot grant scope: rpc error: code = FailedPrecondition desc = …", and + // an administrator is shown that message as the host's reason. + if st := innerStatus(err); st != nil && st.Code() != codes.Unknown && st.Code() != codes.OK { + return status.Error(st.Code(), st.Message()) } return status.Error(codes.Internal, err.Error()) } @@ -336,7 +340,8 @@ func mapInstallationError(err error) error { if errors.Is(err, business.ErrPolicyLogUnreachable) { return status.Error(codes.Unavailable, err.Error()) } - if st, ok := status.FromError(err); ok { + // As above: the carried status's words, never the wrapping chain's. + if st := innerStatus(err); st != nil && st.Code() != codes.OK { return st.Err() } return status.Error(codes.Internal, err.Error()) diff --git a/module/services/accounts/code/pkg/adapters/installation_scope_rpcs_test.go b/module/services/accounts/code/pkg/adapters/installation_scope_rpcs_test.go index 50f6546b3..1fd92c708 100644 --- a/module/services/accounts/code/pkg/adapters/installation_scope_rpcs_test.go +++ b/module/services/accounts/code/pkg/adapters/installation_scope_rpcs_test.go @@ -4,6 +4,7 @@ import ( "accounts/pkg/business" gen "accounts/pkg/gen/saas/accounts/v1" "context" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -36,6 +37,12 @@ func TestNarrowInstallationRequiresOrgAdminAndOwnOrganization(t *testing.T) { func TestInstallationNarrowingErrorMapping(t *testing.T) { require.Equal(t, codes.Unavailable, status.Code(mapInstallationError(business.ErrPolicyLogUnreachable))) require.Equal(t, codes.PermissionDenied, status.Code(mapInstallationError(status.Error(codes.PermissionDenied, "outside ceiling")))) + // A wrapped status keeps its code and answers in its own words, never as + // "cannot retry installation: rpc error: code = … desc = …". + wrapped := mapInstallationError(fmt.Errorf("cannot retry installation: %w", + status.Error(codes.FailedPrecondition, "the installation is not in a failed state"))) + require.Equal(t, codes.FailedPrecondition, status.Code(wrapped)) + require.Equal(t, "the installation is not in a failed state", status.Convert(wrapped).Message()) } // HOST-INST-005: retry is an administration act, never an unauthenticated diff --git a/module/services/accounts/code/pkg/adapters/rpcs.go b/module/services/accounts/code/pkg/adapters/rpcs.go index c85892daf..341a9fe41 100644 --- a/module/services/accounts/code/pkg/adapters/rpcs.go +++ b/module/services/accounts/code/pkg/adapters/rpcs.go @@ -1434,12 +1434,18 @@ func (s *AuditServer) QueryAuditLog(ctx context.Context, req *gen.QueryAuditLogR var events []*gen.AuditEvent resolver := service.AuditEventResolver() + // A module acting on its own is recorded under its principal, which no + // directory row names; the registry does, by the prefix it is declared + // under. Read once per page, never stored on the row. + modules := service.ModulePrincipals() for _, e := range entries { resolved, err := resolver.Resolve(ctx, e.EventType) if err != nil { return nil, status.Error(codes.Unavailable, "audit event types unavailable") } - events = append(events, infra.AuditEntryToProto(e, resolved)) + event := infra.AuditEntryToProto(e, resolved) + event.ActorModule = auditActorModule(modules, e.ActorID) + events = append(events, event) } return &gen.QueryAuditLogResponse{ @@ -1449,6 +1455,17 @@ func (s *AuditServer) QueryAuditLog(ctx context.Context, req *gen.QueryAuditLogR }, nil } +// auditActorModule names the module whose own principal actorID is, by its +// declared prefix, or nothing for any other actor. Only the module's OWN +// principal is named: a subject a module acted for is recorded under that +// subject's id and named by the directory like any other person. +func auditActorModule(modules business.ModulePrincipalRegistry, actorID string) string { + if module, ok := modules[actorID]; ok { + return module.Prefix + } + return "" +} + func (s *AuditServer) AggregateAuditLog(ctx context.Context, req *gen.AggregateAuditLogRequest) (*gen.AggregateAuditLogResponse, error) { if err := Validate(req); err != nil { return nil, err diff --git a/module/services/accounts/code/pkg/business/solution_targets.go b/module/services/accounts/code/pkg/business/solution_targets.go index b3fb29688..53f6247b5 100644 --- a/module/services/accounts/code/pkg/business/solution_targets.go +++ b/module/services/accounts/code/pkg/business/solution_targets.go @@ -97,6 +97,11 @@ type AvailableSolutionTarget struct { // Installed reports whether the asking organisation already holds an ACTIVE // installation of this target. Installed bool + // HasPages reports whether the applied generation declares a frontend block: + // the registration's frontend half, the same record the navigation is built + // from. It is independent of Kind — either kind may declare pages, and + // either may be headless. + HasPages bool } // SolutionGenerationDecision is one entry in the generation history: a generation diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit.pb.go index 3796396d1..974ede25d 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/audit.pb.go @@ -51,7 +51,13 @@ type AuditEvent struct { // client_id names the registered client the call was made through, empty for // a call made from the host's own web session. It answers "what did they do // it through", which actor_id does not. - ClientId string `protobuf:"bytes,15,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ClientId string `protobuf:"bytes,15,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + // actor_module names the module whose own principal is actor_id, by the + // prefix it is declared under in this host's module principal registry, so a + // reader can say "Module " instead of an unresolved id. Empty for every + // other actor, and for a module this host no longer declares. Read at query + // time; nothing about it is stored on the row. + ActorModule string `protobuf:"bytes,16,opt,name=actor_module,json=actorModule,proto3" json:"actor_module,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -193,6 +199,13 @@ func (x *AuditEvent) GetClientId() string { return "" } +func (x *AuditEvent) GetActorModule() string { + if x != nil { + return x.ActorModule + } + return "" +} + type QueryAuditLogRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -1271,7 +1284,7 @@ var File_saas_accounts_v1_audit_proto protoreflect.FileDescriptor const file_saas_accounts_v1_audit_proto_rawDesc = "" + "\n" + - "\x1csaas/accounts/v1/audit.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1csaas/policy/v1/options.proto\"\xdb\x04\n" + + "\x1csaas/accounts/v1/audit.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1csaas/policy/v1/options.proto\"\xfe\x04\n" + "\n" + "AuditEvent\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x19\n" + @@ -1294,7 +1307,8 @@ const file_saas_accounts_v1_audit_proto_rawDesc = "" + "\x0eschema_version\x18\f \x01(\x05R\rschemaVersion\x121\n" + "\apayload\x18\r \x01(\v2\x17.google.protobuf.StructR\apayload\x12\x1a\n" + "\bcategory\x18\x0e \x01(\tR\bcategory\x12\x1b\n" + - "\tclient_id\x18\x0f \x01(\tR\bclientId\x1a;\n" + + "\tclient_id\x18\x0f \x01(\tR\bclientId\x12!\n" + + "\factor_module\x18\x10 \x01(\tR\vactorModule\x1a;\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x98\x05\n" + diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go index 9069c690d..08748085d 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go @@ -1423,7 +1423,17 @@ type AvailableSolution struct { // target. A catalogue shows it as installed rather than offering it twice; the // unique index over (org_id, target_id) would refuse the second install anyway, // and an administrator should not have to discover that by being refused. - Installed bool `protobuf:"varint,9,opt,name=installed,proto3" json:"installed,omitempty"` + Installed bool `protobuf:"varint,9,opt,name=installed,proto3" json:"installed,omitempty"` + // True when the applied generation declares pages people open — the + // `frontend` block the host projects into its navigation. Read from the same + // applied record the navigation is built from, never from the kind: a module + // or a solution may declare pages, and either may be headless. It reports + // pages only: a grant on a headless installation can still matter, because a + // solution may call that component on a person's own session and the host + // then admits only a person with access there. The applied manifest does not + // record that session consumption yet, so this field alone does not decide + // whether access applies. + HasPages bool `protobuf:"varint,10,opt,name=has_pages,json=hasPages,proto3" json:"has_pages,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1521,6 +1531,13 @@ func (x *AvailableSolution) GetInstalled() bool { return false } +func (x *AvailableSolution) GetHasPages() bool { + if x != nil { + return x.HasPages + } + return false +} + type ListAvailableSolutionsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` OrgId string `protobuf:"bytes,1,opt,name=org_id,json=orgId,proto3" json:"org_id,omitempty"` @@ -1733,7 +1750,7 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x121\n" + "\x0finstallation_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x0einstallationId\x12!\n" + "\ateam_id\x18\x03 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06teamId\x12!\n" + - "\arole_id\x18\x04 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06roleId\"\xed\x02\n" + + "\arole_id\x18\x04 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06roleId\"\x8a\x03\n" + "\x11AvailableSolution\x12%\n" + "\ttarget_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\btargetId\x12\x1d\n" + "\n" + @@ -1745,7 +1762,9 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\x0frelease_version\x18\x06 \x01(\tR\x0ereleaseVersion\x12+\n" + "\x11opened_generation\x18\a \x01(\x04R\x10openedGeneration\x12-\n" + "\x12applied_generation\x18\b \x01(\x04R\x11appliedGeneration\x12\x1c\n" + - "\tinstalled\x18\t \x01(\bR\tinstalled\"\x88\x01\n" + + "\tinstalled\x18\t \x01(\bR\tinstalled\x12\x1b\n" + + "\thas_pages\x18\n" + + " \x01(\bR\bhasPages\"\x88\x01\n" + "\x1dListAvailableSolutionsRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12'\n" + "\tpage_size\x18\x02 \x01(\x05B\n" + diff --git a/module/services/accounts/code/pkg/infra/postgres_solution_targets.go b/module/services/accounts/code/pkg/infra/postgres_solution_targets.go index 8a1aea2b3..56d6e9d5f 100644 --- a/module/services/accounts/code/pkg/infra/postgres_solution_targets.go +++ b/module/services/accounts/code/pkg/infra/postgres_solution_targets.go @@ -212,6 +212,10 @@ func (s *PostgresStore) RevokeInstallationsOfTarget( // answer and the one a diagnostic listing would get wrong by reporting the // refused desired generation beside it. // +// `has pages` is the registration's frontend half, which the apply writes in the +// same transaction as the applied generation (a generation that drops its +// `frontend` block drops the half), so it describes the release offered here. +// // `installed` is resolved in the same statement rather than by a second query per // row, for the reason the installation listing resolves health inline: a // catalogue of n rows must not cost n+1 round trips. @@ -234,6 +238,7 @@ func (s *PostgresStore) ListAvailableSolutionTargets( rows, err := s.getQueryExecutor(ctx).Query(ctx, ` SELECT t.id::text, t.binding_id, t.solution_id, r.declared_kind, b.applied_release, t.opened_generation, b.applied_generation, + r.frontend_revision IS NOT NULL, ($1 <> '' AND EXISTS ( SELECT 1 FROM public.installations i WHERE i.target_id = t.id AND i.org_id = $1::uuid AND i.status <> 'revoked' @@ -262,7 +267,7 @@ func (s *PostgresStore) ListAvailableSolutionTargets( appliedGeneration int64 ) if err := rows.Scan(&entry.TargetID, &entry.BindingID, &entry.RouteAlias, &entry.Kind, - &release, &openedGeneration, &appliedGeneration, &entry.Installed); err != nil { + &release, &openedGeneration, &appliedGeneration, &entry.HasPages, &entry.Installed); err != nil { return nil, err } entry.OpenedGeneration = uint64(openedGeneration) diff --git a/module/services/accounts/code/pkg/infra/postgres_solution_targets_pages_test.go b/module/services/accounts/code/pkg/infra/postgres_solution_targets_pages_test.go new file mode 100644 index 000000000..ce7eabf73 --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_solution_targets_pages_test.go @@ -0,0 +1,53 @@ +//go:build !pure + +package infra_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "accounts/pkg/business" +) + +// The catalogue says whether a target's applied generation declares pages, read +// from the registration's frontend half — the record the navigation is built +// from — so an administrator is offered access to an installation for its pages +// without the page guessing from a name or a kind. +func TestAvailableSolutionTargetReportsDeclaredPages(t *testing.T) { + alias := uniqueAlias("example-pages") + target := declarePresence(t, "acme.test."+alias, alias) + + pages := func() bool { + t.Helper() + var hasPages bool + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + targets, err := testStore.ListAvailableSolutionTargets(ctx, business.AvailableSolutionQuery{TargetID: target, Limit: 1}) + if err != nil { + return err + } + require.Len(t, targets, 1) + hasPages = targets[0].HasPages + return nil + })) + return hasPages + } + + require.False(t, pages(), "a headless generation declares no pages") + + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + record, err := testStore.GetSolutionRegistrationForUpdate(ctx, alias) + if err != nil { + return err + } + revision, err := testStore.NextSolutionRegistryRevision(ctx) + if err != nil { + return err + } + record.Revision = revision + record.Frontend = &business.SolutionFrontendHalf{Revision: revision, Manifest: `{"title":"Example"}`} + return testStore.SaveSolutionRegistration(ctx, record) + })) + require.True(t, pages(), "a generation with a frontend block declares pages") +} diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 31efcb498..1e6e6017c 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -7205,6 +7205,10 @@ "clientId": { "type": "string", "description": "client_id names the registered client the call was made through, empty for\na call made from the host's own web session. It answers \"what did they do\nit through\", which actor_id does not." + }, + "actorModule": { + "type": "string", + "description": "actor_module names the module whose own principal is actor_id, by the\nprefix it is declared under in this host's module principal registry, so a\nreader can say \"Module \u003cname\u003e\" instead of an unresolved id. Empty for every\nother actor, and for a module this host no longer declares. Read at query\ntime; nothing about it is stored on the row." } } }, @@ -7382,6 +7386,10 @@ "installed": { "type": "boolean", "description": "True when this organization already has an ACTIVE installation of this\ntarget. A catalogue shows it as installed rather than offering it twice; the\nunique index over (org_id, target_id) would refuse the second install anyway,\nand an administrator should not have to discover that by being refused." + }, + "hasPages": { + "type": "boolean", + "description": "True when the applied generation declares pages people open — the\n`frontend` block the host projects into its navigation. Read from the same\napplied record the navigation is built from, never from the kind: a module\nor a solution may declare pages, and either may be headless. It reports\npages only: a grant on a headless installation can still matter, because a\nsolution may call that component on a person's own session and the host\nthen admits only a person with access there. The applied manifest does not\nrecord that session consumption yet, so this field alone does not decide\nwhether access applies." } }, "description": "AvailableSolution is one installable solution target: its immutable identity,\nthe release the host currently has applied for it, and where it is routed." diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index f6bc28ece..c8b17b1a7 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -694,6 +694,10 @@ "actorId": { "type": "string" }, + "actorModule": { + "description": "actor_module names the module whose own principal is actor_id, by the\nprefix it is declared under in this host's module principal registry, so a\nreader can say \"Module \" instead of an unresolved id. Empty for every\nother actor, and for a module this host no longer declares. Read at query\ntime; nothing about it is stored on the row.", + "type": "string" + }, "actorType": { "type": "string" }, @@ -869,6 +873,10 @@ "description": "The binding whose presence this target records. Carried so an operator can\ncorrelate a catalogue row with delivery; an installation does not join on it,\nbecause one binding may have several targets over time.", "type": "string" }, + "hasPages": { + "description": "True when the applied generation declares pages people open — the\n`frontend` block the host projects into its navigation. Read from the same\napplied record the navigation is built from, never from the kind: a module\nor a solution may declare pages, and either may be headless. It reports\npages only: a grant on a headless installation can still matter, because a\nsolution may call that component on a person's own session and the host\nthen admits only a person with access there. The applied manifest does not\nrecord that session consumption yet, so this field alone does not decide\nwhether access applies.", + "type": "boolean" + }, "installed": { "description": "True when this organization already has an ACTIVE installation of this\ntarget. A catalogue shows it as installed rather than offering it twice; the\nunique index over (org_id, target_id) would refuse the second install anyway,\nand an administrator should not have to discover that by being refused.", "type": "boolean" diff --git a/module/services/accounts/proto/saas/accounts/v1/audit.proto b/module/services/accounts/proto/saas/accounts/v1/audit.proto index ee8451f18..03e42a57c 100644 --- a/module/services/accounts/proto/saas/accounts/v1/audit.proto +++ b/module/services/accounts/proto/saas/accounts/v1/audit.proto @@ -34,6 +34,12 @@ message AuditEvent { // a call made from the host's own web session. It answers "what did they do // it through", which actor_id does not. string client_id = 15; + // actor_module names the module whose own principal is actor_id, by the + // prefix it is declared under in this host's module principal registry, so a + // reader can say "Module " instead of an unresolved id. Empty for every + // other actor, and for a module this host no longer declares. Read at query + // time; nothing about it is stored on the row. + string actor_module = 16; } message QueryAuditLogRequest { diff --git a/module/services/accounts/proto/saas/accounts/v1/installations.proto b/module/services/accounts/proto/saas/accounts/v1/installations.proto index 7b8440c1b..8f272b8a9 100644 --- a/module/services/accounts/proto/saas/accounts/v1/installations.proto +++ b/module/services/accounts/proto/saas/accounts/v1/installations.proto @@ -685,6 +685,16 @@ message AvailableSolution { // unique index over (org_id, target_id) would refuse the second install anyway, // and an administrator should not have to discover that by being refused. bool installed = 9; + // True when the applied generation declares pages people open — the + // `frontend` block the host projects into its navigation. Read from the same + // applied record the navigation is built from, never from the kind: a module + // or a solution may declare pages, and either may be headless. It reports + // pages only: a grant on a headless installation can still matter, because a + // solution may call that component on a person's own session and the host + // then admits only a person with access there. The applied manifest does not + // record that session consumption yet, so this field alone does not decide + // whether access applies. + bool has_pages = 10; } message ListAvailableSolutionsRequest { diff --git a/module/services/frontend/AGENTS.md b/module/services/frontend/AGENTS.md index eef35ccc3..a7fbc2f20 100644 --- a/module/services/frontend/AGENTS.md +++ b/module/services/frontend/AGENTS.md @@ -19,6 +19,11 @@ organisation installs it and grants team access independently through the same target-based entitlement projection. The organization administration destination is labelled "Modules & solutions" and retains `/admin/solutions`; its empty state directs an administrator to the platform operator when nothing is available. +Its Installed table's Access column offers "Grant access" (a holder and its level — +today a team and the role it holds) on every active row; a row whose target +declares no pages (`hasPages` false) shows one muted line explaining when a grant +still matters instead of "nobody has access", because a solution may reach that +component on a person's own session. A failed row offers "Retry installation". The platform catalogue explains the delivery prerequisite. Headless components contribute no page. For a module page, relative data requests through the injected `apiBase` reach diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 560bc4271..fe691c173 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:19b078707414a2946f30788ead4245bb1a4ed970789fd5aced5e5328702c259f", + "digest": "sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 467ab5af0..592d233fa 100644 Binary files a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb and b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb differ diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index ceac75ec3..ed3c27172 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:19b078707414a2946f30788ead4245bb1a4ed970789fd5aced5e5328702c259f + contract-digest: sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/audit_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/audit_pb.ts index 5284c29c4..610d1c065 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/audit_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/audit_pb.ts @@ -15,7 +15,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/audit.proto. */ export const file_saas_accounts_v1_audit: GenFile = /*@__PURE__*/ - fileDesc("ChxzYWFzL2FjY291bnRzL3YxL2F1ZGl0LnByb3RvEhBzYWFzLmFjY291bnRzLnYxIrsDCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmFjdG9yX3R5cGUYAyABKAkSEgoGYWN0aW9uGAQgASgJQgIYARIQCghyZXNvdXJjZRgFIAEoCRITCgtyZXNvdXJjZV9pZBgGIAEoCRIOCgZvcmdfaWQYByABKAkSQAoIbWV0YWRhdGEYCCADKAsyKi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQuTWV0YWRhdGFFbnRyeUICGAESEgoKaXBfYWRkcmVzcxgJIAEoCRIuCgpjcmVhdGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpldmVudF90eXBlGAsgASgJEhYKDnNjaGVtYV92ZXJzaW9uGAwgASgFEigKB3BheWxvYWQYDSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhAKCGNhdGVnb3J5GA4gASgJEhEKCWNsaWVudF9pZBgPIAEoCRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEi+AMKFFF1ZXJ5QXVkaXRMb2dSZXF1ZXN0Eg4KBm9yZ19pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRISCgZhY3Rpb24YAyABKAlCAhgBEhAKCHJlc291cmNlGAQgASgJEhMKC3Jlc291cmNlX2lkGAUgASgJEigKBGZyb20YBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiYKAnRvGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIcCglwYWdlX3NpemUYCCABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAkgASgJEhIKCmV2ZW50X3R5cGUYCiABKAkSEAoIY2F0ZWdvcnkYCyABKAkSVQoQcGF5bG9hZF9jb250YWlucxgMIAMoCzI7LnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSEQoJbmFtZXNwYWNlGA0gASgJEhEKCWNsaWVudF9pZBgOIAEoCRIkCgtldmVudF90eXBlcxgPIAMoCUIPukgMkgEJEEAiBXIDGP8BGjYKFFBheWxvYWRDb250YWluc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEicwoVUXVlcnlBdWRpdExvZ1Jlc3BvbnNlEiwKBmV2ZW50cxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuQXVkaXRFdmVudBIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAUilwEKFUV4cG9ydEF1ZGl0TG9nUmVxdWVzdBIOCgZvcmdfaWQYASABKAkSDgoGZm9ybWF0GAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKBmFjdGlvbhgEIAEoCUICGAESEgoKZXZlbnRfdHlwZRgFIAEoCRIkCgtldmVudF90eXBlcxgGIAMoCUIPukgMkgEJEEAiBXIDGP8BIk4KFkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2USDAoEZGF0YRgBIAEoDBIUCgxjb250ZW50X3R5cGUYAiABKAkSEAoIZmlsZW5hbWUYAyABKAkiSwoLQXVkaXRNZXRyaWMSCgoCb3AYASABKAkSDQoFZmllbGQYAiABKAkSEgoKcGVyY2VudGlsZRgDIAEoARINCgVhbGlhcxgEIAEoCSJLChJBdWRpdERlcml2ZWRNZXRyaWMSDQoFYWxpYXMYASABKAkSEQoJbnVtZXJhdG9yGAIgASgJEhMKC2Rlbm9taW5hdG9yGAMgASgJIu0EChhBZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QSDgoGb3JnX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmV2ZW50X3R5cGUYAyABKAkSEAoIY2F0ZWdvcnkYBCABKAkSEAoIcmVzb3VyY2UYBSABKAkSKAoEZnJvbRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJgoCdG8YByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhAKCGdyb3VwX2J5GAggASgJEg4KBmJ1Y2tldBgJIAEoCRIRCglncm91cF9ieXMYCiADKAkSLgoHbWV0cmljcxgLIAMoCzIdLnNhYXMuYWNjb3VudHMudjEuQXVkaXRNZXRyaWMSNQoHZGVyaXZlZBgMIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuQXVkaXREZXJpdmVkTWV0cmljEhEKCW5hbWVzcGFjZRgNIAEoCRITCgtyZXNvdXJjZV9pZBgOIAEoCRJZChBwYXlsb2FkX2NvbnRhaW5zGA8gAygLMj8uc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSFQoNY29sbGVjdGlvbl9pZBgQIAEoCRIRCgljbGllbnRfaWQYESABKAkSJAoLZXZlbnRfdHlwZXMYEiADKAlCD7pIDJIBCRBAIgVyAxj/ARo2ChRQYXlsb2FkQ29udGFpbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIqwCChRBdWRpdEFnZ3JlZ2F0ZUJ1Y2tldBILCgNrZXkYASABKAkSDQoFY291bnQYAiABKAMSDAoEa2V5cxgDIAMoCRJECgdtZXRyaWNzGAQgAygLMjMuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEFnZ3JlZ2F0ZUJ1Y2tldC5NZXRyaWNzRW50cnkSRAoHc2FtcGxlcxgFIAMoCzIzLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQuU2FtcGxlc0VudHJ5Gi4KDE1ldHJpY3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAE6AjgBGi4KDFNhbXBsZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAM6AjgBInQKGUFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2USNwoHYnVja2V0cxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQSHgoWc2NvcGVfY29udHJhY3RfdmVyc2lvbhgCIAEoDSIcChpMaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdCLBAQoOQXVkaXRFdmVudFR5cGUSDAoEbmFtZRgBIAEoCRIPCgd2ZXJzaW9uGAIgASgFEhAKCGNhdGVnb3J5GAMgASgJEg0KBW93bmVyGAQgASgJEhIKCmRlcHJlY2F0ZWQYBSABKAgSEwoLZGVzY3JpcHRpb24YBiABKAkSEQoJbmFtZXNwYWNlGAcgASgJEhkKEW1hcmtzX3VzZXJfam9pbmVkGAggASgIEhgKEHdlYmhvb2tfZWxpZ2libGUYCSABKAgiTgobTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlEi8KBXR5cGVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEV2ZW50VHlwZTKdBgoMQXVkaXRTZXJ2aWNlErUBCg1RdWVyeUF1ZGl0TG9nEiYuc2Fhcy5hY2NvdW50cy52MS5RdWVyeUF1ZGl0TG9nUmVxdWVzdBonLnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1Jlc3BvbnNlIlPC8xg6CAIQAxoKYXVkaXQ6cmVhZCIKYXVkaXQ6cmVhZCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCDxINL3YxL2F1ZGl0LWxvZxLLAQoRQWdncmVnYXRlQXVkaXRMb2cSKi5zYWFzLmFjY291bnRzLnYxLkFnZ3JlZ2F0ZUF1ZGl0TG9nUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuQWdncmVnYXRlQXVkaXRMb2dSZXNwb25zZSJdwvMYOggCEAMaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhkSFy92MS9hdWRpdC1sb2c6YWdncmVnYXRlEsEBChNMaXN0QXVkaXRFdmVudFR5cGVzEiwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlIk3C8xgsCAIQARoKYXVkaXQ6cmVhZCIKYXVkaXQ6cmVhZDABOgIQAUABSANQAlgCYAGC0+STAhcSFS92MS9hdWRpdC1ldmVudC10eXBlcxLCAQoORXhwb3J0QXVkaXRMb2cSJy5zYWFzLmFjY291bnRzLnYxLkV4cG9ydEF1ZGl0TG9nUmVxdWVzdBooLnNhYXMuYWNjb3VudHMudjEuRXhwb3J0QXVkaXRMb2dSZXNwb25zZSJdwvMYOggCEAQaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhk6ASoiFC92MS9hdWRpdC1sb2c6ZXhwb3J0YgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_policy_v1_options]); + fileDesc("ChxzYWFzL2FjY291bnRzL3YxL2F1ZGl0LnByb3RvEhBzYWFzLmFjY291bnRzLnYxItEDCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmFjdG9yX3R5cGUYAyABKAkSEgoGYWN0aW9uGAQgASgJQgIYARIQCghyZXNvdXJjZRgFIAEoCRITCgtyZXNvdXJjZV9pZBgGIAEoCRIOCgZvcmdfaWQYByABKAkSQAoIbWV0YWRhdGEYCCADKAsyKi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQuTWV0YWRhdGFFbnRyeUICGAESEgoKaXBfYWRkcmVzcxgJIAEoCRIuCgpjcmVhdGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpldmVudF90eXBlGAsgASgJEhYKDnNjaGVtYV92ZXJzaW9uGAwgASgFEigKB3BheWxvYWQYDSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhAKCGNhdGVnb3J5GA4gASgJEhEKCWNsaWVudF9pZBgPIAEoCRIUCgxhY3Rvcl9tb2R1bGUYECABKAkaLwoNTWV0YWRhdGFFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIvgDChRRdWVyeUF1ZGl0TG9nUmVxdWVzdBIOCgZvcmdfaWQYASABKAkSEAoIYWN0b3JfaWQYAiABKAkSEgoGYWN0aW9uGAMgASgJQgIYARIQCghyZXNvdXJjZRgEIAEoCRITCgtyZXNvdXJjZV9pZBgFIAEoCRIoCgRmcm9tGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBImCgJ0bxgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoJcGFnZV9zaXplGAggASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgJIAEoCRISCgpldmVudF90eXBlGAogASgJEhAKCGNhdGVnb3J5GAsgASgJElUKEHBheWxvYWRfY29udGFpbnMYDCADKAsyOy5zYWFzLmFjY291bnRzLnYxLlF1ZXJ5QXVkaXRMb2dSZXF1ZXN0LlBheWxvYWRDb250YWluc0VudHJ5EhEKCW5hbWVzcGFjZRgNIAEoCRIRCgljbGllbnRfaWQYDiABKAkSJAoLZXZlbnRfdHlwZXMYDyADKAlCD7pIDJIBCRBAIgVyAxj/ARo2ChRQYXlsb2FkQ29udGFpbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBInMKFVF1ZXJ5QXVkaXRMb2dSZXNwb25zZRIsCgZldmVudHMYASADKAsyHC5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIpcBChVFeHBvcnRBdWRpdExvZ1JlcXVlc3QSDgoGb3JnX2lkGAEgASgJEg4KBmZvcm1hdBgCIAEoCRIQCghhY3Rvcl9pZBgDIAEoCRISCgZhY3Rpb24YBCABKAlCAhgBEhIKCmV2ZW50X3R5cGUYBSABKAkSJAoLZXZlbnRfdHlwZXMYBiADKAlCD7pIDJIBCRBAIgVyAxj/ASJOChZFeHBvcnRBdWRpdExvZ1Jlc3BvbnNlEgwKBGRhdGEYASABKAwSFAoMY29udGVudF90eXBlGAIgASgJEhAKCGZpbGVuYW1lGAMgASgJIksKC0F1ZGl0TWV0cmljEgoKAm9wGAEgASgJEg0KBWZpZWxkGAIgASgJEhIKCnBlcmNlbnRpbGUYAyABKAESDQoFYWxpYXMYBCABKAkiSwoSQXVkaXREZXJpdmVkTWV0cmljEg0KBWFsaWFzGAEgASgJEhEKCW51bWVyYXRvchgCIAEoCRITCgtkZW5vbWluYXRvchgDIAEoCSLtBAoYQWdncmVnYXRlQXVkaXRMb2dSZXF1ZXN0Eg4KBm9yZ19pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRISCgpldmVudF90eXBlGAMgASgJEhAKCGNhdGVnb3J5GAQgASgJEhAKCHJlc291cmNlGAUgASgJEigKBGZyb20YBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiYKAnRvGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIQCghncm91cF9ieRgIIAEoCRIOCgZidWNrZXQYCSABKAkSEQoJZ3JvdXBfYnlzGAogAygJEi4KB21ldHJpY3MYCyADKAsyHS5zYWFzLmFjY291bnRzLnYxLkF1ZGl0TWV0cmljEjUKB2Rlcml2ZWQYDCADKAsyJC5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RGVyaXZlZE1ldHJpYxIRCgluYW1lc3BhY2UYDSABKAkSEwoLcmVzb3VyY2VfaWQYDiABKAkSWQoQcGF5bG9hZF9jb250YWlucxgPIAMoCzI/LnNhYXMuYWNjb3VudHMudjEuQWdncmVnYXRlQXVkaXRMb2dSZXF1ZXN0LlBheWxvYWRDb250YWluc0VudHJ5EhUKDWNvbGxlY3Rpb25faWQYECABKAkSEQoJY2xpZW50X2lkGBEgASgJEiQKC2V2ZW50X3R5cGVzGBIgAygJQg+6SAySAQkQQCIFcgMY/wEaNgoUUGF5bG9hZENvbnRhaW5zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKsAgoUQXVkaXRBZ2dyZWdhdGVCdWNrZXQSCwoDa2V5GAEgASgJEg0KBWNvdW50GAIgASgDEgwKBGtleXMYAyADKAkSRAoHbWV0cmljcxgEIAMoCzIzLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQuTWV0cmljc0VudHJ5EkQKB3NhbXBsZXMYBSADKAsyMy5zYWFzLmFjY291bnRzLnYxLkF1ZGl0QWdncmVnYXRlQnVja2V0LlNhbXBsZXNFbnRyeRouCgxNZXRyaWNzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgBOgI4ARouCgxTYW1wbGVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgDOgI4ASJ0ChlBZ2dyZWdhdGVBdWRpdExvZ1Jlc3BvbnNlEjcKB2J1Y2tldHMYASADKAsyJi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0QWdncmVnYXRlQnVja2V0Eh4KFnNjb3BlX2NvbnRyYWN0X3ZlcnNpb24YAiABKA0iHAoaTGlzdEF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QiwQEKDkF1ZGl0RXZlbnRUeXBlEgwKBG5hbWUYASABKAkSDwoHdmVyc2lvbhgCIAEoBRIQCghjYXRlZ29yeRgDIAEoCRINCgVvd25lchgEIAEoCRISCgpkZXByZWNhdGVkGAUgASgIEhMKC2Rlc2NyaXB0aW9uGAYgASgJEhEKCW5hbWVzcGFjZRgHIAEoCRIZChFtYXJrc191c2VyX2pvaW5lZBgIIAEoCBIYChB3ZWJob29rX2VsaWdpYmxlGAkgASgIIk4KG0xpc3RBdWRpdEV2ZW50VHlwZXNSZXNwb25zZRIvCgV0eXBlcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQXVkaXRFdmVudFR5cGUynQYKDEF1ZGl0U2VydmljZRK1AQoNUXVlcnlBdWRpdExvZxImLnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QaJy5zYWFzLmFjY291bnRzLnYxLlF1ZXJ5QXVkaXRMb2dSZXNwb25zZSJTwvMYOggCEAMaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAg8SDS92MS9hdWRpdC1sb2cSywEKEUFnZ3JlZ2F0ZUF1ZGl0TG9nEiouc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2UiXcLzGDoIAhADGgphdWRpdDpyZWFkIgphdWRpdDpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIZEhcvdjEvYXVkaXQtbG9nOmFnZ3JlZ2F0ZRLBAQoTTGlzdEF1ZGl0RXZlbnRUeXBlcxIsLnNhYXMuYWNjb3VudHMudjEuTGlzdEF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBdWRpdEV2ZW50VHlwZXNSZXNwb25zZSJNwvMYLAgCEAEaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQwAToCEAFAAUgDUAJYAmABgtPkkwIXEhUvdjEvYXVkaXQtZXZlbnQtdHlwZXMSwgEKDkV4cG9ydEF1ZGl0TG9nEicuc2Fhcy5hY2NvdW50cy52MS5FeHBvcnRBdWRpdExvZ1JlcXVlc3QaKC5zYWFzLmFjY291bnRzLnYxLkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2UiXcLzGDoIAhAEGgphdWRpdDpyZWFkIgphdWRpdDpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIZOgEqIhQvdjEvYXVkaXQtbG9nOmV4cG9ydGIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.AuditEvent @@ -109,6 +109,17 @@ export type AuditEvent = Message<"saas.accounts.v1.AuditEvent"> & { * @generated from field: string client_id = 15; */ clientId: string; + + /** + * actor_module names the module whose own principal is actor_id, by the + * prefix it is declared under in this host's module principal registry, so a + * reader can say "Module " instead of an unresolved id. Empty for every + * other actor, and for a module this host no longer declares. Read at query + * time; nothing about it is stored on the row. + * + * @generated from field: string actor_module = 16; + */ + actorModule: string; }; /** diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts index a0f38962d..75548cac0 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts @@ -17,7 +17,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installations.proto. */ export const file_saas_accounts_v1_installations: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi+QMKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIuADChZJbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoQYWdlbnRfaWRlbnRpZmllchgCIAEoCUIKukgHcgUQARiABBIeCgxkaXNwbGF5X25hbWUYBCABKAlCCLpIBXIDGIAEEiIKEHJvb3Rfc2NvcGVfbGFiZWwYBiABKAlCCLpIBXIDGIAEEhkKB3JvbGVfaWQYByABKAlCCLpIBXIDsAEBEicKEm93bmVyX3ByaW5jaXBhbF9pZBgIIAEoCUILukgI2AEBcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgJIAMoCUIPukgMkgEJECAiBXIDsAEBEioKEWFsbG93ZWRfYXVkaWVuY2VzGAogAygJQg+6SAySAQkQQCIFcgMYgAQSJwoOYWxsb3dlZF9zY29wZXMYCyADKAlCD7pIDJIBCRBAIgVyAxiAARIbCgl0YXJnZXRfaWQYDCABKAlCCLpIBXIDsAEBEh8KBXNjb3BlGA0gASgJQgu6SAhyBhACKICAQEgAiAEBQggKBl9zY29wZUoECAMQBEoECAUQBlITc29sdXRpb25faWRlbnRpZmllclIPcm9vdF9zY29wZV9wYXRoInQKGU5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhoKBXNjb3BlGAMgASgJQgu6SAhyBhACKICAQCJXChhSZXRyeUluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIlcKGFVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEivgEKJFRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESKAoWbmV3X293bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgEIAMoCUIPukgMkgEJECAiBXIDsAEBIlUKFkdldEluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIoUBChdHZXRJbnN0YWxsYXRpb25SZXNwb25zZRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLSAgoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50IoMBCiVMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlEkEKDWluc3RhbGxhdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLk9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikwEKHkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEiTgofR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZRIrCgVncmFudBgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuU2NvcGVHcmFudCKWAQohUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW1SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIZCgd0ZWFtX2lkGAMgASgJQgi6SAVyA7ABARIZCgdyb2xlX2lkGAQgASgJQgi6SAVyA7ABASLtAQoRQXZhaWxhYmxlU29sdXRpb24SGwoJdGFyZ2V0X2lkGAEgASgJQgi6SAVyA7ABARISCgpiaW5kaW5nX2lkGAIgASgJEhMKC3JvdXRlX2FsaWFzGAMgASgJEhkKEXJlbGVhc2VfcHVibGlzaGVyGAQgASgJEhQKDHJlbGVhc2VfbmFtZRgFIAEoCRIXCg9yZWxlYXNlX3ZlcnNpb24YBiABKAkSGQoRb3BlbmVkX2dlbmVyYXRpb24YByABKAQSGgoSYXBwbGllZF9nZW5lcmF0aW9uGAggASgEEhEKCWluc3RhbGxlZBgJIAEoCCJsCh1MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJInEKHkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZRI2Cglzb2x1dGlvbnMYASADKAsyIy5zYWFzLmFjY291bnRzLnYxLkF2YWlsYWJsZVNvbHV0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBTKqEgoTSW5zdGFsbGF0aW9uU2VydmljZRK6AQoPSW5zdGFsbFNvbHV0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsU29sdXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24iXcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5jcmVhdGVkEAJAAUgEUANYA2ABgtPkkwIWOgEqIhEvdjEvaW5zdGFsbGF0aW9ucxLSAQoRVW5pbnN0YWxsU29sdXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ5wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldm9rZWQQAkABSARQA1gDYAGC0+STAjI6ASoiLS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnVuaW5zdGFsbBLSAQoSTmFycm93SW5zdGFsbGF0aW9uEisuc2Fhcy5hY2NvdW50cy52MS5OYXJyb3dJbnN0YWxsYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InfC8xg+CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hgoac2Fhcy5pbnN0YWxsYXRpb24ubmFycm93ZWQQAkABSARQA1gDYAGC0+STAi86ASoiKi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9Om5hcnJvdxLWAQoRUmV0cnlJbnN0YWxsYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uInXC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV0cmllZBACQAFIBFADWANgAYLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06cmV0cnkSiQIKHVRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwEjYuc2Fhcy5hY2NvdW50cy52MS5UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcFJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiKPAcLzGEsIAhAEKgwKBm9yZ19pZBACGAEwATorCidzYWFzLmluc3RhbGxhdGlvbi5vd25lcnNoaXBfdHJhbnNmZXJyZWQQAkABSARQA1gDYAGC0+STAjo6ASoiNS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnRyYW5zZmVyT3duZXJzaGlwErkBCg9HZXRJbnN0YWxsYXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlc3BvbnNlIlHC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCJRIjL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0ShgEKEUxpc3RJbnN0YWxsYXRpb25zEiouc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARLKAQoWTGlzdEF2YWlsYWJsZVNvbHV0aW9ucxIvLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZSJNwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiESHy92MS9pbnN0YWxsYXRpb25zOmxpc3RBdmFpbGFibGUS0QEKHUxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVzcG9uc2UiP8LzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwITEhEvdjEvaW5zdGFsbGF0aW9ucxLwAQoXR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW0SMC5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBoxLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZSJwwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUuZ3JhbnRlZBACQAFIBFADWANgAXgCgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcxLtAQoaUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW0SMy5zYWFzLmFjY291bnRzLnYxLlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSKBAcLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLnJldm9rZWQQAkABSARQA1gDYAF4AoLT5JMCPzoBKiI6L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMve3RlYW1faWR9OnJldm9rZWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi+QMKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIuADChZJbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoQYWdlbnRfaWRlbnRpZmllchgCIAEoCUIKukgHcgUQARiABBIeCgxkaXNwbGF5X25hbWUYBCABKAlCCLpIBXIDGIAEEiIKEHJvb3Rfc2NvcGVfbGFiZWwYBiABKAlCCLpIBXIDGIAEEhkKB3JvbGVfaWQYByABKAlCCLpIBXIDsAEBEicKEm93bmVyX3ByaW5jaXBhbF9pZBgIIAEoCUILukgI2AEBcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgJIAMoCUIPukgMkgEJECAiBXIDsAEBEioKEWFsbG93ZWRfYXVkaWVuY2VzGAogAygJQg+6SAySAQkQQCIFcgMYgAQSJwoOYWxsb3dlZF9zY29wZXMYCyADKAlCD7pIDJIBCRBAIgVyAxiAARIbCgl0YXJnZXRfaWQYDCABKAlCCLpIBXIDsAEBEh8KBXNjb3BlGA0gASgJQgu6SAhyBhACKICAQEgAiAEBQggKBl9zY29wZUoECAMQBEoECAUQBlITc29sdXRpb25faWRlbnRpZmllclIPcm9vdF9zY29wZV9wYXRoInQKGU5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhoKBXNjb3BlGAMgASgJQgu6SAhyBhACKICAQCJXChhSZXRyeUluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIlcKGFVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEivgEKJFRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESKAoWbmV3X293bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgEIAMoCUIPukgMkgEJECAiBXIDsAEBIlUKFkdldEluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIoUBChdHZXRJbnN0YWxsYXRpb25SZXNwb25zZRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLSAgoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50IoMBCiVMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlEkEKDWluc3RhbGxhdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLk9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikwEKHkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEiTgofR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZRIrCgVncmFudBgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuU2NvcGVHcmFudCKWAQohUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW1SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIZCgd0ZWFtX2lkGAMgASgJQgi6SAVyA7ABARIZCgdyb2xlX2lkGAQgASgJQgi6SAVyA7ABASKAAgoRQXZhaWxhYmxlU29sdXRpb24SGwoJdGFyZ2V0X2lkGAEgASgJQgi6SAVyA7ABARISCgpiaW5kaW5nX2lkGAIgASgJEhMKC3JvdXRlX2FsaWFzGAMgASgJEhkKEXJlbGVhc2VfcHVibGlzaGVyGAQgASgJEhQKDHJlbGVhc2VfbmFtZRgFIAEoCRIXCg9yZWxlYXNlX3ZlcnNpb24YBiABKAkSGQoRb3BlbmVkX2dlbmVyYXRpb24YByABKAQSGgoSYXBwbGllZF9nZW5lcmF0aW9uGAggASgEEhEKCWluc3RhbGxlZBgJIAEoCBIRCgloYXNfcGFnZXMYCiABKAgibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUyqhIKE0luc3RhbGxhdGlvblNlcnZpY2USugEKD0luc3RhbGxTb2x1dGlvbhIoLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIl3C8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24uY3JlYXRlZBACQAFIBFADWANgAYLT5JMCFjoBKiIRL3YxL2luc3RhbGxhdGlvbnMS0gEKEVVuaW5zdGFsbFNvbHV0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5Vbmluc3RhbGxTb2x1dGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiecLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXZva2VkEAJAAUgEUANYA2ABgtPkkwIyOgEqIi0vdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp1bmluc3RhbGwS0gEKEk5hcnJvd0luc3RhbGxhdGlvbhIrLnNhYXMuYWNjb3VudHMudjEuTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ3wvMYPggCEAQqDAoGb3JnX2lkEAIYATABOh4KGnNhYXMuaW5zdGFsbGF0aW9uLm5hcnJvd2VkEAJAAUgEUANYA2ABgtPkkwIvOgEqIiovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpuYXJyb3cS1gEKEVJldHJ5SW5zdGFsbGF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5SZXRyeUluc3RhbGxhdGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJ1wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldHJpZWQQAkABSARQA1gDYAGC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnJldHJ5EokCCh1UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcBI2LnNhYXMuYWNjb3VudHMudjEuVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24ijwHC8xhLCAIQBCoMCgZvcmdfaWQQAhgBMAE6Kwonc2Fhcy5pbnN0YWxsYXRpb24ub3duZXJzaGlwX3RyYW5zZmVycmVkEAJAAUgEUANYA2ABgtPkkwI6OgEqIjUvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp0cmFuc2Zlck93bmVyc2hpcBK5AQoPR2V0SW5zdGFsbGF0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXNwb25zZSJRwvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiUSIy92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9EoYBChFMaXN0SW5zdGFsbGF0aW9ucxIqLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESygEKFkxpc3RBdmFpbGFibGVTb2x1dGlvbnMSLy5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2UiTcLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIhEh8vdjEvaW5zdGFsbGF0aW9uczpsaXN0QXZhaWxhYmxlEtEBCh1MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9ucxI2LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlIj/C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCExIRL3YxL2luc3RhbGxhdGlvbnMS8AEKF0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtEjAuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2UicMLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLmdyYW50ZWQQAkABSARQA1gDYAF4AoLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMS7QEKGlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtEjMuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkigQHC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5yZXZva2VkEAJAAUgEUANYA2ABeAKC0+STAj86ASoiOi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zL3t0ZWFtX2lkfTpyZXZva2ViBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); /** * Installation is one solution's bound identity in an org. Server-assigned ids; @@ -762,6 +762,21 @@ export type AvailableSolution = Message<"saas.accounts.v1.AvailableSolution"> & * @generated from field: bool installed = 9; */ installed: boolean; + + /** + * True when the applied generation declares pages people open — the + * `frontend` block the host projects into its navigation. Read from the same + * applied record the navigation is built from, never from the kind: a module + * or a solution may declare pages, and either may be headless. It reports + * pages only: a grant on a headless installation can still matter, because a + * solution may call that component on a person's own session and the host + * then admits only a person with access there. The applied manifest does not + * record that session consumption yet, so this field alone does not decide + * whether access applies. + * + * @generated from field: bool has_pages = 10; + */ + hasPages: boolean; }; /** diff --git a/module/services/frontend/code/src/components/__tests__/activity-feed.test.tsx b/module/services/frontend/code/src/components/__tests__/activity-feed.test.tsx index 47e89bf27..0e71e5fea 100644 --- a/module/services/frontend/code/src/components/__tests__/activity-feed.test.tsx +++ b/module/services/frontend/code/src/components/__tests__/activity-feed.test.tsx @@ -157,6 +157,26 @@ describe("ActivityFeed actors", () => { expect(screen.getByText("agent")).toBeTruthy(); }); + it("names a module acting on its own as the module the host names", () => { + useAuditLogMock.mockReturnValue({ + data: { + events: [ + event(undefined, { + actorId: "c9d1e2f3-0000-5000-8000-000000000005", + actorType: "system", + actorModule: "example-chat", + }), + ], + }, + isLoading: false, + }); + + render(); + + expect(screen.getByText("Module example-chat")).toBeTruthy(); + expect(screen.queryByText("Actor unavailable")).toBeNull(); + }); + it('leaves the signed-in user as "You" without a type tag', () => { useAuditLogMock.mockReturnValue({ data: { events: [event(undefined)] }, diff --git a/module/services/frontend/code/src/components/activity-feed.tsx b/module/services/frontend/code/src/components/activity-feed.tsx index bf300b4df..0b2879a71 100644 --- a/module/services/frontend/code/src/components/activity-feed.tsx +++ b/module/services/frontend/code/src/components/activity-feed.tsx @@ -120,7 +120,8 @@ export function ActivityFeed({ {isYou ? "You" - : resolveActor(e.actorId, actorNames).label} + : resolveActor(e.actorId, actorNames, e.actorModule) + .label} {" "} {e.actorType && e.actorType !== "user" ? ( // Only a non-human actor earns the tag: the sentence diff --git a/module/services/frontend/code/src/features/audit/model/__tests__/transforms.test.ts b/module/services/frontend/code/src/features/audit/model/__tests__/transforms.test.ts index 783b39fd1..2c2fd36d3 100644 --- a/module/services/frontend/code/src/features/audit/model/__tests__/transforms.test.ts +++ b/module/services/frontend/code/src/features/audit/model/__tests__/transforms.test.ts @@ -97,6 +97,27 @@ describe("resolveActor", () => { }); }); + // A module acting on its own has no directory row; the host names it on the + // row, and it reads as the module rather than as an unavailable actor. + it("names a module acting on its own by the module the host names", () => { + expect( + resolveActor( + "c9d1e2f3-0000-5000-8000-000000000005", + directory, + "example-chat", + ), + ).toEqual({ label: "Module example-chat", resolved: true }); + // A directory name still wins: the module name is for a principal the + // directory cannot name. + expect( + resolveActor( + "a3f81c2e-0000-4000-8000-000000000001", + directory, + "example-chat", + ).label, + ).toBe("Jane Doe"); + }); + it("falls back when the directory has not landed yet", () => { expect( resolveActor("a3f81c2e-0000-4000-8000-000000000001", new Map()).label, @@ -128,6 +149,7 @@ function makeEvent(overrides: Partial = {}): AuditEvent { payload: {}, ipAddress: "127.0.0.1", clientId: "", + actorModule: "", ...overrides, }; } diff --git a/module/services/frontend/code/src/features/audit/model/transforms.ts b/module/services/frontend/code/src/features/audit/model/transforms.ts index 37700795c..da233c2f0 100644 --- a/module/services/frontend/code/src/features/audit/model/transforms.ts +++ b/module/services/frontend/code/src/features/audit/model/transforms.ts @@ -24,6 +24,7 @@ export function toAuditEvent(e: ProtoAuditEvent): AuditEvent { payload: e.payload, ipAddress: e.ipAddress, clientId: e.clientId, + actorModule: e.actorModule, createdAt: e.createdAt ? timestampDate(e.createdAt).toISOString() : undefined, @@ -56,16 +57,20 @@ export interface ResolvedActor { resolved: boolean; } -// resolveActor renders an audit row's actor as a person, service, or agent -// rather than as an opaque id. A principal that is revoked, cross-org, or past -// the directory's page walk is absent from the map, so the fallback is the -// unavailable label: normal screens must not expose opaque identifiers. +// resolveActor renders an audit row's actor as a person, service, agent or +// module rather than as an opaque id. A principal that is revoked, cross-org, or +// past the directory's page walk is absent from the map, so the fallback is the +// unavailable label: normal screens must not expose opaque identifiers. A +// module acting on its own has no directory row; the host names it on the row +// (`actorModule`), and it reads as "Module ". export function resolveActor( actorId: string, directory: PrincipalDirectory, + actorModule = "", ): ResolvedActor { const displayName = directory.get(actorId); if (displayName) return { label: displayName, resolved: true }; + if (actorModule) return { label: `Module ${actorModule}`, resolved: true }; // A row with no actor id is automated work the server attributed to no // principal; truncating "" would leave the cell blank. if (!actorId) return { label: "System", resolved: false }; diff --git a/module/services/frontend/code/src/features/audit/model/types.ts b/module/services/frontend/code/src/features/audit/model/types.ts index b66ab3b40..e45e737b5 100644 --- a/module/services/frontend/code/src/features/audit/model/types.ts +++ b/module/services/frontend/code/src/features/audit/model/types.ts @@ -16,6 +16,10 @@ export interface AuditEvent { // from the host's own web session. Separate from actorId: one answers who // acted, the other what they acted through. clientId: string; + // The module whose own principal acted, named by the host from its module + // principal registry; empty for every other actor. A module's principal has + // no directory row, so without it the row would read "Actor unavailable". + actorModule: string; createdAt?: string; } diff --git a/module/services/frontend/code/src/features/audit/ui/audit-table.tsx b/module/services/frontend/code/src/features/audit/ui/audit-table.tsx index bfaf243d9..c32de1a85 100644 --- a/module/services/frontend/code/src/features/audit/ui/audit-table.tsx +++ b/module/services/frontend/code/src/features/audit/ui/audit-table.tsx @@ -65,11 +65,20 @@ export function AuditTable({ // sort would order rows by raw uuid — visibly arbitrary against // the names on screen. Sort by what the cell actually shows. sortingFn: (a, b) => - resolveActor(a.original.actorId, actorNames).label.localeCompare( - resolveActor(b.original.actorId, actorNames).label, + resolveActor( + a.original.actorId, + actorNames, + a.original.actorModule, + ).label.localeCompare( + resolveActor(b.original.actorId, actorNames, b.original.actorModule) + .label, ), cell: (info) => { - const actor = resolveActor(info.getValue(), actorNames); + const actor = resolveActor( + info.getValue(), + actorNames, + info.row.original.actorModule, + ); return (
{ expect(row.inheritedTeams.map((t) => t.team)).toEqual(["Everyone"]); }); + it("reads whether people open its pages from the host, never from a name", () => { + const withPages = installed("t-1", false); + withPages.target = create(AvailableSolutionSchema, { + ...target, + hasPages: true, + }); + expect(installedRows([withPages])[0].pages).toBe("pages"); + expect(installedRows([installed("t-1", true)])[0].pages).toBe("headless"); + // A target the host no longer offers has no applied generation to ask. + expect(installedRows([installed("t-gone", false)])[0].pages).toBe( + "unknown", + ); + }); + it("reads an unreported health as a gap, not as healthy", () => { expect(healthView(InstallationHealth.UNSPECIFIED)).toMatchObject({ kind: "gap", diff --git a/module/services/frontend/code/src/features/solution-installations/model/installations.ts b/module/services/frontend/code/src/features/solution-installations/model/installations.ts index 094befca1..e29f6c17b 100644 --- a/module/services/frontend/code/src/features/solution-installations/model/installations.ts +++ b/module/services/frontend/code/src/features/solution-installations/model/installations.ts @@ -128,7 +128,29 @@ export function healthView(health: InstallationHealth): FactView { } } -/** One team grant at an installation, as the page lists and revokes it. */ +/** + * Whether people open this installation's own pages, as the host states it from + * the applied generation's `frontend` block — never inferred from a name or a + * kind: a module may have pages and a solution may be headless. `unknown` when + * the host no longer offers the installed target, so it cannot say. + */ +export type PageReach = "pages" | "headless" | "unknown"; + +/** + * Said once beside the access of an installation with no pages. Access is still + * offered: a solution may call the component on a person's own session, and the + * host then admits the call only for a person who has access here. The host + * does not yet know which components are reached that way, so the page + * explains when a grant matters rather than hiding the one that does. + */ +export const HEADLESS_ACCESS_NOTE = + "No pages of its own. Grant access only if a solution calls it on a person's own session; modules and solutions that mint their own access need none."; + +/** + * One access grant at an installation, as the page lists and revokes it. Today + * the holder is a team and the level is the role it holds; the row names both + * so another kind of holder or level adds a field rather than a second shape. + */ export interface TeamGrantRow { key: string; teamId: string; @@ -149,6 +171,7 @@ export interface InstalledRow { lifecycle: FactView; failureReason: string; installedAt: string | undefined; + pages: PageReach; /** Granted at this installation's authority root: revocable here. */ grantedTeams: TeamGrantRow[]; /** Reaching it from a grant above: real reach, but not granted here. */ @@ -212,6 +235,7 @@ export function installedRows( installedAt: installation.createdAt ? timestampDate(installation.createdAt).toISOString() : undefined, + pages: !target ? "unknown" : target.hasPages ? "pages" : "headless", grantedTeams: teamGrantRows(grantedTeams), inheritedTeams: teamGrantRows(inheritedTeams), }, diff --git a/module/services/frontend/code/src/features/solution-installations/ui/grant-team-dialog.tsx b/module/services/frontend/code/src/features/solution-installations/ui/grant-team-dialog.tsx index 3a0df80df..846a5872d 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/grant-team-dialog.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/grant-team-dialog.tsx @@ -101,10 +101,11 @@ export function GrantTeamDialog({ !open && onClose()}> - Grant a team access to {solution} + Grant access to {solution} - Record a role grant for this team at the installation. Availability - and effective access are reported by the host. + Choose who gets access and at which level: a team, and the role it + holds at this installation. Availability and effective access are + reported by the host.
@@ -164,7 +165,7 @@ export function GrantTeamDialog({ name="roleId" render={({ field }) => ( ({ organizationId: "org-1", @@ -70,6 +71,7 @@ const available = { openedGeneration: "2", appliedGeneration: "3", installed: true, + hasPages: true, }, { targetId: LEDGER_TARGET, @@ -81,6 +83,7 @@ const available = { openedGeneration: "5", appliedGeneration: "5", installed: false, + hasPages: false, }, ], }; @@ -223,12 +226,12 @@ async function openInstall() { async function openGrant() { fireEvent.click( await screen.findByRole("button", { - name: "Grant a team access to expenses", + name: "Grant access to expenses", }), ); const dialog = await screen.findByRole("dialog"); await choose(dialog, "Team", "Support"); - await choose(dialog, "Role", "Solution user — solution:use"); + await choose(dialog, "Access level (role)", "Solution user — solution:use"); return dialog; } @@ -290,7 +293,7 @@ describe("SolutionInstallationsPage admin container", () => { // Granted here and inherited from above are kept apart; only the first is // removable here. const granted = within(row).getByRole("list", { - name: "Teams granted expenses", + name: "Access granted to expenses", }); expect(within(granted).getByText("Finance")).toBeTruthy(); expect(within(granted).queryByText("Everyone")).toBeNull(); @@ -557,12 +560,12 @@ describe("SolutionInstallationsPage admin container", () => { fireEvent.click( await screen.findByRole("button", { - name: "Grant a team access to expenses", + name: "Grant access to expenses", }), ); const dialog = await screen.findByRole("dialog"); await choose(dialog, "Team", "Support"); - await choose(dialog, "Role", "Solution user — solution:use"); + await choose(dialog, "Access level (role)", "Solution user — solution:use"); fireEvent.click(within(dialog).getByRole("button", { name: "Grant" })); await waitFor(() => expect(granted).toBeDefined()); @@ -582,7 +585,7 @@ describe("SolutionInstallationsPage admin container", () => { ).toBeTruthy(); expect( within( - screen.getByRole("list", { name: "Teams granted expenses" }), + screen.getByRole("list", { name: "Access granted to expenses" }), ).getByText("Support"), ).toBeTruthy(); expect( @@ -591,7 +594,7 @@ describe("SolutionInstallationsPage admin container", () => { ).getByText("Agent disabled"), ).toBeTruthy(); expect(toast.success).toHaveBeenCalledExactlyOnceWith( - "Team grant recorded for expenses", + "Access granted to expenses", ); }); @@ -612,12 +615,16 @@ describe("SolutionInstallationsPage admin container", () => { renderInApp(); fireEvent.click( await screen.findByRole("button", { - name: "Grant a team access to expenses", + name: "Grant access to expenses", }), ); const dialog = await screen.findByRole("dialog"); await choose(dialog, "Team", "Support"); - await choose(dialog, "Role", "Expense reader — expenses:read"); + await choose( + dialog, + "Access level (role)", + "Expense reader — expenses:read", + ); fireEvent.click(within(dialog).getByRole("button", { name: "Grant" })); const alert = await within(dialog).findByRole("alert"); expect(alert.textContent).toContain( @@ -673,7 +680,7 @@ describe("SolutionInstallationsPage admin container", () => { // Still granted: the host did not remove it. expect( within( - screen.getByRole("list", { name: "Teams granted expenses" }), + screen.getByRole("list", { name: "Access granted to expenses" }), ).getByText("Finance"), ).toBeTruthy(); }); @@ -907,13 +914,13 @@ describe("organization administration boundary", () => { ).toBeNull(); fireEvent.click( await screen.findByRole("button", { - name: "Grant a team access to payables", + name: "Grant access to payables", }), ); const dialog = await screen.findByRole("dialog"); expect(within(dialog).queryByText("Support")).toBeNull(); await choose(dialog, "Team", "Organization two team"); - await choose(dialog, "Role", "Organization two role"); + await choose(dialog, "Access level (role)", "Organization two role"); fireEvent.click(within(dialog).getByRole("button", { name: "Grant" })); await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull()); expect(grant).toHaveBeenCalledOnce(); @@ -1028,7 +1035,7 @@ describe("host read refusals and retained data", () => { }, { kind: "grant", - label: "Role", + label: "Access level (role)", service: "PermissionService", method: "ListRoles", key: ["roles", "org-1"], @@ -1050,9 +1057,7 @@ describe("host read refusals and retained data", () => { fireEvent.click( await screen.findByRole("button", { name: - kind === "install" - ? "Install ledger" - : "Grant a team access to expenses", + kind === "install" ? "Install ledger" : "Grant access to expenses", }), ); const dialog = await screen.findByRole("dialog"); @@ -1405,9 +1410,7 @@ describe("accessible validation and host text", () => { fireEvent.click( await screen.findByRole("button", { name: - kind === "install" - ? "Install ledger" - : "Grant a team access to expenses", + kind === "install" ? "Install ledger" : "Grant access to expenses", }), ); const dialog = await screen.findByRole("dialog"); @@ -1430,7 +1433,7 @@ describe("accessible validation and host text", () => { else { expectFieldError(first, "Choose a team"); expectFieldError( - within(dialog).getByRole("combobox", { name: "Role" }), + within(dialog).getByRole("combobox", { name: "Access level (role)" }), "Choose the role the team is granted", ); } @@ -1541,8 +1544,67 @@ it("shows pending and failed installations and retries the failed step", async ( ); renderInApp(); expect(await screen.findByText("Example setup refused")).toBeTruthy(); - expect(screen.queryByRole("button", { name: /grant.*team/i })).toBeNull(); - fireEvent.click(screen.getByRole("button", { name: "Retry expenses" })); + // A failed installation says what recovers it, in place of an access action + // and a health it does not have. + expect( + screen.getByText(/Retry installation runs the install step again/), + ).toBeTruthy(); + expect(screen.getByText("Not running")).toBeTruthy(); + expect( + screen.getByText("Available once the installation succeeds"), + ).toBeTruthy(); + expect(screen.queryByRole("button", { name: /grant access/i })).toBeNull(); + fireEvent.click( + screen.getByRole("button", { name: "Retry installation of expenses" }), + ); expect(await screen.findByText("Pending")).toBeTruthy(); - expect(screen.queryByRole("button", { name: "Retry expenses" })).toBeNull(); + expect( + screen.queryByRole("button", { name: "Retry installation of expenses" }), + ).toBeNull(); + expect(screen.getByText("Available after activation")).toBeTruthy(); +}); + +// Access follows pages, as the host states them from the applied generation: +// an installation with pages and no grant says nobody has access yet; one with +// no pages says once why a grant may still matter, and never "nobody". +it("explains access by whether the installation has pages of its own", async () => { + const both = structuredClone(installations); + both.installations[0].grantedTeams = []; + both.installations[0].inheritedTeams = []; + const headless = structuredClone(both.installations[0]); + headless.installation.id = "cccccccc-0000-4000-8000-000000000002"; + headless.installation.targetId = LEDGER_TARGET; + headless.target = { ...available.solutions[1], installed: true }; + both.installations.push(headless); + server.use( + http.post(rpc("InstallationService", "ListAvailableSolutions"), () => + HttpResponse.json(available), + ), + http.post(rpc("InstallationService", "ListOrganizationInstallations"), () => + HttpResponse.json(both), + ), + ); + renderInApp(); + const table = await screen.findByRole("table", { + name: "Installed modules and solutions", + }); + const withPages = within(table).getByText("expenses").closest("tr"); + const withoutPages = within(table).getByText("ledger").closest("tr"); + if (!withPages || !withoutPages) throw new Error("installed rows missing"); + + expect(within(withPages).getByText("No one has access yet")).toBeTruthy(); + expect(within(withPages).queryByText(HEADLESS_ACCESS_NOTE)).toBeNull(); + expect( + within(withPages).getByRole("button", { name: "Grant access to expenses" }), + ).toBeTruthy(); + + expect(within(withoutPages).getByText(HEADLESS_ACCESS_NOTE)).toBeTruthy(); + expect(within(withoutPages).queryByText("No one has access yet")).toBeNull(); + expect(within(withoutPages).queryByText(/No team/)).toBeNull(); + // Kept, because a solution may reach it on a person's own session. + expect( + within(withoutPages).getByRole("button", { + name: "Grant access to ledger", + }), + ).toBeTruthy(); }); diff --git a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx index 029a148ca..21b77030b 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx @@ -46,6 +46,7 @@ import { import { availableRows, type FactView, + HEADLESS_ACCESS_NOTE, hostRefusal, type InstalledRow, installedRows, @@ -99,7 +100,7 @@ function Fact({ view }: { view: FactView }) { /** * The organization's modules and solutions: what it may install, what it has installed and * at which release, and the runtime acts an administrator performs on them — - * install, expose to a team, uninstall. Each act is the host's to accept or + * install, grant access, retry a failed install, uninstall. Each act is the host's to accept or * refuse, and a refusal is shown as the host words it. */ export function SolutionInstallationsPage() { @@ -123,8 +124,7 @@ function SolutionInstallationsForOrganization({ orgId }: { orgId: string }) {

Install the modules and solutions this organization may use, review - their status and release, and choose which teams can access each - one. + their status and release, and choose who can access their pages.

@@ -410,7 +410,7 @@ function InstalledSection({ Revision Status Health - Teams + Access Installed Action @@ -431,19 +431,27 @@ function InstalledSection({ {row.failureReason}

)} + {row.status === InstallationStatus.FAILED && ( +

+ Retry installation runs the install step again; + nothing needs to be uninstalled first. +

+ )} {row.status === InstallationStatus.ACTIVE ? ( ) : ( - Unavailable + {row.status === InstallationStatus.FAILED + ? "Not running" + : "Not running yet"} )} {row.status === InstallationStatus.ACTIVE ? ( - ) : ( - Available after activation + {row.status === InstallationStatus.FAILED + ? "Available once the installation succeeds" + : "Available after activation"} )} {formatDate(row.installedAt)} - + {row.status === InstallationStatus.FAILED && ( )}
); diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/audit_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/audit_pb.ts index 8541e08c0..18c52f318 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/audit_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/audit_pb.ts @@ -15,7 +15,7 @@ import type { JsonObject, Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/audit.proto. */ export const file_saas_accounts_v1_audit: GenFile = /*@__PURE__*/ - fileDesc("ChxzYWFzL2FjY291bnRzL3YxL2F1ZGl0LnByb3RvEhBzYWFzLmFjY291bnRzLnYxIrsDCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmFjdG9yX3R5cGUYAyABKAkSEgoGYWN0aW9uGAQgASgJQgIYARIQCghyZXNvdXJjZRgFIAEoCRITCgtyZXNvdXJjZV9pZBgGIAEoCRIOCgZvcmdfaWQYByABKAkSQAoIbWV0YWRhdGEYCCADKAsyKi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQuTWV0YWRhdGFFbnRyeUICGAESEgoKaXBfYWRkcmVzcxgJIAEoCRIuCgpjcmVhdGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpldmVudF90eXBlGAsgASgJEhYKDnNjaGVtYV92ZXJzaW9uGAwgASgFEigKB3BheWxvYWQYDSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhAKCGNhdGVnb3J5GA4gASgJEhEKCWNsaWVudF9pZBgPIAEoCRovCg1NZXRhZGF0YUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEi+AMKFFF1ZXJ5QXVkaXRMb2dSZXF1ZXN0Eg4KBm9yZ19pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRISCgZhY3Rpb24YAyABKAlCAhgBEhAKCHJlc291cmNlGAQgASgJEhMKC3Jlc291cmNlX2lkGAUgASgJEigKBGZyb20YBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiYKAnRvGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIcCglwYWdlX3NpemUYCCABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAkgASgJEhIKCmV2ZW50X3R5cGUYCiABKAkSEAoIY2F0ZWdvcnkYCyABKAkSVQoQcGF5bG9hZF9jb250YWlucxgMIAMoCzI7LnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSEQoJbmFtZXNwYWNlGA0gASgJEhEKCWNsaWVudF9pZBgOIAEoCRIkCgtldmVudF90eXBlcxgPIAMoCUIPukgMkgEJEEAiBXIDGP8BGjYKFFBheWxvYWRDb250YWluc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEicwoVUXVlcnlBdWRpdExvZ1Jlc3BvbnNlEiwKBmV2ZW50cxgBIAMoCzIcLnNhYXMuYWNjb3VudHMudjEuQXVkaXRFdmVudBIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAUilwEKFUV4cG9ydEF1ZGl0TG9nUmVxdWVzdBIOCgZvcmdfaWQYASABKAkSDgoGZm9ybWF0GAIgASgJEhAKCGFjdG9yX2lkGAMgASgJEhIKBmFjdGlvbhgEIAEoCUICGAESEgoKZXZlbnRfdHlwZRgFIAEoCRIkCgtldmVudF90eXBlcxgGIAMoCUIPukgMkgEJEEAiBXIDGP8BIk4KFkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2USDAoEZGF0YRgBIAEoDBIUCgxjb250ZW50X3R5cGUYAiABKAkSEAoIZmlsZW5hbWUYAyABKAkiSwoLQXVkaXRNZXRyaWMSCgoCb3AYASABKAkSDQoFZmllbGQYAiABKAkSEgoKcGVyY2VudGlsZRgDIAEoARINCgVhbGlhcxgEIAEoCSJLChJBdWRpdERlcml2ZWRNZXRyaWMSDQoFYWxpYXMYASABKAkSEQoJbnVtZXJhdG9yGAIgASgJEhMKC2Rlbm9taW5hdG9yGAMgASgJIu0EChhBZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QSDgoGb3JnX2lkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmV2ZW50X3R5cGUYAyABKAkSEAoIY2F0ZWdvcnkYBCABKAkSEAoIcmVzb3VyY2UYBSABKAkSKAoEZnJvbRgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASJgoCdG8YByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhAKCGdyb3VwX2J5GAggASgJEg4KBmJ1Y2tldBgJIAEoCRIRCglncm91cF9ieXMYCiADKAkSLgoHbWV0cmljcxgLIAMoCzIdLnNhYXMuYWNjb3VudHMudjEuQXVkaXRNZXRyaWMSNQoHZGVyaXZlZBgMIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuQXVkaXREZXJpdmVkTWV0cmljEhEKCW5hbWVzcGFjZRgNIAEoCRITCgtyZXNvdXJjZV9pZBgOIAEoCRJZChBwYXlsb2FkX2NvbnRhaW5zGA8gAygLMj8uc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QuUGF5bG9hZENvbnRhaW5zRW50cnkSFQoNY29sbGVjdGlvbl9pZBgQIAEoCRIRCgljbGllbnRfaWQYESABKAkSJAoLZXZlbnRfdHlwZXMYEiADKAlCD7pIDJIBCRBAIgVyAxj/ARo2ChRQYXlsb2FkQ29udGFpbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIqwCChRBdWRpdEFnZ3JlZ2F0ZUJ1Y2tldBILCgNrZXkYASABKAkSDQoFY291bnQYAiABKAMSDAoEa2V5cxgDIAMoCRJECgdtZXRyaWNzGAQgAygLMjMuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEFnZ3JlZ2F0ZUJ1Y2tldC5NZXRyaWNzRW50cnkSRAoHc2FtcGxlcxgFIAMoCzIzLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQuU2FtcGxlc0VudHJ5Gi4KDE1ldHJpY3NFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAE6AjgBGi4KDFNhbXBsZXNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAM6AjgBInQKGUFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2USNwoHYnVja2V0cxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQSHgoWc2NvcGVfY29udHJhY3RfdmVyc2lvbhgCIAEoDSIcChpMaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdCLBAQoOQXVkaXRFdmVudFR5cGUSDAoEbmFtZRgBIAEoCRIPCgd2ZXJzaW9uGAIgASgFEhAKCGNhdGVnb3J5GAMgASgJEg0KBW93bmVyGAQgASgJEhIKCmRlcHJlY2F0ZWQYBSABKAgSEwoLZGVzY3JpcHRpb24YBiABKAkSEQoJbmFtZXNwYWNlGAcgASgJEhkKEW1hcmtzX3VzZXJfam9pbmVkGAggASgIEhgKEHdlYmhvb2tfZWxpZ2libGUYCSABKAgiTgobTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlEi8KBXR5cGVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5BdWRpdEV2ZW50VHlwZTKdBgoMQXVkaXRTZXJ2aWNlErUBCg1RdWVyeUF1ZGl0TG9nEiYuc2Fhcy5hY2NvdW50cy52MS5RdWVyeUF1ZGl0TG9nUmVxdWVzdBonLnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1Jlc3BvbnNlIlPC8xg6CAIQAxoKYXVkaXQ6cmVhZCIKYXVkaXQ6cmVhZCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCDxINL3YxL2F1ZGl0LWxvZxLLAQoRQWdncmVnYXRlQXVkaXRMb2cSKi5zYWFzLmFjY291bnRzLnYxLkFnZ3JlZ2F0ZUF1ZGl0TG9nUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuQWdncmVnYXRlQXVkaXRMb2dSZXNwb25zZSJdwvMYOggCEAMaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhkSFy92MS9hdWRpdC1sb2c6YWdncmVnYXRlEsEBChNMaXN0QXVkaXRFdmVudFR5cGVzEiwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXVkaXRFdmVudFR5cGVzUmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuTGlzdEF1ZGl0RXZlbnRUeXBlc1Jlc3BvbnNlIk3C8xgsCAIQARoKYXVkaXQ6cmVhZCIKYXVkaXQ6cmVhZDABOgIQAUABSANQAlgCYAGC0+STAhcSFS92MS9hdWRpdC1ldmVudC10eXBlcxLCAQoORXhwb3J0QXVkaXRMb2cSJy5zYWFzLmFjY291bnRzLnYxLkV4cG9ydEF1ZGl0TG9nUmVxdWVzdBooLnNhYXMuYWNjb3VudHMudjEuRXhwb3J0QXVkaXRMb2dSZXNwb25zZSJdwvMYOggCEAQaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhk6ASoiFC92MS9hdWRpdC1sb2c6ZXhwb3J0QrIBChRjb20uc2Fhcy5hY2NvdW50cy52MUIKQXVkaXRQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_policy_v1_options]); + fileDesc("ChxzYWFzL2FjY291bnRzL3YxL2F1ZGl0LnByb3RvEhBzYWFzLmFjY291bnRzLnYxItEDCgpBdWRpdEV2ZW50EgoKAmlkGAEgASgJEhAKCGFjdG9yX2lkGAIgASgJEhIKCmFjdG9yX3R5cGUYAyABKAkSEgoGYWN0aW9uGAQgASgJQgIYARIQCghyZXNvdXJjZRgFIAEoCRITCgtyZXNvdXJjZV9pZBgGIAEoCRIOCgZvcmdfaWQYByABKAkSQAoIbWV0YWRhdGEYCCADKAsyKi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQuTWV0YWRhdGFFbnRyeUICGAESEgoKaXBfYWRkcmVzcxgJIAEoCRIuCgpjcmVhdGVkX2F0GAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpldmVudF90eXBlGAsgASgJEhYKDnNjaGVtYV92ZXJzaW9uGAwgASgFEigKB3BheWxvYWQYDSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0EhAKCGNhdGVnb3J5GA4gASgJEhEKCWNsaWVudF9pZBgPIAEoCRIUCgxhY3Rvcl9tb2R1bGUYECABKAkaLwoNTWV0YWRhdGFFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIvgDChRRdWVyeUF1ZGl0TG9nUmVxdWVzdBIOCgZvcmdfaWQYASABKAkSEAoIYWN0b3JfaWQYAiABKAkSEgoGYWN0aW9uGAMgASgJQgIYARIQCghyZXNvdXJjZRgEIAEoCRITCgtyZXNvdXJjZV9pZBgFIAEoCRIoCgRmcm9tGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBImCgJ0bxgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASHAoJcGFnZV9zaXplGAggASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgJIAEoCRISCgpldmVudF90eXBlGAogASgJEhAKCGNhdGVnb3J5GAsgASgJElUKEHBheWxvYWRfY29udGFpbnMYDCADKAsyOy5zYWFzLmFjY291bnRzLnYxLlF1ZXJ5QXVkaXRMb2dSZXF1ZXN0LlBheWxvYWRDb250YWluc0VudHJ5EhEKCW5hbWVzcGFjZRgNIAEoCRIRCgljbGllbnRfaWQYDiABKAkSJAoLZXZlbnRfdHlwZXMYDyADKAlCD7pIDJIBCRBAIgVyAxj/ARo2ChRQYXlsb2FkQ29udGFpbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBInMKFVF1ZXJ5QXVkaXRMb2dSZXNwb25zZRIsCgZldmVudHMYASADKAsyHC5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RXZlbnQSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIpcBChVFeHBvcnRBdWRpdExvZ1JlcXVlc3QSDgoGb3JnX2lkGAEgASgJEg4KBmZvcm1hdBgCIAEoCRIQCghhY3Rvcl9pZBgDIAEoCRISCgZhY3Rpb24YBCABKAlCAhgBEhIKCmV2ZW50X3R5cGUYBSABKAkSJAoLZXZlbnRfdHlwZXMYBiADKAlCD7pIDJIBCRBAIgVyAxj/ASJOChZFeHBvcnRBdWRpdExvZ1Jlc3BvbnNlEgwKBGRhdGEYASABKAwSFAoMY29udGVudF90eXBlGAIgASgJEhAKCGZpbGVuYW1lGAMgASgJIksKC0F1ZGl0TWV0cmljEgoKAm9wGAEgASgJEg0KBWZpZWxkGAIgASgJEhIKCnBlcmNlbnRpbGUYAyABKAESDQoFYWxpYXMYBCABKAkiSwoSQXVkaXREZXJpdmVkTWV0cmljEg0KBWFsaWFzGAEgASgJEhEKCW51bWVyYXRvchgCIAEoCRITCgtkZW5vbWluYXRvchgDIAEoCSLtBAoYQWdncmVnYXRlQXVkaXRMb2dSZXF1ZXN0Eg4KBm9yZ19pZBgBIAEoCRIQCghhY3Rvcl9pZBgCIAEoCRISCgpldmVudF90eXBlGAMgASgJEhAKCGNhdGVnb3J5GAQgASgJEhAKCHJlc291cmNlGAUgASgJEigKBGZyb20YBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEiYKAnRvGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIQCghncm91cF9ieRgIIAEoCRIOCgZidWNrZXQYCSABKAkSEQoJZ3JvdXBfYnlzGAogAygJEi4KB21ldHJpY3MYCyADKAsyHS5zYWFzLmFjY291bnRzLnYxLkF1ZGl0TWV0cmljEjUKB2Rlcml2ZWQYDCADKAsyJC5zYWFzLmFjY291bnRzLnYxLkF1ZGl0RGVyaXZlZE1ldHJpYxIRCgluYW1lc3BhY2UYDSABKAkSEwoLcmVzb3VyY2VfaWQYDiABKAkSWQoQcGF5bG9hZF9jb250YWlucxgPIAMoCzI/LnNhYXMuYWNjb3VudHMudjEuQWdncmVnYXRlQXVkaXRMb2dSZXF1ZXN0LlBheWxvYWRDb250YWluc0VudHJ5EhUKDWNvbGxlY3Rpb25faWQYECABKAkSEQoJY2xpZW50X2lkGBEgASgJEiQKC2V2ZW50X3R5cGVzGBIgAygJQg+6SAySAQkQQCIFcgMY/wEaNgoUUGF5bG9hZENvbnRhaW5zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKsAgoUQXVkaXRBZ2dyZWdhdGVCdWNrZXQSCwoDa2V5GAEgASgJEg0KBWNvdW50GAIgASgDEgwKBGtleXMYAyADKAkSRAoHbWV0cmljcxgEIAMoCzIzLnNhYXMuYWNjb3VudHMudjEuQXVkaXRBZ2dyZWdhdGVCdWNrZXQuTWV0cmljc0VudHJ5EkQKB3NhbXBsZXMYBSADKAsyMy5zYWFzLmFjY291bnRzLnYxLkF1ZGl0QWdncmVnYXRlQnVja2V0LlNhbXBsZXNFbnRyeRouCgxNZXRyaWNzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgBOgI4ARouCgxTYW1wbGVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgDOgI4ASJ0ChlBZ2dyZWdhdGVBdWRpdExvZ1Jlc3BvbnNlEjcKB2J1Y2tldHMYASADKAsyJi5zYWFzLmFjY291bnRzLnYxLkF1ZGl0QWdncmVnYXRlQnVja2V0Eh4KFnNjb3BlX2NvbnRyYWN0X3ZlcnNpb24YAiABKA0iHAoaTGlzdEF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QiwQEKDkF1ZGl0RXZlbnRUeXBlEgwKBG5hbWUYASABKAkSDwoHdmVyc2lvbhgCIAEoBRIQCghjYXRlZ29yeRgDIAEoCRINCgVvd25lchgEIAEoCRISCgpkZXByZWNhdGVkGAUgASgIEhMKC2Rlc2NyaXB0aW9uGAYgASgJEhEKCW5hbWVzcGFjZRgHIAEoCRIZChFtYXJrc191c2VyX2pvaW5lZBgIIAEoCBIYChB3ZWJob29rX2VsaWdpYmxlGAkgASgIIk4KG0xpc3RBdWRpdEV2ZW50VHlwZXNSZXNwb25zZRIvCgV0eXBlcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQXVkaXRFdmVudFR5cGUynQYKDEF1ZGl0U2VydmljZRK1AQoNUXVlcnlBdWRpdExvZxImLnNhYXMuYWNjb3VudHMudjEuUXVlcnlBdWRpdExvZ1JlcXVlc3QaJy5zYWFzLmFjY291bnRzLnYxLlF1ZXJ5QXVkaXRMb2dSZXNwb25zZSJTwvMYOggCEAMaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAg8SDS92MS9hdWRpdC1sb2cSywEKEUFnZ3JlZ2F0ZUF1ZGl0TG9nEiouc2Fhcy5hY2NvdW50cy52MS5BZ2dyZWdhdGVBdWRpdExvZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkFnZ3JlZ2F0ZUF1ZGl0TG9nUmVzcG9uc2UiXcLzGDoIAhADGgphdWRpdDpyZWFkIgphdWRpdDpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIZEhcvdjEvYXVkaXQtbG9nOmFnZ3JlZ2F0ZRLBAQoTTGlzdEF1ZGl0RXZlbnRUeXBlcxIsLnNhYXMuYWNjb3VudHMudjEuTGlzdEF1ZGl0RXZlbnRUeXBlc1JlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBdWRpdEV2ZW50VHlwZXNSZXNwb25zZSJNwvMYLAgCEAEaCmF1ZGl0OnJlYWQiCmF1ZGl0OnJlYWQwAToCEAFAAUgDUAJYAmABgtPkkwIXEhUvdjEvYXVkaXQtZXZlbnQtdHlwZXMSwgEKDkV4cG9ydEF1ZGl0TG9nEicuc2Fhcy5hY2NvdW50cy52MS5FeHBvcnRBdWRpdExvZ1JlcXVlc3QaKC5zYWFzLmFjY291bnRzLnYxLkV4cG9ydEF1ZGl0TG9nUmVzcG9uc2UiXcLzGDoIAhAEGgphdWRpdDpyZWFkIgphdWRpdDpyZWFkKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIZOgEqIhQvdjEvYXVkaXQtbG9nOmV4cG9ydEKyAQoUY29tLnNhYXMuYWNjb3VudHMudjFCCkF1ZGl0UHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_struct, file_google_protobuf_timestamp, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.AuditEvent @@ -109,6 +109,17 @@ export type AuditEvent = Message<"saas.accounts.v1.AuditEvent"> & { * @generated from field: string client_id = 15; */ clientId: string; + + /** + * actor_module names the module whose own principal is actor_id, by the + * prefix it is declared under in this host's module principal registry, so a + * reader can say "Module " instead of an unresolved id. Empty for every + * other actor, and for a module this host no longer declares. Read at query + * time; nothing about it is stored on the row. + * + * @generated from field: string actor_module = 16; + */ + actorModule: string; }; /** diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts index f5437db7a..04c9c75a3 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts @@ -17,7 +17,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installations.proto. */ export const file_saas_accounts_v1_installations: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi+QMKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIuADChZJbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoQYWdlbnRfaWRlbnRpZmllchgCIAEoCUIKukgHcgUQARiABBIeCgxkaXNwbGF5X25hbWUYBCABKAlCCLpIBXIDGIAEEiIKEHJvb3Rfc2NvcGVfbGFiZWwYBiABKAlCCLpIBXIDGIAEEhkKB3JvbGVfaWQYByABKAlCCLpIBXIDsAEBEicKEm93bmVyX3ByaW5jaXBhbF9pZBgIIAEoCUILukgI2AEBcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgJIAMoCUIPukgMkgEJECAiBXIDsAEBEioKEWFsbG93ZWRfYXVkaWVuY2VzGAogAygJQg+6SAySAQkQQCIFcgMYgAQSJwoOYWxsb3dlZF9zY29wZXMYCyADKAlCD7pIDJIBCRBAIgVyAxiAARIbCgl0YXJnZXRfaWQYDCABKAlCCLpIBXIDsAEBEh8KBXNjb3BlGA0gASgJQgu6SAhyBhACKICAQEgAiAEBQggKBl9zY29wZUoECAMQBEoECAUQBlITc29sdXRpb25faWRlbnRpZmllclIPcm9vdF9zY29wZV9wYXRoInQKGU5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhoKBXNjb3BlGAMgASgJQgu6SAhyBhACKICAQCJXChhSZXRyeUluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIlcKGFVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEivgEKJFRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESKAoWbmV3X293bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgEIAMoCUIPukgMkgEJECAiBXIDsAEBIlUKFkdldEluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIoUBChdHZXRJbnN0YWxsYXRpb25SZXNwb25zZRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLSAgoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50IoMBCiVMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlEkEKDWluc3RhbGxhdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLk9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikwEKHkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEiTgofR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZRIrCgVncmFudBgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuU2NvcGVHcmFudCKWAQohUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW1SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIZCgd0ZWFtX2lkGAMgASgJQgi6SAVyA7ABARIZCgdyb2xlX2lkGAQgASgJQgi6SAVyA7ABASLtAQoRQXZhaWxhYmxlU29sdXRpb24SGwoJdGFyZ2V0X2lkGAEgASgJQgi6SAVyA7ABARISCgpiaW5kaW5nX2lkGAIgASgJEhMKC3JvdXRlX2FsaWFzGAMgASgJEhkKEXJlbGVhc2VfcHVibGlzaGVyGAQgASgJEhQKDHJlbGVhc2VfbmFtZRgFIAEoCRIXCg9yZWxlYXNlX3ZlcnNpb24YBiABKAkSGQoRb3BlbmVkX2dlbmVyYXRpb24YByABKAQSGgoSYXBwbGllZF9nZW5lcmF0aW9uGAggASgEEhEKCWluc3RhbGxlZBgJIAEoCCJsCh1MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEh0KCXBhZ2Vfc2l6ZRgCIAEoBUIKukgHGgUY9AMoABISCgpwYWdlX3Rva2VuGAMgASgJInEKHkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZRI2Cglzb2x1dGlvbnMYASADKAsyIy5zYWFzLmFjY291bnRzLnYxLkF2YWlsYWJsZVNvbHV0aW9uEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSq7AQoSSW5zdGFsbGF0aW9uU3RhdHVzEiMKH0lOU1RBTExBVElPTl9TVEFUVVNfVU5TUEVDSUZJRUQQABIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0FDVElWRRABEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUkVWT0tFRBACEh8KG0lOU1RBTExBVElPTl9TVEFUVVNfUEVORElORxADEh4KGklOU1RBTExBVElPTl9TVEFUVVNfRkFJTEVEEAQqhAIKEkluc3RhbGxhdGlvbkhlYWx0aBIjCh9JTlNUQUxMQVRJT05fSEVBTFRIX1VOU1BFQ0lGSUVEEAASHwobSU5TVEFMTEFUSU9OX0hFQUxUSF9IRUFMVEhZEAESKQolSU5TVEFMTEFUSU9OX0hFQUxUSF9OT19FTElHSUJMRV9PV05FUhACEiUKIUlOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfUkVWT0tFRBADEiYKIklOU1RBTExBVElPTl9IRUFMVEhfQUdFTlRfRElTQUJMRUQQBBIuCipJTlNUQUxMQVRJT05fSEVBTFRIX1NUQU5ESU5HX0dSQU5UX01JU1NJTkcQBTKqEgoTSW5zdGFsbGF0aW9uU2VydmljZRK6AQoPSW5zdGFsbFNvbHV0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsU29sdXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24iXcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5jcmVhdGVkEAJAAUgEUANYA2ABgtPkkwIWOgEqIhEvdjEvaW5zdGFsbGF0aW9ucxLSAQoRVW5pbnN0YWxsU29sdXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ5wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldm9rZWQQAkABSARQA1gDYAGC0+STAjI6ASoiLS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnVuaW5zdGFsbBLSAQoSTmFycm93SW5zdGFsbGF0aW9uEisuc2Fhcy5hY2NvdW50cy52MS5OYXJyb3dJbnN0YWxsYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InfC8xg+CAIQBCoMCgZvcmdfaWQQAhgBMAE6Hgoac2Fhcy5pbnN0YWxsYXRpb24ubmFycm93ZWQQAkABSARQA1gDYAGC0+STAi86ASoiKi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9Om5hcnJvdxLWAQoRUmV0cnlJbnN0YWxsYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uInXC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV0cmllZBACQAFIBFADWANgAYLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06cmV0cnkSiQIKHVRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwEjYuc2Fhcy5hY2NvdW50cy52MS5UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcFJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiKPAcLzGEsIAhAEKgwKBm9yZ19pZBACGAEwATorCidzYWFzLmluc3RhbGxhdGlvbi5vd25lcnNoaXBfdHJhbnNmZXJyZWQQAkABSARQA1gDYAGC0+STAjo6ASoiNS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnRyYW5zZmVyT3duZXJzaGlwErkBCg9HZXRJbnN0YWxsYXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkdldEluc3RhbGxhdGlvblJlc3BvbnNlIlHC8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCJRIjL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0ShgEKEUxpc3RJbnN0YWxsYXRpb25zEiouc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVzcG9uc2UiGMLzGBQIAxABMAE6AhABQAFIB1ADWANgARLKAQoWTGlzdEF2YWlsYWJsZVNvbHV0aW9ucxIvLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QaMC5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXNwb25zZSJNwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiESHy92MS9pbnN0YWxsYXRpb25zOmxpc3RBdmFpbGFibGUS0QEKHUxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zEjYuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QaNy5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVzcG9uc2UiP8LzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwITEhEvdjEvaW5zdGFsbGF0aW9ucxLwAQoXR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW0SMC5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBoxLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZSJwwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUuZ3JhbnRlZBACQAFIBFADWANgAXgCgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcxLtAQoaUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW0SMy5zYWFzLmFjY291bnRzLnYxLlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSKBAcLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLnJldm9rZWQQAkABSARQA1gDYAF4AoLT5JMCPzoBKiI6L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMve3RlYW1faWR9OnJldm9rZUK6AQoUY29tLnNhYXMuYWNjb3VudHMudjFCEkluc3RhbGxhdGlvbnNQcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEi+QMKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEQg0KC19yZXZva2VkX2F0SgQIBBAFUhNzb2x1dGlvbl9pZGVudGlmaWVyIuADChZJbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESJAoQYWdlbnRfaWRlbnRpZmllchgCIAEoCUIKukgHcgUQARiABBIeCgxkaXNwbGF5X25hbWUYBCABKAlCCLpIBXIDGIAEEiIKEHJvb3Rfc2NvcGVfbGFiZWwYBiABKAlCCLpIBXIDGIAEEhkKB3JvbGVfaWQYByABKAlCCLpIBXIDsAEBEicKEm93bmVyX3ByaW5jaXBhbF9pZBgIIAEoCUILukgI2AEBcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgJIAMoCUIPukgMkgEJECAiBXIDsAEBEioKEWFsbG93ZWRfYXVkaWVuY2VzGAogAygJQg+6SAySAQkQQCIFcgMYgAQSJwoOYWxsb3dlZF9zY29wZXMYCyADKAlCD7pIDJIBCRBAIgVyAxiAARIbCgl0YXJnZXRfaWQYDCABKAlCCLpIBXIDsAEBEh8KBXNjb3BlGA0gASgJQgu6SAhyBhACKICAQEgAiAEBQggKBl9zY29wZUoECAMQBEoECAUQBlITc29sdXRpb25faWRlbnRpZmllclIPcm9vdF9zY29wZV9wYXRoInQKGU5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhoKBXNjb3BlGAMgASgJQgu6SAhyBhACKICAQCJXChhSZXRyeUluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIlcKGFVuaW5zdGFsbFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEivgEKJFRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESKAoWbmV3X293bmVyX3ByaW5jaXBhbF9pZBgDIAEoCUIIukgFcgOwAQESLwoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgEIAMoCUIPukgMkgEJECAiBXIDsAEBIlUKFkdldEluc3RhbGxhdGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBIoUBChdHZXRJbnN0YWxsYXRpb25SZXNwb25zZRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCKdAQoYTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESNAoGc3RhdHVzGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdGF0dXMSHQoJcGFnZV9zaXplGAMgASgFQgq6SAcaBRj0AygAEhIKCnBhZ2VfdG9rZW4YBCABKAkigQEKE0luc3RhbGxhdGlvblN1bW1hcnkSNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SNAoGaGVhbHRoGAIgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25IZWFsdGgicgoZTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZRI8Cg1pbnN0YWxsYXRpb25zGAEgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb25TdW1tYXJ5EhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLSAgoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50IoMBCiVMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlEkEKDWluc3RhbGxhdGlvbnMYASADKAsyKi5zYWFzLmFjY291bnRzLnYxLk9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikwEKHkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQESGQoHcm9sZV9pZBgEIAEoCUIIukgFcgOwAQEiTgofR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXNwb25zZRIrCgVncmFudBgBIAEoCzIcLnNhYXMuYWNjb3VudHMudjEuU2NvcGVHcmFudCKWAQohUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW1SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIZCgd0ZWFtX2lkGAMgASgJQgi6SAVyA7ABARIZCgdyb2xlX2lkGAQgASgJQgi6SAVyA7ABASKAAgoRQXZhaWxhYmxlU29sdXRpb24SGwoJdGFyZ2V0X2lkGAEgASgJQgi6SAVyA7ABARISCgpiaW5kaW5nX2lkGAIgASgJEhMKC3JvdXRlX2FsaWFzGAMgASgJEhkKEXJlbGVhc2VfcHVibGlzaGVyGAQgASgJEhQKDHJlbGVhc2VfbmFtZRgFIAEoCRIXCg9yZWxlYXNlX3ZlcnNpb24YBiABKAkSGQoRb3BlbmVkX2dlbmVyYXRpb24YByABKAQSGgoSYXBwbGllZF9nZW5lcmF0aW9uGAggASgEEhEKCWluc3RhbGxlZBgJIAEoCBIRCgloYXNfcGFnZXMYCiABKAgibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUyqhIKE0luc3RhbGxhdGlvblNlcnZpY2USugEKD0luc3RhbGxTb2x1dGlvbhIoLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbFNvbHV0aW9uUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIl3C8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24uY3JlYXRlZBACQAFIBFADWANgAYLT5JMCFjoBKiIRL3YxL2luc3RhbGxhdGlvbnMS0gEKEVVuaW5zdGFsbFNvbHV0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5Vbmluc3RhbGxTb2x1dGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiecLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXZva2VkEAJAAUgEUANYA2ABgtPkkwIyOgEqIi0vdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp1bmluc3RhbGwS0gEKEk5hcnJvd0luc3RhbGxhdGlvbhIrLnNhYXMuYWNjb3VudHMudjEuTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJ3wvMYPggCEAQqDAoGb3JnX2lkEAIYATABOh4KGnNhYXMuaW5zdGFsbGF0aW9uLm5hcnJvd2VkEAJAAUgEUANYA2ABgtPkkwIvOgEqIiovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpuYXJyb3cS1gEKEVJldHJ5SW5zdGFsbGF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5SZXRyeUluc3RhbGxhdGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJ1wvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLnJldHJpZWQQAkABSARQA1gDYAGC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9OnJldHJ5EokCCh1UcmFuc2Zlckluc3RhbGxhdGlvbk93bmVyc2hpcBI2LnNhYXMuYWNjb3VudHMudjEuVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24ijwHC8xhLCAIQBCoMCgZvcmdfaWQQAhgBMAE6Kwonc2Fhcy5pbnN0YWxsYXRpb24ub3duZXJzaGlwX3RyYW5zZmVycmVkEAJAAUgEUANYA2ABgtPkkwI6OgEqIjUvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTp0cmFuc2Zlck93bmVyc2hpcBK5AQoPR2V0SW5zdGFsbGF0aW9uEiguc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5HZXRJbnN0YWxsYXRpb25SZXNwb25zZSJRwvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAiUSIy92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9EoYBChFMaXN0SW5zdGFsbGF0aW9ucxIqLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5MaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlIhjC8xgUCAMQATABOgIQAUABSAdQA1gDYAESygEKFkxpc3RBdmFpbGFibGVTb2x1dGlvbnMSLy5zYWFzLmFjY291bnRzLnYxLkxpc3RBdmFpbGFibGVTb2x1dGlvbnNSZXF1ZXN0GjAuc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2UiTcLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIhEh8vdjEvaW5zdGFsbGF0aW9uczpsaXN0QXZhaWxhYmxlEtEBCh1MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9ucxI2LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXF1ZXN0Gjcuc2Fhcy5hY2NvdW50cy52MS5MaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1Jlc3BvbnNlIj/C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCExIRL3YxL2luc3RhbGxhdGlvbnMS8AEKF0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtEjAuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QaMS5zYWFzLmFjY291bnRzLnYxLkdyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2UicMLzGDgIAhAEKgwKBm9yZ19pZBACGAEwAToWChJzYWFzLnNjb3BlLmdyYW50ZWQQAkABSARQA1gDYAF4AoLT5JMCLjoBKiIpL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH0vdGVhbXMS7QEKGlJldm9rZUluc3RhbGxhdGlvbkZyb21UZWFtEjMuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkigQHC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5yZXZva2VkEAJAAUgEUANYA2ABeAKC0+STAj86ASoiOi92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zL3t0ZWFtX2lkfTpyZXZva2VCugEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhJJbnN0YWxsYXRpb25zUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); /** * Installation is one solution's bound identity in an org. Server-assigned ids; @@ -762,6 +762,21 @@ export type AvailableSolution = Message<"saas.accounts.v1.AvailableSolution"> & * @generated from field: bool installed = 9; */ installed: boolean; + + /** + * True when the applied generation declares pages people open — the + * `frontend` block the host projects into its navigation. Read from the same + * applied record the navigation is built from, never from the kind: a module + * or a solution may declare pages, and either may be headless. It reports + * pages only: a grant on a headless installation can still matter, because a + * solution may call that component on a person's own session and the host + * then admits only a person with access there. The applied manifest does not + * record that session consumption yet, so this field alone does not decide + * whether access applies. + * + * @generated from field: bool has_pages = 10; + */ + hasPages: boolean; }; /** diff --git a/module/services/frontend/code/src/solutions/SolutionDashboard.tsx b/module/services/frontend/code/src/solutions/SolutionDashboard.tsx index 42ce473b0..6671f8352 100644 --- a/module/services/frontend/code/src/solutions/SolutionDashboard.tsx +++ b/module/services/frontend/code/src/solutions/SolutionDashboard.tsx @@ -486,7 +486,8 @@ function MetricInfo({ orgId, recent.data?.map((event) => event.actorId) ?? [], ); - const who = (actorId: string) => resolveActor(actorId, directory).label; + const who = (event: { actorId: string; actorModule: string }) => + resolveActor(event.actorId, directory, event.actorModule).label; const summary = activity.data; const counted = activity.isPending @@ -506,7 +507,7 @@ function MetricInfo({ : recent.isPending ? "Loading…" : newest - ? `${formatMoment(newest.at)}, by ${who(newest.actorId)}` + ? `${formatMoment(newest.at)}, by ${who(newest)}` : "No events yet"; return ( @@ -577,8 +578,7 @@ function MetricInfo({
    {recent.data.map((event) => (
  • - {formatMoment(event.at, { year: false })} ·{" "} - {who(event.actorId)} + {formatMoment(event.at, { year: false })} · {who(event)}
  • ))}
diff --git a/module/services/frontend/code/src/solutions/__tests__/metric-info.test.ts b/module/services/frontend/code/src/solutions/__tests__/metric-info.test.ts index 5d28fc6b8..4457ff4d6 100644 --- a/module/services/frontend/code/src/solutions/__tests__/metric-info.test.ts +++ b/module/services/frontend/code/src/solutions/__tests__/metric-info.test.ts @@ -328,6 +328,20 @@ describe("newestEvents", () => { expect(newest.map((e) => e.id)).toEqual(["a", "c"]); expect(newest[0].at.toISOString()).toBe("2026-09-24T14:32:00.000Z"); }); + + it("keeps the module the host named for a module acting on its own", () => { + const [event] = newestEvents([ + [ + { + id: "m", + actorId: "p9", + actorModule: "example-chat", + createdAt: at("2026-09-24T14:32:00Z"), + }, + ], + ]); + expect(event.actorModule).toBe("example-chat"); + }); }); it("formats a day bucket as its UTC calendar day", () => { diff --git a/module/services/frontend/code/src/solutions/metric-info.ts b/module/services/frontend/code/src/solutions/metric-info.ts index 3d45fd4c3..f55e4ad39 100644 --- a/module/services/frontend/code/src/solutions/metric-info.ts +++ b/module/services/frontend/code/src/solutions/metric-info.ts @@ -275,6 +275,8 @@ export function recentEventsQueries( export interface RecentEvent { id: string; actorId: string; + /** The module whose own principal acted, as the host names it; else "". */ + actorModule: string; at: Date; } @@ -286,6 +288,7 @@ export function newestEvents( results: readonly (readonly { id: string; actorId: string; + actorModule?: string; createdAt?: Timestamp; }[])[], limit = 5, @@ -296,6 +299,7 @@ export function newestEvents( byId.set(event.id, { id: event.id, actorId: event.actorId, + actorModule: event.actorModule ?? "", at: timestampDate(event.createdAt), }); }