From b1150ee590d78c021e52e4a56d412d90f23ee970 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:22:26 -0400 Subject: [PATCH 1/2] feat(accounts,frontend): installed access follows pages, a failed install retries in place, refusals and module actors read in words The Modules & solutions page showed "No team" and "Grant a team" on every installed module, which reads as if every module needs people granted to it, and a failed installation offered only a bare "Retry" beside "Unavailable" and "Available after activation". - accounts: AvailableSolution.has_pages says whether the applied generation declares pages, read from the registration's frontend half (the record the navigation is built from), never from the kind. It rides on the installed rows through OrganizationInstallation.target. - accounts: the installation RPC error mappers answer with the carried status's own message (innerStatus), so a refusal no longer reads as "cannot grant scope: rpc error: code = ... desc = ...". - accounts: QueryAuditLog names a module acting on its own principal (AuditEvent.actor_module, the prefix the module principal registry declares it under) at read time; nothing is stored on the row. - frontend: the Teams column becomes Access; "Grant a team" becomes "Grant access" and the dialog asks for a holder and an access level (team and role today), so a member holder or a level ladder adds a field rather than a new shape (#1162). A row with pages and no grant says "No one has access yet"; a row without pages drops "No team" and shows one muted line saying when a grant still matters, with the action kept but secondary, because a solution may reach that module on a person's own session and the host cannot see that consumption yet (#1163). - frontend: a failed installation shows "Retry installation" as the primary action with a line saying nothing needs uninstalling; Health reads "Not running" and Access "Available once the installation succeeds". - frontend: Recent activity and the audit table name a module actor "Module ". - Regenerated: Go and TS bindings, OpenAPI, contracts, the package manifest and the SDK's vendored contract. AuditEvent is on @codefly-dev/saas-sdk's published surface; the unreleased 0.3.32 already on main carries it. Co-Authored-By: Claude Opus 5.5 --- module/SOLUTION_REGISTRATION.md | 15 ++- .../api/accounts/authority/contract.binpb | Bin 1179687 -> 1180734 bytes .../proto/saas/accounts/v1/audit.proto | 6 + .../saas/accounts/v1/installations.proto | 8 ++ .../api/accounts/connect/contract.binpb | Bin 1179687 -> 1180734 bytes .../proto/saas/accounts/v1/audit.proto | 6 + .../saas/accounts/v1/installations.proto | 8 ++ module/contracts/api/catalog.codefly.json | 4 +- module/module.package.codefly.yaml | 4 +- module/services/accounts/AGENTS.md | 4 + .../pkg/adapters/audit_actor_module_test.go | 23 ++++ .../installation_exposure_rpcs_test.go | 21 +++- .../code/pkg/adapters/installation_rpcs.go | 11 +- .../adapters/installation_scope_rpcs_test.go | 7 ++ .../accounts/code/pkg/adapters/rpcs.go | 19 ++- .../code/pkg/business/solution_targets.go | 5 + .../code/pkg/gen/saas/accounts/v1/audit.pb.go | 20 +++- .../gen/saas/accounts/v1/installations.pb.go | 23 +++- .../pkg/infra/postgres_solution_targets.go | 7 +- .../postgres_solution_targets_pages_test.go | 53 +++++++++ .../generated/openapi-raw/api.swagger.json | 8 ++ .../accounts/openapi/api.swagger.json | 8 ++ .../proto/saas/accounts/v1/audit.proto | 6 + .../saas/accounts/v1/installations.proto | 8 ++ module/services/frontend/AGENTS.md | 5 + .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1179687 -> 1180734 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../src/gen/saas/accounts/v1/audit_pb.ts | 13 ++- .../gen/saas/accounts/v1/installations_pb.ts | 15 ++- .../__tests__/activity-feed.test.tsx | 20 ++++ .../code/src/components/activity-feed.tsx | 3 +- .../audit/model/__tests__/transforms.test.ts | 22 ++++ .../src/features/audit/model/transforms.ts | 13 ++- .../code/src/features/audit/model/types.ts | 4 + .../src/features/audit/ui/audit-table.tsx | 15 ++- .../model/installations.test.ts | 14 +++ .../model/installations.ts | 26 ++++- .../ui/grant-team-dialog.tsx | 9 +- ...tion-installations-page.container.test.tsx | 108 ++++++++++++++---- .../ui/solution-installations-page.tsx | 65 +++++++---- .../code/src/gen/saas/accounts/v1/audit_pb.ts | 13 ++- .../gen/saas/accounts/v1/installations_pb.ts | 15 ++- 43 files changed, 552 insertions(+), 86 deletions(-) create mode 100644 module/services/accounts/code/pkg/adapters/audit_actor_module_test.go create mode 100644 module/services/accounts/code/pkg/infra/postgres_solution_targets_pages_test.go 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 467ab5af0442797a71bc1521a6f022dbb2e5cc7d..4bf7dc703ce759643c8ac02787a5aed8c9af3fe3 100644 GIT binary patch delta 7773 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 F{~ue>GJ603 delta 6807 zcmZ`;X^<7w6@K^j?Y{lGXYJWwWO?(3bp}Q@B`SHtLypYHQ_`;yX9V=iNdOaKR!mD03|fg1`}sMb03< zPf!xLg7J(SIfJ<$gn=MLpxQ1$ad5WWA_pj{GKj%!NMZ)XA-vWu5z1CtRDj)(>KX$% z?1sXwkJ^qC7?Ll~Enm7Yw{XS6#mjD9wsOfrs|!a%ll?jT3{CduK2Y`GpfvDsZ0^%y z%^+jwV3Q_$xB?kAC8u-Xv?)2ALu<+51niov9Tx<88<$|vY0g?^1k9T2dYLuXi=1Kn z5H@r_ji(odqA#aex4lP^)QO0B> zTKGuUuEDOwx7_#VcSpt^ZEbK-nb~uwWy4Ly(U^XT( z3kcMilLRmulVmc0*_diugCW=)%WHx=0wEDt2o%@dBJXS-TWgCXgu&RM&Bj>?_PG-K zJk1oVB|!1Zx5(WiuFOVO1qN5v*xgHE&?wD?S~;P~kbM!Obwo0E(O$rojO~ zafWFyKyX)>VLFVXjz1H2eZc`xsCZ_X5(aY6EM}Q*AYYKPUo0=2?V=2U|X~5qC)1PLbg-+I|xzC<@4P7 zhgkecoBUQ>dZ)ahFxS*Fmf>lxsbwH|nwx7-wG4z>o|~ViYnh;`<++XXbS)bRXuHYO zvXQ7-zS-2Wh{XKN_jUT|*z^5njET%>};hY(O~Hg7OL#pit>w zXovxdoQ1sJE)mM=D}W`KE$pRx8LAh2F4ATr>FyBHO$h*Yi%hnGV7Ca#?&2t4@V7W~ zcd_#>c?IjU*i^Wy&b|cRt^>(Hk+UTJ<5D@Lc}X@@XMqe}mXzr?+Es^M2K{}M6~M?@ z7SCHIUv65KEi(pm++~%zr@895D`0S=(#L66#N(IC8BHrp+Mxmc6(;Sjs>zkmU##@8 zv@$-tTrTOiGVxEfw-Ww$Q&?W)tzwz>;-nRFAS4*asXGL{M&25hImj5gk|QKE*Dyb)G^Glr!+Hgb1dw(E1lqR$0f=EqLkvq|V`{2Vqg{mQMuvIG=)!a(!@#5=1}4$Tc&Q(3rh6f2X6lU3 zuau7r?@Z5%VF_AEn3!|KCJOTgWr8NPDSq{CIlX0*N~TI4S}AmrTxP+=7KWzyl-?!_ z7`NUdca7V^&}jiGOHMc#CD4Ohsr|< zP(1BExw2(DGZ#zp5}0gfm}qoR7>2}7GI?A_UJ4lh>puBJ%T9ZtB*zd0r8<4xBrih> zcQMRpySUm1W6-YRuYV*5vzA>9Q=0aSfdyI^(=;r{Mtf+Z_w`2QfN|ed^23%rENclW zhs_?=+frNso4sVSR@+nn#?5VV{m8v(Kozjrn+8+~7WWa0TlC>7BMJZ&=W4qimyqo!@RtYxUtU_0-&+!o?dWeTP$^@tEiT}_pzcaqa zQUDz|>S2*)9}pgTSa03weeNL5M=8yp>RqvVG`{Bn`B}?RR;y`)5=ogE34D4t#4&22 zA7~pK;#eH7k!K3WSdC?Xddo4UrcExgk@p76{KjxackB)3>j@)E)2uj7K~UELRA`tA z7zleEXJLN}VS1e9=)=+_U2JlkHRwDPs+s0Ny-C{x-JF#kBbU@g?M=!b#-h=XKI`Jl|vXG+?PREz8mz!Ho+r9uH zY);z=BINXxoS~!cag+^Sk~78g>*aH-#3pB{Hpd0r%S5jCIm^5jU;ATuRpE2yR|jQ*@G|h|onw3rcPIeDaB_}$ zDw_mR80T2^5W5PW=h)~8)bnUnMBd++Xh*#0c}j5TA%|pPJ9nw)SsA?koq2<_D1agK z;QAkNWv8ra{epS@;UTcv0&)J6dG$ecDsyqt^UV7eSnM06Qh@ocjpTv3olEe zAQ=LDfak^-rdi~5aQ@STX}X^s+*37yz;WpTdyO!g&~@$LSKVlsW?*_P7mp=OGh&cr z;bV&F0#U7maJ!CMsO0Y_wpJwe-Z@2d--LP48aZvex8e-gkVP#{5%hP z6u&vDawUY@7gBOqXWyix?B>sH-Ws47Q2~K?}-rvlFxUjl>V6k&^_+>di%Zr^)hqp5~kJ-3y z%w7z8702!l{}KiVe#`W~7LF80><|AspS@UYs4e-ChhKA11ej{w%qy<`8je8wKG)Y@ z6Oo!-_Pb_w0mNYI@>aS$0tmYMUHTf<5UJkpnhz+fg3`{TQpT`cR!gFqamIl4 z##?&OMdP^(?{9>g>H!^c-TLpvtFk3S<0t!Lsoal)dRVaVV|Ev%N#|i!Bx@458>J~eZ6+2aiTte)C5q#M$|*PT=V@6 z2-_}K{fxpzRpfQMnIne2Drk1QzW(5;Qjhy#*9@zGs0)|t`4b40dDsp0U;zlz!*1Wi z6q_7&hcfJ F=znH?^y>fs 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..05aca7cb0 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,14 @@ 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. Access to an + // installation is granted for its pages, so a catalogue offers a grant only + // where this is true; a headless component is reached through the components + // that use it, and no grant of its own exposes anything to a person. + 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 467ab5af0442797a71bc1521a6f022dbb2e5cc7d..4bf7dc703ce759643c8ac02787a5aed8c9af3fe3 100644 GIT binary patch delta 7773 zcmZ8meUwzimG7!o@1x(F&o>_o!`EYwsC-+e zpb*waA^}7}0prscm55@pCIQjd$_5cKDC;pN#@(FY*M{t#v;48^x`{an`7-;vRrOv^ zat?D2zgzd-y7jxaZq@60dfN>z@48{zcD9{wH@1uI=636O`x`g%KOcx5yU~7~eO$|b z+MdFv`BUA7OtxAouNf-!jTCZSEdjeMz1)+s&qz^i*JtgKX7|@c*Y?d+>UAFdB52=#@}6fd7b#yfrDi<6X0>gF4X!CtyxH`P%emuT;I1G@XhQefJQzDwP|OVFDnWHH z7pS>|+XqXPTu{2b7z~&5#cX~!QwZ`EZIthu6|CD#>Mn+J{rNkBe3gd#a@j(roa+ll zihZrQa*!`V3352AURsHM@=kMiI4h`>f{YuKbD2J;@&oQdZH;f_|SI%fyRGVhD<* zFesFY15nnnR(gZwG*6})Y#foks`;VZHPD1raUjU7D~(`na(NXb2N9HLRk@&Cy1mzR zHOmaVraNL}0Cjb{E;7P=F)Q^shW{IaK`Wf)Oq6Pjr;wW2NR5Zg01;@9hoCt--5ewM zo9(x0#9-E&m_f0Zcg916(ycLqS#P@vig6CJE4bsi6XkRqDEd>MJvqE0nTNyRiew({ z0JUH0Ho6wV;|>jWd_wy+;B8J41cv4$K^O>PP7;JcV@VJKW^)rWK_DEjU4klOZaQW_ zz;5oiEp#R7hn8IQqmffI9V!)^hV*C@T%4pcj@5^js@kfF0PZ9p>a zDg%;qEWxs`6zNzJk`lapQZ(op9L&BjAqiPn$YUNHOujH3R|2JEVdpq)mWJ8YiCLOJ zOl=xwS0`qO$+)^RcKLTM>=q?vMc0ej=j zC7p4Nc+g*R`Ai*N1EyJuX`W=#paCfQkA`IGdNP4KWR=+XIW zLjIObTOQ}H390cRxny~qKPkc1xk@Q+56tE$? z*D4(bkZ-Kg-3tJUt8`ZZg1d>UbXzfG__x7s5;y?zjoYFRhU_Vs+Y$qc`?f@%!d{)s zgRocYPGiWhSL^Zwg4t@_X$(#cY7NY&+W`SYLq&AEHOVxz>DDCE7}#fPbhm>(ZLKxB z+ZmkN*4iWnByp{#4ntVMTD`S^V76B8NQ0Zu%p%>7QUwG0BFlMy)KIb?4YRr+A%=&n zE=VAF$OcnoK?0#5vpxN)APLHX%uef91*s(4Fr#$ft%>4k_HcGg%d+ zkV#yxYXZnO)^oK_0HC?vQ9TI=VXbd&k+~N#hZ{5>K)$hocgI77^ht;zm~H4%Z3ra@ z8Vky79&G?Zs+|D9uAp-X1iJ!qd7PsLP2a4hwz1l|Vf$(}v8wCTl)-O;yU&7MAm7*& z%`V$F&D@msWH6Ay&88-GjG8j)Ezp0K(gEljTcU53?dN7}NjGT&ig!z^>PM!GcPk8T zmih>HYxLEMy<)~zoo{GBf2+>7DQhl-eo^XUD2&XiJv=!~{8P<^@PC-rW%=v8C)ue<{);}O`!`kE*_K+ z-BatrbQkOBlrXetx#I4+E&3SHc&!Y&0C7AkTPz$c6c6MO5nPzF-*Vo*YqjMjzeUZ#&? zTQ26lx+xX|)TMQ4G50ZKQJKXG-<$S$7cJf#g8!bLKO@yDp2w# z#1JkXVUy!UyKwOsxu83T&0U~qV2izR@ng)3ImVR*I^*KS;?4p%HKxLYhlAwd2I;{A zirT+q|NV6bnKwyK>cPW7HaR99w-~_7X6R8|W5gj^`R&p}gG623`G^hLozeHdWUuZ$ z#Pk-kc>|0OG2Bqp@^GaQhspSH#X%!rblq0_o4tqQtz~muYM?YuZ9JPdA(_uGT(q8| zFF0VpTFXD$m$Kex817oiGcF*|!evWAeD#SVG||7SiJAeU+@1FM-Xkm>Q`HQcBW!$3 zb_;Bdk?|8Ez}?&RT|j{363y*M`fN7SUNJ zUSc@x8qieX=>$vth{UxfV@99@N2APF$pD0hQ8r#30S-5i^|O@q|53AI^lbFQyX^n%J3i%=pd)0YK@ z4ixbQ_12#&8wBx2^z-fZFTyuiN6Y{iPY4X~Mt^HG$nl8^1+R;B{4t<+(;rZ6jSnB_D#I#j^#vH|=C$oEwrlE)d zeF3CSJ0R$uV`-H!KumWoj8o>xqvmZ&v$BOUkUFL?D4~O63d;k8?b~r@^W;(UPJLQr z8z_#6()>=`N1+STcj7+kg>+>Q@78TG3Z%{@kJC}}ZhX`O%29pPdvq@n@8Qc;M~rL% z#naMJ^B%KRK|&X%?5RERp?=$tHaKD~)Rx|DKf`)2Fgx~4XUqkL4;D2M zoiQJh(QlLy4w(;YOZ{3`M+4UJE9R))-yrMx zqqv>{eF3BnP#`ShBjzaz0AczO>sGZ1h)F)8+Fa~%E9INkZ&~W~=xclIYr@|$r`>IG zg_VLw>muX#a)TB?7)~xSOJsS4b%c@RQT5KnAnZ=q6JF9i+c@{ zdAqquEijhB>+hNM2_>2bL+HWvADA^0rf#feV1a*R)|9a_`hD|7JovZPq>9k_7@X&;bCu=IA+S z@9yE(gg0y68F=5z#p4O@jQAv(_jtm)l)>$NJm#HC5nSKT6WXPSXx~?-QObz+{oLwy z+hux478)kXVA~XK^|PIdFQ_M1n18tyi=!pf?IM6fp>h~ z0d_CYyi?REmhxZ`V1MGI07&bxx%SoIX?MREogqcer)MSpVSkyJNIM ziV&QBug)pm2aa-aZDOT3J<7c$$r^NFALYSfO)#qP80Y_z5KPCxF>c922?W!oKgK)c z#grfjKE|)S(JiTlVu0c2xEM_sHWb6pac?wXSjyn``GjFEMR5InZq3laNf8nJLY+n_ zqr$F5zuXVfU*y&gl2Vp~p#8*O=@acQ*J+n(p#5Z>b}0hwuha>ZBG7)SUdmDg+D|8j zD^=W}{fDGhr{D}1XF)-m?4O1zX6p?1&VmEgEtL=%dX-x}e<8y$_1eyN57_u~@RzQ= zd%*p-kNJs*YtQcYrnm6jM(x}a-Vp1-2TOV|Q&}^d8OT+-T)f*j%N+=ABHV-f^rYw~ z-|;$YYoGLf>3P3-pQ-no*Gzi9SsG1Q)apgwtu=eKV@JJ*v#eJ8bbG^{)+{;)TRDE$ z6)%`r!+!c*6IjB|?z=>Ab9p3(rE#cTEd%01@A(e@&lP_zEsrDB(NQl z1{FNhxIt!kxRA$_NI91+mHUE_-bLy*^dz)yBwwfoWHu`((rbthK5VUT#;UgLVDH84q~3_V7W9hFeulVXjEKaJT#5qX)Rw>>nWDv$@(SZ z^yHSiV;HY*h@-e!Jwnl|zF^wwGX)Ua|Ao+OqhFBGZhY${Gj-Zj zzsUGdeaUpx7p*U^$;VB7O$PJ@(6!Pn5Oj~5^otB;0fgJ*rv8Wr#BM!qwlASZMx*#l zYw9FX$6_)V4;kI{Pnc*0WmGF5zQ;`dO0;@H!^ZH0iH8&An9ku7CVo)BRwXt1ZZkVy zG1V6!3Fz$vvT?7N>I+cI;QnQ3=?J76T%9!We4;$l4=1PU%R|cnl6TAy6VWMk%G5vo z0K)c^DSyG^qRqEP&D3j}wKiyunvVLyZj;x?Gp7C=0`vut`oaN1vpZvY>goyz(=+Cz z#1xa9F)yF7P)69siK18I2mvKhA9X+y;zxX14Ipe^-Dys0z+WT&n()_*zm}RgskP@n F{~ue>GJ603 delta 6807 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 F=znH?^y>fs 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..05aca7cb0 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,14 @@ 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. Access to an + // installation is granted for its pages, so a catalogue offers a grant only + // where this is true; a headless component is reached through the components + // that use it, and no grant of its own exposes anything to a person. + bool has_pages = 10; } message ListAvailableSolutionsRequest { diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 709b51a03..db985e455 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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d", "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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d", "services": [ { "name": "APIKeyService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 28fcb967a..f497ab7fd 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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:19b078707414a2946f30788ead4245bb1a4ed970789fd5aced5e5328702c259f + digest: sha256:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d - 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..9e9c5ffaf 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,15 @@ 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. Access to an + // installation is granted for its pages, so a catalogue offers a grant only + // where this is true; a headless component is reached through the components + // that use it, and no grant of its own exposes anything to a person. + HasPages bool `protobuf:"varint,10,opt,name=has_pages,json=hasPages,proto3" json:"has_pages,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1521,6 +1529,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 +1748,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 +1760,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..54a5a4322 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. Access to an\ninstallation is granted for its pages, so a catalogue offers a grant only\nwhere this is true; a headless component is reached through the components\nthat use it, and no grant of its own exposes anything to a person." } }, "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..acfb9e14b 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. Access to an\ninstallation is granted for its pages, so a catalogue offers a grant only\nwhere this is true; a headless component is reached through the components\nthat use it, and no grant of its own exposes anything to a person.", + "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..05aca7cb0 100644 --- a/module/services/accounts/proto/saas/accounts/v1/installations.proto +++ b/module/services/accounts/proto/saas/accounts/v1/installations.proto @@ -685,6 +685,14 @@ 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. Access to an + // installation is granted for its pages, so a catalogue offers a grant only + // where this is true; a headless component is reached through the components + // that use it, and no grant of its own exposes anything to a person. + 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..3ee32ce0f 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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d", "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 467ab5af0442797a71bc1521a6f022dbb2e5cc7d..4bf7dc703ce759643c8ac02787a5aed8c9af3fe3 100644 GIT binary patch delta 7773 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 F{~ue>GJ603 delta 6807 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 F=znH?^y>fs 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..d7a7cb575 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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d 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..de4a2d6df 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,19 @@ 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. Access to an + * installation is granted for its pages, so a catalogue offers a grant only + * where this is true; a headless component is reached through the components + * that use it, and no grant of its own exposes anything to a person. + * + * @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..b75f9d138 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,19 @@ 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. Access to an + * installation is granted for its pages, so a catalogue offers a grant only + * where this is true; a headless component is reached through the components + * that use it, and no grant of its own exposes anything to a person. + * + * @generated from field: bool has_pages = 10; + */ + hasPages: boolean; }; /** From 77f07cb4211447d6989287e60a64b48135544dff Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:40:10 -0400 Subject: [PATCH 2/2] fix(accounts,frontend): has_pages says what it reports, and the solution dashboard names module actors too (#1164 review) - The has_pages doc comment claimed a catalogue offers a grant only where it is true; the page keeps access on headless rows because a solution may call them on a person's own session. The comment now says it reports pages only. Regenerated bindings, OpenAPI, contracts and the SDK's vendored contract. - The solution dashboard's recent events carry actorModule through newestEvents, so a module acting on its own reads "Module " there as it does in Recent activity and the audit table. Co-Authored-By: Claude Opus 5.5 --- .../api/accounts/authority/contract.binpb | Bin 1180734 -> 1180837 bytes .../saas/accounts/v1/installations.proto | 10 ++++++---- .../api/accounts/connect/contract.binpb | Bin 1180734 -> 1180837 bytes .../saas/accounts/v1/installations.proto | 10 ++++++---- module/contracts/api/catalog.codefly.json | 4 ++-- module/module.package.codefly.yaml | 4 ++-- .../gen/saas/accounts/v1/installations.pb.go | 10 ++++++---- .../generated/openapi-raw/api.swagger.json | 2 +- .../accounts/openapi/api.swagger.json | 2 +- .../saas/accounts/v1/installations.proto | 10 ++++++---- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 1180734 -> 1180837 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../gen/saas/accounts/v1/installations_pb.ts | 10 ++++++---- .../gen/saas/accounts/v1/installations_pb.ts | 10 ++++++---- .../code/src/solutions/SolutionDashboard.tsx | 8 ++++---- .../solutions/__tests__/metric-info.test.ts | 14 ++++++++++++++ .../code/src/solutions/metric-info.ts | 4 ++++ 18 files changed, 66 insertions(+), 36 deletions(-) diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 4bf7dc703ce759643c8ac02787a5aed8c9af3fe3..592d233fa9b908bef3cd81ec93b227cceccd529d 100644 GIT binary patch delta 972 zcmYjN&rj4q6yDCxblcg&vR!sDWQl_b8k7)^#;7L`c=sO|+JSA-ZJW*z!l@)S0dF3N zBp!|a1IA=E5Ku7u_#^b<&68Jez8SXYVUqdY``-6`?;Y)~ynnf}U0Nk7>m@EpX_t-C zDczEOY_9L&cW)5gFJ813Mz8joujlCZ9~N!+q*l8e-T%=19ax`7^F43Z?tUW3rhhQS zxQ$#N1MJQI8Xfh$zEwO^EEidtD@j?#*M($h5^p_ZoUP|PQGnn(5Il^9lq^c5;&IGX z1oeO?OsXi3*^nzG@+Gz^0=^*yf10j9a1iNE5j+%uL!Xp_U*!Fe1s;A`yp-#}N2hgM}i9LP6Q)Kxog( z|H@P@Q#wJ3zO$td-I%IiUBgb7dK6*bw+~3|8?k7!pmpp6^?*1FhU%jl@$n%+4p1=$ zXwZ|Y3EV^Cwkim+hs5tx(9v-EF>O;E zzwLU9`)_Z0Pm5#Xw5tJdI409w+Jd!wWI1}EcS11rsbdN|Ze7=APDp+1Yk_YajJ^Qt z{}M=jCDuW5@BpZk`ad$)QRfH_0UMhv+ z1Bmw`E)66m(zGw3SH6H(UVCQ9s<6ZIJLiA?=i|eh!^O$r>&(tj=43b{nVXd|FY`0{ zx!gN-=I_zj+g4D|$EW3w`}Fq@o0b@{tgHOtr}D41o#(AaaN^d#5aiN#m||Q-KEr^G z>yv!Z44U@%dAlu=gr|y!lJdSx(r_>c)4r0tpYUEB%2afCSH--aCVUw7L~_6rQ1f;O zQq>y?uDV?jClE4nuH@jD@}o$^f~OIU1o^b_9LW zC|09h1bjVBw44eoP-N;Ed^HgkcNXWwrIFUND@RSOvy z&&h6`R$v>3Y)|VNOM)q*o(W80BZ%!SiL+^ELpulT7ohq-1Xf=O4mQpR6sv#hAw$=6 zatyu*_FECZSO`JwiZu4j)oRSWAlB99YBf+^5but$tsx6Dye5d6s{tCiUnE_Pdj3EiNm(xirfVTy28(sz8PCjW>Z~goO DrJbyy 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 05aca7cb0..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 @@ -688,10 +688,12 @@ message AvailableSolution { // 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. Access to an - // installation is granted for its pages, so a catalogue offers a grant only - // where this is true; a headless component is reached through the components - // that use it, and no grant of its own exposes anything to a person. + // 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; } diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 4bf7dc703ce759643c8ac02787a5aed8c9af3fe3..592d233fa9b908bef3cd81ec93b227cceccd529d 100644 GIT binary patch delta 972 zcmYjN&rj4q6yDCxblcg&vR!sDWQl_b8k7)^#;7L`c=sO|+JSA-ZJW*z!l@)S0dF3N zBp!|a1IA=E5Ku7u_#^b<&68Jez8SXYVUqdY``-6`?;Y)~ynnf}U0Nk7>m@EpX_t-C zDczEOY_9L&cW)5gFJ813Mz8joujlCZ9~N!+q*l8e-T%=19ax`7^F43Z?tUW3rhhQS zxQ$#N1MJQI8Xfh$zEwO^EEidtD@j?#*M($h5^p_ZoUP|PQGnn(5Il^9lq^c5;&IGX z1oeO?OsXi3*^nzG@+Gz^0=^*yf10j9a1iNE5j+%uL!Xp_U*!Fe1s;A`yp-#}N2hgM}i9LP6Q)Kxog( z|H@P@Q#wJ3zO$td-I%IiUBgb7dK6*bw+~3|8?k7!pmpp6^?*1FhU%jl@$n%+4p1=$ zXwZ|Y3EV^Cwkim+hs5tx(9v-EF>O;E zzwLU9`)_Z0Pm5#Xw5tJdI409w+Jd!wWI1}EcS11rsbdN|Ze7=APDp+1Yk_YajJ^Qt z{}M=jCDuW5@BpZk`ad$)QRfH_0UMhv+ z1Bmw`E)66m(zGw3SH6H(UVCQ9s<6ZIJLiA?=i|eh!^O$r>&(tj=43b{nVXd|FY`0{ zx!gN-=I_zj+g4D|$EW3w`}Fq@o0b@{tgHOtr}D41o#(AaaN^d#5aiN#m||Q-KEr^G z>yv!Z44U@%dAlu=gr|y!lJdSx(r_>c)4r0tpYUEB%2afCSH--aCVUw7L~_6rQ1f;O zQq>y?uDV?jClE4nuH@jD@}o$^f~OIU1o^b_9LW zC|09h1bjVBw44eoP-N;Ed^HgkcNXWwrIFUND@RSOvy z&&h6`R$v>3Y)|VNOM)q*o(W80BZ%!SiL+^ELpulT7ohq-1Xf=O4mQpR6sv#hAw$=6 zatyu*_FECZSO`JwiZu4j)oRSWAlB99YBf+^5but$tsx6Dye5d6s{tCiUnE_Pdj3EiNm(xirfVTy28(sz8PCjW>Z~goO DrJbyy 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 05aca7cb0..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 @@ -688,10 +688,12 @@ message AvailableSolution { // 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. Access to an - // installation is granted for its pages, so a catalogue offers a grant only - // where this is true; a headless component is reached through the components - // that use it, and no grant of its own exposes anything to a person. + // 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; } diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index db985e455..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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d", + "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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d", + "digest": "sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f", "services": [ { "name": "APIKeyService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index f497ab7fd..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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d + digest: sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d + digest: sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f - name: auth-gateway endpoints: [grpc, rest] api-contracts: 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 9e9c5ffaf..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 @@ -1427,10 +1427,12 @@ type AvailableSolution struct { // 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. Access to an - // installation is granted for its pages, so a catalogue offers a grant only - // where this is true; a headless component is reached through the components - // that use it, and no grant of its own exposes anything to a person. + // 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 diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 54a5a4322..1e6e6017c 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -7389,7 +7389,7 @@ }, "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. Access to an\ninstallation is granted for its pages, so a catalogue offers a grant only\nwhere this is true; a headless component is reached through the components\nthat use it, and no grant of its own exposes anything to a person." + "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 acfb9e14b..c8b17b1a7 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -874,7 +874,7 @@ "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. Access to an\ninstallation is granted for its pages, so a catalogue offers a grant only\nwhere this is true; a headless component is reached through the components\nthat use it, and no grant of its own exposes anything to a person.", + "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": { diff --git a/module/services/accounts/proto/saas/accounts/v1/installations.proto b/module/services/accounts/proto/saas/accounts/v1/installations.proto index 05aca7cb0..8f272b8a9 100644 --- a/module/services/accounts/proto/saas/accounts/v1/installations.proto +++ b/module/services/accounts/proto/saas/accounts/v1/installations.proto @@ -688,10 +688,12 @@ message AvailableSolution { // 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. Access to an - // installation is granted for its pages, so a catalogue offers a grant only - // where this is true; a headless component is reached through the components - // that use it, and no grant of its own exposes anything to a person. + // 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; } 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 3ee32ce0f..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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d", + "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 4bf7dc703ce759643c8ac02787a5aed8c9af3fe3..592d233fa9b908bef3cd81ec93b227cceccd529d 100644 GIT binary patch delta 972 zcmYjN&rj4q6yDCxblcg&vR!sDWQl_b8k7)^#;7L`c=sO|+JSA-ZJW*z!l@)S0dF3N zBp!|a1IA=E5Ku7u_#^b<&68Jez8SXYVUqdY``-6`?;Y)~ynnf}U0Nk7>m@EpX_t-C zDczEOY_9L&cW)5gFJ813Mz8joujlCZ9~N!+q*l8e-T%=19ax`7^F43Z?tUW3rhhQS zxQ$#N1MJQI8Xfh$zEwO^EEidtD@j?#*M($h5^p_ZoUP|PQGnn(5Il^9lq^c5;&IGX z1oeO?OsXi3*^nzG@+Gz^0=^*yf10j9a1iNE5j+%uL!Xp_U*!Fe1s;A`yp-#}N2hgM}i9LP6Q)Kxog( z|H@P@Q#wJ3zO$td-I%IiUBgb7dK6*bw+~3|8?k7!pmpp6^?*1FhU%jl@$n%+4p1=$ zXwZ|Y3EV^Cwkim+hs5tx(9v-EF>O;E zzwLU9`)_Z0Pm5#Xw5tJdI409w+Jd!wWI1}EcS11rsbdN|Ze7=APDp+1Yk_YajJ^Qt z{}M=jCDuW5@BpZk`ad$)QRfH_0UMhv+ z1Bmw`E)66m(zGw3SH6H(UVCQ9s<6ZIJLiA?=i|eh!^O$r>&(tj=43b{nVXd|FY`0{ zx!gN-=I_zj+g4D|$EW3w`}Fq@o0b@{tgHOtr}D41o#(AaaN^d#5aiN#m||Q-KEr^G z>yv!Z44U@%dAlu=gr|y!lJdSx(r_>c)4r0tpYUEB%2afCSH--aCVUw7L~_6rQ1f;O zQq>y?uDV?jClE4nuH@jD@}o$^f~OIU1o^b_9LW zC|09h1bjVBw44eoP-N;Ed^HgkcNXWwrIFUND@RSOvy z&&h6`R$v>3Y)|VNOM)q*o(W80BZ%!SiL+^ELpulT7ohq-1Xf=O4mQpR6sv#hAw$=6 zatyu*_FECZSO`JwiZu4j)oRSWAlB99YBf+^5but$tsx6Dye5d6s{tCiUnE_Pdj3EiNm(xirfVTy28(sz8PCjW>Z~goO DrJbyy 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 d7a7cb575..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:388768237c56406b6c73ce3630be7e6965735ecfcb3cc1a89d48071ff00d7e8d + contract-digest: sha256:4c2d604cc70c04130c4476b7ab838415942747aa2999af07bee3a8496d6ee50f services: - APIKeyService - AccessibleScopeService 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 de4a2d6df..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 @@ -767,10 +767,12 @@ export type AvailableSolution = Message<"saas.accounts.v1.AvailableSolution"> & * 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. Access to an - * installation is granted for its pages, so a catalogue offers a grant only - * where this is true; a headless component is reached through the components - * that use it, and no grant of its own exposes anything to a person. + * 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; */ 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 b75f9d138..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 @@ -767,10 +767,12 @@ export type AvailableSolution = Message<"saas.accounts.v1.AvailableSolution"> & * 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. Access to an - * installation is granted for its pages, so a catalogue offers a grant only - * where this is true; a headless component is reached through the components - * that use it, and no grant of its own exposes anything to a person. + * 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; */ 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), }); }