@k2b/cloud 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/package.json +13 -5
  2. package/scripts/app-favicon.ts +7 -6
  3. package/scripts/build.ts +3 -0
  4. package/scripts/installable-assets.ts +61 -0
  5. package/scripts/preload.ts +3 -0
  6. package/src/_internal/app-access-path.ts +12 -0
  7. package/src/_internal/app-access-request.ts +13 -0
  8. package/src/_internal/app-access-resources.ts +141 -0
  9. package/src/_internal/app-event-catalog.ts +40 -0
  10. package/src/_internal/app-event-schemas.ts +127 -0
  11. package/src/_internal/application-package.ts +40 -0
  12. package/src/_internal/built-in-app-ids.json +27 -0
  13. package/src/_internal/capabilities.ts +185 -12
  14. package/src/_internal/capability-origin.ts +4 -0
  15. package/src/_internal/capability-streams.ts +10 -5
  16. package/src/_internal/define-app.ts +159 -45
  17. package/src/_internal/installable-assets.ts +90 -0
  18. package/src/_internal/installable-head.ts +22 -0
  19. package/src/_internal/object-store-area.ts +9 -0
  20. package/src/_internal/object-store-config.ts +101 -0
  21. package/src/_internal/object-store-key.ts +14 -0
  22. package/src/_internal/page-responses.ts +34 -3
  23. package/src/_internal/process-identity.ts +7 -2
  24. package/src/_internal/registry-validation.ts +15 -1
  25. package/src/_internal/registry.ts +44 -1
  26. package/src/_internal/runtime-context.ts +9 -1
  27. package/src/_internal/runtime-watcher.ts +8 -0
  28. package/src/_internal/status-preserving-ssr.ts +12 -1
  29. package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
  30. package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
  31. package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
  32. package/src/access/AppAccessEditor.tsx +380 -0
  33. package/src/access/PermissionEditor.browser-harness.tsx +22 -2
  34. package/src/access/PermissionEditor.tsx +386 -77
  35. package/src/access/PrincipalAvatar.tsx +63 -0
  36. package/src/access/PrincipalPicker.tsx +29 -8
  37. package/src/access/ResourceApiKeys.tsx +3 -4
  38. package/src/access/app-access-adapter.ts +51 -0
  39. package/src/access/app-access-messages.ts +172 -0
  40. package/src/access/messages.ts +14 -0
  41. package/src/access/ui.ts +4 -1
  42. package/src/account/Avatar.tsx +21 -3
  43. package/src/account/ProfilePictureDialog.tsx +699 -0
  44. package/src/account/profile-picture-messages.ts +269 -0
  45. package/src/account/ui.ts +8 -1
  46. package/src/ai/admin.ts +3 -0
  47. package/src/ai/app-frame.ts +12 -0
  48. package/src/ai/application-tasks.ts +101 -0
  49. package/src/ai/assistant-models.ts +126 -64
  50. package/src/ai/capabilities.ts +19 -4
  51. package/src/ai/capability-execution.ts +5 -2
  52. package/src/ai/chart-block.ts +63 -30
  53. package/src/ai/chat/chart-block.tsx +12 -3
  54. package/src/ai/chat/composer-adapter.ts +8 -7
  55. package/src/ai/chat/message-actions.tsx +1 -1
  56. package/src/ai/chat/message-utils.ts +2 -2
  57. package/src/ai/chat/presentation.tsx +22 -1
  58. package/src/ai/chat-quotas.ts +5 -4
  59. package/src/ai/chat-tasks.ts +2 -2
  60. package/src/ai/client/controller.ts +11 -10
  61. package/src/ai/client/projection.ts +3 -3
  62. package/src/ai/code-capability-routes.ts +1 -0
  63. package/src/ai/compaction.ts +3 -3
  64. package/src/ai/credentials.ts +13 -114
  65. package/src/ai/default-tool-contracts.ts +4 -3
  66. package/src/ai/dictation-runtime.ts +4 -4
  67. package/src/ai/enrich.ts +2 -2
  68. package/src/ai/executor.ts +38 -27
  69. package/src/ai/http.ts +17 -29
  70. package/src/ai/index.ts +11 -5
  71. package/src/ai/inference-calls.ts +6 -4
  72. package/src/ai/maintenance.ts +1 -1
  73. package/src/ai/memory-learning-runs.ts +5 -5
  74. package/src/ai/memory-learning.ts +3 -3
  75. package/src/ai/message-queue.ts +1 -2
  76. package/src/ai/migrate.ts +75 -42
  77. package/src/ai/model-access-migrate.ts +3 -17
  78. package/src/ai/model-access.ts +44 -85
  79. package/src/ai/model-card.tsx +134 -0
  80. package/src/ai/model-catalog.ts +359 -0
  81. package/src/ai/model-chooser-messages.ts +88 -0
  82. package/src/ai/model-chooser.tsx +577 -0
  83. package/src/ai/model-delete.ts +234 -0
  84. package/src/ai/model-fixture.ts +39 -0
  85. package/src/ai/model-icon.tsx +36 -0
  86. package/src/ai/model-overview.ts +53 -0
  87. package/src/ai/model-usage.ts +45 -0
  88. package/src/ai/openai-api-messages.ts +0 -4
  89. package/src/ai/openai-api-slots.ts +10 -2
  90. package/src/ai/openai-api.ts +83 -16
  91. package/src/ai/personal-agent.ts +1 -1
  92. package/src/ai/prefs.ts +12 -10
  93. package/src/ai/projects-routes.ts +2 -1
  94. package/src/ai/projects.ts +13 -12
  95. package/src/ai/protocol.ts +2 -2
  96. package/src/ai/provider-connections.ts +161 -0
  97. package/src/ai/provider-errors.ts +138 -0
  98. package/src/ai/provider-fetch.ts +4 -0
  99. package/src/ai/provider-retry.ts +1 -1
  100. package/src/ai/provider.ts +2 -1
  101. package/src/ai/quota-limits.ts +36 -0
  102. package/src/ai/quota-provider.ts +11 -2
  103. package/src/ai/quota-report.ts +7 -4
  104. package/src/ai/quotas-migrate.ts +1 -0
  105. package/src/ai/quotas.ts +19 -8
  106. package/src/ai/recent-resources.ts +6 -0
  107. package/src/ai/request-headers.ts +14 -87
  108. package/src/ai/resource-refs.ts +6 -2
  109. package/src/ai/routes.ts +17 -18
  110. package/src/ai/runtime.ts +50 -22
  111. package/src/ai/settings.ts +93 -580
  112. package/src/ai/store.ts +45 -23
  113. package/src/ai/stream.ts +1 -1
  114. package/src/ai/structured-runs.ts +2 -2
  115. package/src/ai/structured.ts +29 -46
  116. package/src/ai/task-contracts.ts +25 -5
  117. package/src/ai/task-execution.ts +15 -6
  118. package/src/ai/task-model.ts +9 -0
  119. package/src/ai/transcription.ts +37 -21
  120. package/src/ai/turn-failure.ts +24 -2
  121. package/src/ai/types.ts +50 -62
  122. package/src/ai/ui.tsx +13 -0
  123. package/src/ai/usage.ts +8 -8
  124. package/src/ai/validate.ts +8 -7
  125. package/src/ai/view-block.ts +2 -2
  126. package/src/api/accounts-entities.ts +40 -16
  127. package/src/api/admin-ai-quotas.ts +34 -56
  128. package/src/api/admin-ai-usage.ts +1 -1
  129. package/src/api/admin-ai.ts +173 -0
  130. package/src/api/admin-core-settings.ts +68 -244
  131. package/src/api/admin-directory-shares.ts +55 -0
  132. package/src/api/admin-identity.ts +81 -2
  133. package/src/api/app-access.ts +85 -0
  134. package/src/api/app-approval.ts +3 -1
  135. package/src/api/app-event-delivery.ts +51 -0
  136. package/src/api/app-events.ts +123 -0
  137. package/src/api/app-resource-access.ts +48 -0
  138. package/src/api/auth/account-mismatch.ts +24 -0
  139. package/src/api/auth.ts +26 -3
  140. package/src/api/capabilities.ts +127 -15
  141. package/src/api/capability-streams.ts +2 -5
  142. package/src/api/identity-child-invocation.ts +145 -0
  143. package/src/api/index.ts +11 -2
  144. package/src/api/mcp.ts +180 -5
  145. package/src/api/me.ts +38 -17
  146. package/src/api/pwa-phone.ts +87 -48
  147. package/src/api/pwa.ts +34 -8
  148. package/src/api/resource-links.ts +83 -6
  149. package/src/api/resource-resolve.ts +50 -0
  150. package/src/api/search/schemas.ts +3 -1
  151. package/src/api/search.ts +9 -7
  152. package/src/api/widgets.ts +1 -0
  153. package/src/app-pwa/authority.ts +75 -0
  154. package/src/app-pwa/contracts.ts +177 -0
  155. package/src/app-pwa/core-query-error.ts +20 -0
  156. package/src/app-pwa/index.ts +23 -0
  157. package/src/app-pwa/protocol.ts +27 -0
  158. package/src/app-pwa/schema.ts +59 -0
  159. package/src/app-pwa/service.ts +861 -0
  160. package/src/app-pwa/transaction.ts +338 -0
  161. package/src/app-pwa/transport-input.ts +57 -0
  162. package/src/app-pwa/transport.ts +235 -0
  163. package/src/approvals/messages.ts +2 -2
  164. package/src/browser/app-access-recovery.ts +44 -0
  165. package/src/browser/app-session.ts +14 -3
  166. package/src/browser/cloud-id.ts +86 -0
  167. package/src/browser/live-websocket.ts +5 -0
  168. package/src/browser/pwa-drafts.ts +57 -0
  169. package/src/browser/resource-clipboard.ts +4 -0
  170. package/src/browser/resource-links.ts +52 -0
  171. package/src/browser/resource-picker.tsx +3 -2
  172. package/src/capabilities/calendar-sources.ts +302 -0
  173. package/src/capabilities/executions.ts +10 -3
  174. package/src/capabilities/migrate.ts +4 -0
  175. package/src/capabilities/nested.ts +43 -0
  176. package/src/capabilities/server-invoke.ts +202 -0
  177. package/src/capabilities/server.ts +26 -187
  178. package/src/capabilities/testing.ts +5 -2
  179. package/src/cli/access.ts +6 -5
  180. package/src/cli/account.ts +57 -0
  181. package/src/cli/admin/agents.ts +66 -0
  182. package/src/cli/admin/ai-models.ts +199 -0
  183. package/src/cli/admin/ai-quotas.ts +6 -102
  184. package/src/cli/admin/ai-usage.ts +4 -4
  185. package/src/cli/admin/app-credentials.ts +5 -3
  186. package/src/cli/admin/index.ts +7 -3
  187. package/src/cli/app-access.ts +60 -0
  188. package/src/cli/apps.ts +89 -26
  189. package/src/cli/avatar.ts +64 -0
  190. package/src/cli/capabilities.ts +22 -1
  191. package/src/cli/events.ts +320 -0
  192. package/src/cli/index.ts +1 -1
  193. package/src/config/env.ts +40 -0
  194. package/src/contracts/app-access.ts +52 -5
  195. package/src/contracts/app-approval.ts +8 -1
  196. package/src/contracts/app-details.ts +89 -0
  197. package/src/contracts/app-events.ts +51 -0
  198. package/src/contracts/app.ts +24 -5
  199. package/src/contracts/calendar-source.ts +274 -0
  200. package/src/contracts/calendar-view.ts +35 -0
  201. package/src/contracts/capabilities.ts +164 -3
  202. package/src/contracts/geo-admin.ts +2 -0
  203. package/src/contracts/geo-route.ts +0 -11
  204. package/src/contracts/geo.ts +14 -2
  205. package/src/contracts/index.ts +6 -0
  206. package/src/contracts/installable.ts +40 -0
  207. package/src/contracts/notification-types.ts +36 -9
  208. package/src/contracts/profile.ts +32 -0
  209. package/src/contracts/pwa-builds.ts +14 -0
  210. package/src/contracts/pwa-location.ts +38 -0
  211. package/src/contracts/pwa.ts +24 -3
  212. package/src/contracts/registry.ts +8 -0
  213. package/src/contracts/resource-clipboard.ts +35 -0
  214. package/src/contracts/resource-pages.ts +140 -0
  215. package/src/contracts/resource-resolution.ts +13 -0
  216. package/src/contracts/shared.ts +4 -0
  217. package/src/contracts/user-notifications.ts +2 -0
  218. package/src/events/app-event-delivery.ts +100 -0
  219. package/src/events/app-events.testing.ts +65 -0
  220. package/src/events/app-events.ts +134 -0
  221. package/src/events/core-user-created.ts +24 -0
  222. package/src/events/index.ts +9 -0
  223. package/src/events/testing.ts +2 -0
  224. package/src/index.ts +2 -1
  225. package/src/server/api/respond.ts +8 -3
  226. package/src/server/index.ts +1 -0
  227. package/src/server/middleware/app-access.ts +7 -6
  228. package/src/server/middleware/auth.ts +168 -66
  229. package/src/server/middleware/index.ts +1 -0
  230. package/src/server/middleware/invocation.ts +4 -2
  231. package/src/server/middleware/workload.ts +3 -3
  232. package/src/server/oauth-scope.ts +10 -0
  233. package/src/server/public-storage.ts +106 -0
  234. package/src/server/services/access.ts +8 -0
  235. package/src/services/accounts/app.ts +35 -3
  236. package/src/services/accounts/avatar.ts +342 -35
  237. package/src/services/accounts/base-group.ts +4 -5
  238. package/src/services/accounts/directory-shares.ts +157 -0
  239. package/src/services/accounts/entities.ts +50 -6
  240. package/src/services/accounts/groups.ts +3 -1
  241. package/src/services/accounts/identities.ts +64 -12
  242. package/src/services/accounts/local-groups.ts +1 -1
  243. package/src/services/accounts/users.ts +23 -15
  244. package/src/services/app-access-management.ts +519 -0
  245. package/src/services/app-access.ts +25 -7
  246. package/src/services/app-approval.ts +15 -4
  247. package/src/services/app-details.ts +224 -0
  248. package/src/services/app-discovery.ts +2 -1
  249. package/src/services/app-event-shapes.ts +139 -0
  250. package/src/services/app-events/accept.ts +116 -0
  251. package/src/services/app-events/admin.ts +250 -0
  252. package/src/services/app-events/declarations.ts +187 -0
  253. package/src/services/app-events/deliver.ts +218 -0
  254. package/src/services/app-events.ts +92 -0
  255. package/src/services/config-conflict.ts +11 -0
  256. package/src/services/core-user-events.ts +1 -0
  257. package/src/services/freeipa-config.ts +9 -0
  258. package/src/services/geo/admin.ts +4 -1
  259. package/src/services/geo/ledger.ts +47 -14
  260. package/src/services/geo/route-chain.ts +40 -18
  261. package/src/services/geo/route-estimate.ts +19 -0
  262. package/src/services/geo/route-plan.ts +0 -2
  263. package/src/services/geo/route-service.ts +57 -39
  264. package/src/services/geo/service.ts +51 -25
  265. package/src/services/identity/core-origin.ts +12 -0
  266. package/src/services/identity/index.ts +2 -1
  267. package/src/services/identity/invocation-actor.ts +50 -2
  268. package/src/services/identity/invocation-authority.ts +38 -1
  269. package/src/services/identity/invocation-token.ts +255 -50
  270. package/src/services/identity/issue-invocation.ts +9 -1
  271. package/src/services/identity/models-only-auth.ts +13 -4
  272. package/src/services/identity/nested-invocation.ts +128 -0
  273. package/src/services/image-metadata.ts +65 -1
  274. package/src/services/index.ts +24 -1
  275. package/src/services/ipa/sync.ts +6 -2
  276. package/src/services/ipa/users.ts +4 -1
  277. package/src/services/mandates/policy.ts +19 -12
  278. package/src/services/notifications/browser.ts +4 -0
  279. package/src/services/notifications/catalog.ts +5 -2
  280. package/src/services/notifications/platform.ts +7 -2
  281. package/src/services/notifications/user.ts +18 -7
  282. package/src/services/oauth-tokens.ts +28 -2
  283. package/src/services/object-store.ts +217 -0
  284. package/src/services/providers/local/users.ts +5 -0
  285. package/src/services/pwa-devices.ts +147 -37
  286. package/src/services/service-account-credentials.ts +5 -2
  287. package/src/services/session/index.ts +14 -0
  288. package/src/services/settings/core-settings.ts +91 -47
  289. package/src/services/settings/index.ts +8 -0
  290. package/src/services/settings/versions.ts +26 -0
  291. package/src/shared/ai-models.ts +274 -0
  292. package/src/shared/ai-quotas.ts +2 -2
  293. package/src/shared/ai-usage.ts +2 -2
  294. package/src/shared/index.ts +2 -0
  295. package/src/shared-text/documents.ts +25 -8
  296. package/src/shared-text/log.ts +1 -1
  297. package/src/shared-text/text-diff.ts +75 -0
  298. package/src/ssr/AdminArea.tsx +131 -0
  299. package/src/ssr/AdminLayout.tsx +11 -3
  300. package/src/ssr/AdminSidebar.tsx +7 -7
  301. package/src/ssr/AppAccessDenied.tsx +114 -0
  302. package/src/ssr/AppBadge.tsx +14 -0
  303. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  304. package/src/ssr/AppPwaRuntime.island.tsx +82 -0
  305. package/src/ssr/CloudShell.island.tsx +25 -4
  306. package/src/ssr/Layout.tsx +15 -7
  307. package/src/ssr/PwaLayout.tsx +73 -8
  308. package/src/ssr/PwaPartCount.island.tsx +23 -0
  309. package/src/ssr/PwaRuntime.island.tsx +85 -6
  310. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  311. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  312. package/src/ssr/ShellRuntime.island.tsx +5 -0
  313. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  314. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
  315. package/src/ssr/admin-active-link.ts +16 -0
  316. package/src/ssr/admin-navigation.ts +26 -20
  317. package/src/ssr/app-badges.ts +31 -7
  318. package/src/ssr/app-pwa-document.tsx +16 -0
  319. package/src/ssr/app-pwa-messages.ts +17 -0
  320. package/src/ssr/index.ts +8 -0
  321. package/src/ssr/layout-context.ts +1 -1
  322. package/src/ssr/page-trail-runtime.ts +3 -1
  323. package/src/ssr/platform-messages.ts +9 -1
  324. package/src/ssr/pwa-messages.ts +10 -2
  325. package/src/ssr/rail-navigation.ts +5 -1
  326. package/src/ssr/settings-draft.ts +153 -0
  327. package/src/ssr/shell-data.tsx +2 -0
  328. package/src/styles/global.css +1 -0
  329. package/src/styles/profile-picture.css +270 -0
  330. package/src/styles/utilities-navigation.css +6 -0
  331. package/src/workflows/ai/policy.ts +49 -0
  332. package/src/workflows/ai/runtime.ts +131 -21
  333. package/src/workflows/ai/store.ts +283 -35
  334. package/src/workflows/ai/types.ts +8 -2
  335. package/src/workflows/ai-actions.ts +148 -120
  336. package/src/workflows/authoring.ts +31 -0
  337. package/src/workflows/contracts.ts +1 -0
  338. package/src/workflows/definition.ts +2 -0
  339. package/src/workflows/editor.ts +15 -0
  340. package/src/workflows/index.ts +2 -1
  341. package/src/workflows/language/compiler.ts +34 -3
  342. package/src/workflows/runtime/executor.ts +132 -33
  343. package/src/workflows/runtime/index.ts +2 -0
  344. package/src/workflows/runtime/ports.ts +5 -1
  345. package/src/workflows/store/actions.ts +1 -1
  346. package/src/workflows/store/definitions.ts +5 -0
  347. package/src/workflows/store/runs.ts +5 -3
  348. package/src/workflows/store/worker.ts +7 -2
  349. package/src/ai/model-pricing.ts +0 -43
  350. package/src/ai/model-request-settings.ts +0 -98
  351. package/src/ai/settings-messages.ts +0 -129
  352. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -0,0 +1,63 @@
1
+ import { Show } from "solid-js";
2
+ import { CloudAvatar } from "../account/Avatar";
3
+ import type { AccessEntry } from "../contracts/shared";
4
+
5
+ // One hue per principal kind from the avatar palette, so a group, a service account and all signed-in people
6
+ // read apart at a glance. People use a neutral background.
7
+ const hues = { group: "#22c55e", service_account: "#8b5cf6", authenticated: "#3b82f6" } as const;
8
+
9
+ /** The 32px avatar of an access entry in the expanded editor and on the denial page. */
10
+ export default function PrincipalAvatar(props: {
11
+ entry: Pick<AccessEntry, "principal" | "avatarHash" | "serviceAccountKind">;
12
+ label: string;
13
+ }) {
14
+ const icon = () => {
15
+ switch (props.entry.principal.type) {
16
+ case "group":
17
+ return "ti-users-group";
18
+ case "service_account":
19
+ return "ti-robot";
20
+ case "authenticated":
21
+ return "ti-world";
22
+ default:
23
+ return "ti-world";
24
+ }
25
+ };
26
+ const hue = () => {
27
+ const type = props.entry.principal.type;
28
+ return type === "group" || type === "service_account" || type === "authenticated" ? hues[type] : undefined;
29
+ };
30
+ return (
31
+ <Show
32
+ when={props.entry.principal.type === "user" ? props.entry.principal : undefined}
33
+ fallback={
34
+ <span
35
+ class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--ui-surface-muted)] text-secondary"
36
+ // The avatar formula: a light share of the hue on the surface, a dark share on the text, in both themes.
37
+ style={
38
+ hue()
39
+ ? {
40
+ background: `color-mix(in srgb, ${hue()} 18%, var(--k2b-surface))`,
41
+ color: `color-mix(in srgb, ${hue()} 55%, var(--k2b-text))`,
42
+ }
43
+ : undefined
44
+ }
45
+ aria-hidden="true"
46
+ >
47
+ <i class={`ti ${icon()} text-xs`} />
48
+ </span>
49
+ }
50
+ >
51
+ {(user) => (
52
+ <CloudAvatar
53
+ username={props.label}
54
+ userId={user().userId}
55
+ avatarHash={props.entry.avatarHash}
56
+ size="sm"
57
+ class="h-8 w-8"
58
+ style={{ background: "var(--k2b-surface-muted)", color: "var(--k2b-text-secondary)" }}
59
+ />
60
+ )}
61
+ </Show>
62
+ );
63
+ }
@@ -1,5 +1,5 @@
1
1
  import { Combobox, type ComboboxOption, useLocale } from "@k2b/ui";
2
- import type { Principal, ServiceAccountKind } from "../contracts/shared";
2
+ import type { Principal, ServiceAccountKind, ServiceAccountStatus } from "../contracts/shared";
3
3
  import { accessMessages } from "./messages";
4
4
  import { principalLabel } from "./principal-label";
5
5
  import { serviceAccountKindDisplay } from "./service-account-kind";
@@ -24,16 +24,29 @@ type Entity =
24
24
  appId: string | null;
25
25
  resourceType: string | null;
26
26
  resourceId: string | null;
27
+ status: "active" | "disabled";
27
28
  };
28
29
  };
30
+ /** The service-account facts a consumer can check before offering an account. */
31
+ export type PrincipalPickerServiceAccount = Extract<Entity, { kind: "service_account" }>["serviceAccount"];
29
32
  /** Shared identity selector; the consumer owns the meaning of the assignment. */
30
33
  export default function PrincipalPicker(props: {
31
34
  existing?: Principal[];
32
35
  allowPublic?: boolean;
33
36
  allowAuthenticated?: boolean;
37
+ /** Defaults to true; disable only for a documented user-backed workflow. */
34
38
  allowServiceAccounts?: boolean;
39
+ /** Optional directory filters for the resource's accepted credential kinds. */
40
+ serviceAccountKinds?: ServiceAccountKind[];
41
+ serviceAccountStatus?: ServiceAccountStatus;
42
+ /** Offers only the service accounts this returns `true` for, such as active accounts the resource can accept. */
43
+ serviceAccountFilter?: (account: PrincipalPickerServiceAccount) => boolean;
35
44
  disabled?: boolean;
36
45
  readOnly?: boolean;
46
+ /** Replaces the default placeholder, which lists the principal kinds on offer. */
47
+ placeholder?: string;
48
+ /** The consumer's name and description for every signed-in person. */
49
+ signedIn?: { label: string; description: string };
37
50
  /** `serviceAccountKind` is set for service accounts so a new row can show the same kind as the result. */
38
51
  onSelect: (
39
52
  principal: Principal,
@@ -56,8 +69,9 @@ export default function PrincipalPicker(props: {
56
69
  if (props.allowAuthenticated !== false)
57
70
  entries.push({
58
71
  principal: { type: "authenticated" },
59
- label: principalLabel({ principal: { type: "authenticated" } }, locale()),
72
+ label: props.signedIn?.label ?? principalLabel({ principal: { type: "authenticated" } }, locale()),
60
73
  icon: "ti ti-lock-open-2",
74
+ description: props.signedIn?.description,
61
75
  });
62
76
  if (props.allowPublic)
63
77
  entries.push({
@@ -68,7 +82,13 @@ export default function PrincipalPicker(props: {
68
82
  if (search.length >= 2) {
69
83
  const url = new URL("/api/accounts/entities", window.location.origin);
70
84
  url.searchParams.set("search", search);
71
- url.searchParams.set("kinds", props.allowServiceAccounts ? "user,group,service_account" : "user,group");
85
+ url.searchParams.set("kinds", props.allowServiceAccounts !== false ? "user,group,service_account" : "user,group");
86
+ if (props.allowServiceAccounts !== false && props.serviceAccountKinds?.length) {
87
+ url.searchParams.set("service_account_kinds", props.serviceAccountKinds.join(","));
88
+ }
89
+ if (props.allowServiceAccounts !== false && props.serviceAccountStatus) {
90
+ url.searchParams.set("service_account_status", props.serviceAccountStatus);
91
+ }
72
92
  url.searchParams.set("per_page", "10");
73
93
  const users = props.existing?.flatMap((p) => (p.type === "user" ? [p.userId] : [])) ?? [];
74
94
  const groups = props.existing?.flatMap((p) => (p.type === "group" ? [p.groupId] : [])) ?? [];
@@ -98,7 +118,7 @@ export default function PrincipalPicker(props: {
98
118
  icon: "ti ti-users-group",
99
119
  description: e.group.description ?? undefined,
100
120
  });
101
- else {
121
+ else if (!props.serviceAccountFilter || props.serviceAccountFilter(e.serviceAccount)) {
102
122
  const account = e.serviceAccount;
103
123
  const kind = serviceAccountKindDisplay(account.kind, t());
104
124
  entries.push({
@@ -133,13 +153,14 @@ export default function PrincipalPicker(props: {
133
153
  disabled={props.disabled}
134
154
  readOnly={props.readOnly}
135
155
  placeholder={
136
- props.allowAuthenticated !== false || props.allowPublic
137
- ? props.allowServiceAccounts
156
+ props.placeholder ??
157
+ (props.allowAuthenticated !== false || props.allowPublic
158
+ ? props.allowServiceAccounts !== false
138
159
  ? t().addAll
139
160
  : t().addAudience
140
- : props.allowServiceAccounts
161
+ : props.allowServiceAccounts !== false
141
162
  ? t().addService
142
- : t().addBasic
163
+ : t().addBasic)
143
164
  }
144
165
  fetchData={load}
145
166
  onSelect={(option) => {
@@ -217,12 +217,11 @@ export default function ResourceApiKeys(props: ResourceApiKeysProps) {
217
217
  if (confirmed) await revokeMutation.mutate({ id: key.id, name: key.name });
218
218
  };
219
219
 
220
- // The heading is a detail panel section heading: in a detail panel the keys line up with the sections around them,
221
- // and below a settings group title they read as one level down.
220
+ // The heading is a detail panel section heading without an icon: in a details panel the keys line up with the
221
+ // sections around them, and below a settings group title they read as one level down. Keys are flat rows.
222
222
  return (
223
223
  <DetailPanel.Section
224
224
  title={props.title ?? t().apiKeys}
225
- icon="ti ti-key"
226
225
  description={props.description ?? t().apiKeysDescription}
227
226
  actions={
228
227
  <Button type="button" variant="secondary" size="sm" onClick={openCreate} disabled={createMutation.loading()}>
@@ -235,7 +234,7 @@ export default function ResourceApiKeys(props: ResourceApiKeysProps) {
235
234
  <div class="flex flex-col gap-2">
236
235
  <For each={keys()}>
237
236
  {(key) => (
238
- <div class="group/api-key flex items-center gap-3 rounded-lg bg-zinc-50/70 p-3 dark:bg-zinc-900/35">
237
+ <div class="group/api-key flex items-center gap-3 py-1.5">
239
238
  <div class="min-w-0 flex-1">
240
239
  {/* The name and its tags wrap instead of squeezing the name to a few letters in a narrow section. */}
241
240
  <div class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
@@ -0,0 +1,51 @@
1
+ import { hc } from "hono/client";
2
+ import { z } from "zod";
3
+ import type { AppAccessApi } from "../api/app-access";
4
+ import { AppAccessSnapshotSchema } from "../contracts/app-access";
5
+ import { AccessEntrySchema } from "../contracts/shared";
6
+ import type { AppAccessChange, AppAccessSnapshot } from "../services/app-access-management";
7
+
8
+ export class AppAccessClientError extends Error {
9
+ constructor(
10
+ readonly code: string,
11
+ message: string,
12
+ readonly status: number,
13
+ ) {
14
+ super(message);
15
+ }
16
+ }
17
+
18
+ /** Same audited route for an app's own settings and explicit administration repair. */
19
+ export const appAccessAdapter = (appId: string, options: { repair?: boolean } = {}) => {
20
+ const client = hc<AppAccessApi>("/_cloud/app-access");
21
+ const param = { appId };
22
+ const query = options.repair ? { repair: "1" as const } : {};
23
+ return {
24
+ async read(signal?: AbortSignal): Promise<AppAccessSnapshot> {
25
+ const response = await client[":appId"].$get({ param, query }, { init: { credentials: "same-origin", signal } });
26
+ const data = await response.json();
27
+ if (response.ok) return AppAccessSnapshotSchema.parse(data);
28
+ throw new AppAccessClientError(
29
+ typeof data === "object" && data !== null && "code" in data && typeof data.code === "string" ? data.code : "APP_ACCESS_READ_FAILED",
30
+ typeof data === "object" && data !== null && "message" in data && typeof data.message === "string"
31
+ ? data.message
32
+ : "The access list could not be loaded.",
33
+ response.status,
34
+ );
35
+ },
36
+ async change(change: AppAccessChange, signal?: AbortSignal) {
37
+ const response = await client[":appId"].$post({ param, query, json: change }, { init: { credentials: "same-origin", signal } });
38
+ const data = await response.json();
39
+ if (response.ok) return z.object({ entry: AccessEntrySchema.optional(), canUse: z.boolean() }).parse(data);
40
+ throw new AppAccessClientError(
41
+ typeof data === "object" && data !== null && "code" in data && typeof data.code === "string"
42
+ ? data.code
43
+ : "APP_ACCESS_CHANGE_FAILED",
44
+ typeof data === "object" && data !== null && "message" in data && typeof data.message === "string"
45
+ ? data.message
46
+ : "The access change could not be confirmed.",
47
+ response.status,
48
+ );
49
+ },
50
+ };
51
+ };
@@ -0,0 +1,172 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+
3
+ export const appAccessMessages = i18n.define({
4
+ baseLocale: "en",
5
+ messages: {
6
+ en: {
7
+ access: "Access",
8
+ apps: "Apps",
9
+ app: "App",
10
+ use: "Use",
11
+ manage: "Manage access",
12
+ retry: "Try again",
13
+ reload: "Reload",
14
+ accessChanged: "Your app access changed. Reload to check your access.",
15
+ whoUses: ({ name }: { name: string }) => `Who uses ${name}`,
16
+ description: ({ name }: { name: string }) =>
17
+ `Only these people, groups and service accounts see ${name} in the launcher, search and Assistant.`,
18
+ useDescription: "Open the app and work with it. Permissions within the app control what each person sees.",
19
+ manageDescription: "Use the app and change this list.",
20
+ repairNotice: ({ name }: { name: string }) =>
21
+ `You are editing access as a Cloud administrator. This does not give you use of ${name}. Every change is recorded in the audit log.`,
22
+ noManagerLead: "No one manages access.",
23
+ noManager: "Assign at least one person Manage access.",
24
+ inherited: ({ name }: { name: string }) =>
25
+ `Until now, all signed-in people could use ${name}. That stays the same until you change it.`,
26
+ loadFailedLead: "The list could not be loaded.",
27
+ loadFailed: "No changes were made.",
28
+ saveFailed: "Not saved.",
29
+ listChanged: "This grant has changed. Reload the list before trying again.",
30
+ alreadyGranted: "This entry already has access. Reload the list to see the current access.",
31
+ uncertain: "The change could not be confirmed. Reload the list before trying again.",
32
+ savedRefreshFailed: "Saved. The list could not be refreshed. Reload before making another change.",
33
+ lastManager: "At least one person must manage access. Add another manager first.",
34
+ forbidden: "You are not allowed to manage this list.",
35
+ removeSelfTitle: "Remove your own access?",
36
+ changeSelfTitle: "Change your own access?",
37
+ removeAudienceTitle: "Remove access for all signed-in people?",
38
+ removeAudience: ({ name }: { name: string }) => `People without another grant can no longer use ${name}.`,
39
+ changeSelfEntry: ({ name }: { name: string }) => `This grant for ${name} changes to Use. Your other grants still apply.`,
40
+ removeSelf: ({ name }: { name: string }) => `Afterwards, you cannot open ${name} or change this list.`,
41
+ removeSelfManagement: ({ name }: { name: string }) => `You can still use ${name}, but can no longer change this list.`,
42
+ removeSelfEntry: ({ name }: { name: string }) => `This removes this grant for ${name}. Your other grants still apply.`,
43
+ remainingManagers: ({ managers, count }: { managers: string; count: number }) =>
44
+ count === 0 ? "" : count === 1 ? `${managers} still manages access.` : `${managers} still manage access.`,
45
+ closeNotice: "Close notice",
46
+ cancel: "Cancel",
47
+ remove: "Remove",
48
+ change: "Change",
49
+ add: "Add a person, group or service account",
50
+ signedIn: "All signed-in people",
51
+ signedInDescription: "Everyone with an account in this Cloud",
52
+ group: ({ count }: { count: number }) =>
53
+ `Group · ${count === 0 ? "no people" : count === 1 ? "1 person" : `${count.toLocaleString("en")} people`}`,
54
+ fixedSignedIn: "All signed-in people. The app sets this rule; there is nothing to configure here.",
55
+ fixedNoGuests: "Signed-in people except guests. The app sets this rule; there is nothing to configure here.",
56
+ fixedAdmins: "Cloud administrators only. The app sets this rule; there is nothing to configure here.",
57
+ fixedRoles: "People with the Cloud roles required by the app. The app sets this rule.",
58
+ resources: "Accounts with an effective resource permission in this app. Manage permissions within the app.",
59
+ resourcesLabel: "Resource permissions",
60
+ legacy: "This app uses its existing access checks.",
61
+ deniedTitle: ({ name }: { name: string }) => `${name} is not available to you`,
62
+ revokedTitle: ({ name }: { name: string }) => `Your access to ${name} was removed`,
63
+ revokedDescription: "You had access until now. If this was a mistake, ask:",
64
+ deniedGrants: "Only people with access may use this app. Access is managed by:",
65
+ deniedFixed: "If you need this app, contact your Cloud administration.",
66
+ deniedResources: "Ask someone who manages resource permissions in this app for access.",
67
+ deniedAdmins: ({ name }: { name: string }) => `Only Cloud administrators use ${name}.`,
68
+ deniedNoGuests: ({ name }: { name: string }) => `Guests cannot use ${name}.`,
69
+ deniedRoles: ({ name }: { name: string }) => `Only people with specific Cloud roles use ${name}.`,
70
+ askAdministration: "Cloud administration",
71
+ home: "Go to home",
72
+ adminHintBefore: "As a Cloud administrator, you can repair access under ",
73
+ adminHintLink: "Administration → Apps",
74
+ adminHintAfter: ". You need your own grant to use the app.",
75
+ repair: "Repair access",
76
+ repairShort: "Repair",
77
+ backToApps: "Back to Apps",
78
+ configurable: "Configured in the app",
79
+ rule: "Who uses the app",
80
+ overview: "Each app decides who may use it. Review its rule here and repair configurable access when no one manages it.",
81
+ fixedSignedInLabel: "Fixed: all signed-in people",
82
+ fixedNoGuestsLabel: "Fixed: no guests",
83
+ fixedAdminsLabel: "Fixed: Cloud administrators only",
84
+ fixedRolesLabel: "Fixed: Cloud roles",
85
+ legacyLabel: "Existing access checks",
86
+ count: ({ count }: { count: number }) => (count === 1 ? "1 grant" : `${count} grants`),
87
+ },
88
+ de: {
89
+ access: "Zugriff",
90
+ apps: "Apps",
91
+ app: "App",
92
+ use: "Nutzen",
93
+ manage: "Zugriff verwalten",
94
+ retry: "Erneut versuchen",
95
+ reload: "Erneut laden",
96
+ accessChanged: "Dein App-Zugriff hat sich geändert. Lade die Seite neu, um deinen Zugriff zu prüfen.",
97
+ whoUses: ({ name }: { name: string }) => `Wer ${name} nutzt`,
98
+ description: ({ name }: { name: string }) =>
99
+ `Nur diese Personen, Gruppen und Dienstkonten sehen ${name} im Start-Menü, in der Suche und beim Assistenten.`,
100
+ useDescription: "App öffnen und mit ihr arbeiten. Was jemand darin sieht, regeln die Rechte der App.",
101
+ manageDescription: "Nutzen und diese Liste ändern.",
102
+ repairNotice: ({ name }: { name: string }) =>
103
+ `Du bearbeitest den Zugriff als Cloud-Admin. Nutzen kannst du ${name} damit nicht. Jede Änderung steht im Protokoll.`,
104
+ noManagerLead: "Niemand verwaltet den Zugriff.",
105
+ noManager: "Lege mindestens eine Person mit „Zugriff verwalten“ fest.",
106
+ inherited: ({ name }: { name: string }) => `Bisher nutzten alle angemeldeten Personen ${name}. Das bleibt so, bis du es änderst.`,
107
+ loadFailedLead: "Die Liste lädt gerade nicht.",
108
+ loadFailed: "Es hat sich nichts geändert.",
109
+ saveFailed: "Nicht gespeichert.",
110
+ listChanged: "Diese Freigabe wurde inzwischen geändert. Lade die Liste vor einem neuen Versuch erneut.",
111
+ alreadyGranted: "Dieser Eintrag hat schon Zugriff. Lade die Liste erneut, um den aktuellen Zugriff zu sehen.",
112
+ uncertain: "Die Änderung konnte nicht bestätigt werden. Lade die Liste vor einem neuen Versuch erneut.",
113
+ savedRefreshFailed: "Gespeichert. Die Liste konnte nicht aktualisiert werden. Lade sie vor einer weiteren Änderung erneut.",
114
+ lastManager: "Mindestens eine Person muss den Zugriff verwalten. Füge zuerst eine weitere hinzu.",
115
+ forbidden: "Du darfst diese Liste nicht verwalten.",
116
+ removeSelfTitle: "Eigenen Zugriff entfernen?",
117
+ changeSelfTitle: "Eigenen Zugriff ändern?",
118
+ removeAudienceTitle: "Zugriff für alle angemeldeten Personen entfernen?",
119
+ removeAudience: ({ name }: { name: string }) => `Personen ohne weitere Freigabe können ${name} danach nicht mehr nutzen.`,
120
+ changeSelfEntry: ({ name }: { name: string }) =>
121
+ `Diese Freigabe für ${name} wird auf Nutzen geändert. Deine anderen Freigaben gelten weiter.`,
122
+ removeSelf: ({ name }: { name: string }) => `Danach kannst du ${name} nicht mehr öffnen und diese Liste nicht mehr ändern.`,
123
+ removeSelfManagement: ({ name }: { name: string }) => `Du kannst ${name} weiter nutzen, aber diese Liste nicht mehr ändern.`,
124
+ removeSelfEntry: ({ name }: { name: string }) => `Diese Freigabe für ${name} wird entfernt. Deine anderen Freigaben gelten weiter.`,
125
+ remainingManagers: ({ managers, count }: { managers: string; count: number }) =>
126
+ count === 0 ? "" : count === 1 ? `${managers} verwaltet den Zugriff weiter.` : `${managers} verwalten den Zugriff weiter.`,
127
+ closeNotice: "Hinweis schließen",
128
+ cancel: "Abbrechen",
129
+ remove: "Entfernen",
130
+ change: "Ändern",
131
+ add: "Person, Gruppe oder Dienstkonto hinzufügen",
132
+ signedIn: "Alle angemeldeten Personen",
133
+ signedInDescription: "Jede Person mit Konto in dieser Cloud",
134
+ group: ({ count }: { count: number }) =>
135
+ `Gruppe · ${count === 0 ? "keine Personen" : count === 1 ? "1 Person" : `${count.toLocaleString("de")} Personen`}`,
136
+ fixedSignedIn: "Alle angemeldeten Personen. Das legt die App fest; hier gibt es dazu nichts einzustellen.",
137
+ fixedNoGuests: "Angemeldete Personen außer Gästen. Das legt die App fest; hier gibt es dazu nichts einzustellen.",
138
+ fixedAdmins: "Nur Cloud-Admins. Das legt die App fest; hier gibt es dazu nichts einzustellen.",
139
+ fixedRoles: "Personen mit den von der App verlangten Cloud-Rollen. Das legt die App fest.",
140
+ resources: "Konten mit einer wirksamen Ressourcenberechtigung in dieser App. Berechtigungen werden innerhalb der App verwaltet.",
141
+ resourcesLabel: "Ressourcenberechtigungen",
142
+ legacy: "Diese App verwendet ihre bisherigen Zugriffsprüfungen.",
143
+ deniedTitle: ({ name }: { name: string }) => `${name} ist für dich nicht freigegeben`,
144
+ revokedTitle: ({ name }: { name: string }) => `Dein Zugriff auf ${name} wurde entfernt`,
145
+ revokedDescription: "Du hattest bis eben Zugriff. Wenn das ein Versehen ist, frag:",
146
+ deniedGrants: "Diese App nutzen nur Personen mit Freigabe. Den Zugriff verwalten:",
147
+ deniedFixed: "Wenn du die App brauchst, wende dich an die Cloud-Verwaltung.",
148
+ deniedResources: "Bitte jemanden, der Ressourcenberechtigungen in dieser App verwaltet, um Zugriff.",
149
+ deniedAdmins: ({ name }: { name: string }) => `${name} nutzen nur Cloud-Admins.`,
150
+ deniedNoGuests: ({ name }: { name: string }) => `${name} nutzen nur angemeldete Personen außer Gästen.`,
151
+ deniedRoles: ({ name }: { name: string }) => `${name} nutzen nur Personen mit bestimmten Cloud-Rollen.`,
152
+ askAdministration: "Cloud-Verwaltung",
153
+ home: "Zur Startseite",
154
+ adminHintBefore: "Als Cloud-Admin kannst du den Zugriff unter ",
155
+ adminHintLink: "Verwaltung → Apps",
156
+ adminHintAfter: " reparieren. Nutzen kannst du die App erst mit einer Freigabe.",
157
+ repair: "Zugriff reparieren",
158
+ repairShort: "Reparieren",
159
+ backToApps: "Zurück zu Apps",
160
+ configurable: "Einstellbar in der App",
161
+ rule: "Wer nutzt die App",
162
+ overview:
163
+ "Jede App legt selbst fest, wer sie nutzt. Hier siehst du die Regeln und reparierst einstellbaren Zugriff, wenn niemand ihn mehr verwaltet.",
164
+ fixedSignedInLabel: "Fest: alle angemeldeten Personen",
165
+ fixedNoGuestsLabel: "Fest: keine Gäste",
166
+ fixedAdminsLabel: "Fest: nur Cloud-Admins",
167
+ fixedRolesLabel: "Fest: Cloud-Rollen",
168
+ legacyLabel: "Bisherige Zugriffsprüfungen",
169
+ count: ({ count }: { count: number }) => (count === 1 ? "1 Freigabe" : `${count} Freigaben`),
170
+ },
171
+ },
172
+ });
@@ -4,6 +4,13 @@ export const accessMessages = i18n.define({
4
4
  baseLocale: "en",
5
5
  messages: {
6
6
  en: {
7
+ newPermission: "Permission for new entry",
8
+ saved: "Saved",
9
+ saving: "Saving",
10
+ you: "you",
11
+ clearSelection: ({ name }: { name: string }) => `Remove selection: ${name}`,
12
+ person: "Person",
13
+ group: "Group",
7
14
  view: "View",
8
15
  edit: "Edit",
9
16
  manage: "Manage",
@@ -87,6 +94,13 @@ export const accessMessages = i18n.define({
87
94
  neverUsed: "Never used",
88
95
  },
89
96
  de: {
97
+ newPermission: "Recht für neuen Eintrag",
98
+ saved: "Gespeichert",
99
+ saving: "Wird gespeichert",
100
+ you: "du",
101
+ clearSelection: ({ name }) => `Auswahl entfernen: ${name}`,
102
+ person: "Person",
103
+ group: "Gruppe",
90
104
  view: "Ansehen",
91
105
  edit: "Bearbeiten",
92
106
  manage: "Verwalten",
package/src/access/ui.ts CHANGED
@@ -6,10 +6,13 @@
6
6
  * from depending on Cloud's broad legacy UI entrypoint.
7
7
  */
8
8
 
9
+ export { type AppAccessEditorProps, default as AppAccessEditor } from "./AppAccessEditor";
10
+ export { appAccessAdapter } from "./app-access-adapter";
11
+ export { appAccessMessages } from "./app-access-messages";
9
12
  export { isManagerEntry, type ManagerCandidate } from "./managers";
10
13
  export type { AllowedLevel, GrantableLevel } from "./PermissionEditor";
11
14
  export { default as PermissionEditor } from "./PermissionEditor";
12
- export { default as PrincipalPicker, principalKey } from "./PrincipalPicker";
15
+ export { default as PrincipalPicker, type PrincipalPickerServiceAccount, principalKey } from "./PrincipalPicker";
13
16
  export { principalLabel } from "./principal-label";
14
17
  export type {
15
18
  ResourceApiKey,
@@ -16,22 +16,40 @@ const avatarSource = (userId?: string | null, avatarHash?: string | null): strin
16
16
  return `/api/accounts/users/${encodeURIComponent(userId)}/avatar?rev=${encodeURIComponent(avatarHash)}`;
17
17
  };
18
18
 
19
- /** The URL of a person's uploaded picture, or undefined when there is none. */
19
+ /** The URL of a person's picture, or undefined while the person shows initials. */
20
20
  export const cloudAvatarSource = avatarSource;
21
21
 
22
+ /**
23
+ * Where an avatar's image comes from. A person has one only with a hash; a service account always has one, its photo
24
+ * or its robot, so it is linked even without a hash and revalidates. A system actor has none.
25
+ */
26
+ export const cloudAvatarImage = (params: {
27
+ kind: CloudAvatarKind;
28
+ userId?: string | null;
29
+ avatarHash?: string | null;
30
+ }): string | undefined => {
31
+ if (params.kind === "user") return avatarSource(params.userId, params.avatarHash);
32
+ if (params.kind === "system" || !params.userId) return undefined;
33
+ const rev = params.avatarHash ? `?rev=${encodeURIComponent(params.avatarHash)}` : "";
34
+ return `/api/accounts/users/${encodeURIComponent(params.userId)}/avatar${rev}`;
35
+ };
36
+
22
37
  const KIND_ICON: Record<CloudAvatarKind, string | undefined> = {
23
38
  user: undefined,
24
39
  service_account: "ti ti-robot",
25
40
  system: "ti ti-settings-automation",
26
41
  };
27
42
 
28
- /** One avatar for every Cloud surface: the uploaded picture when there is one, otherwise initials. */
43
+ /**
44
+ * One avatar for every Cloud surface. A person shows a photo, a built avatar, or initials; a service account shows its
45
+ * photo or robot in a square, with its icon only while the image cannot load.
46
+ */
29
47
  export function CloudAvatar(props: CloudAvatarProps): JSX.Element {
30
48
  const kind = () => props.kind ?? "user";
31
49
  return (
32
50
  <Avatar
33
51
  name={props.username}
34
- src={kind() === "user" ? avatarSource(props.userId, props.avatarHash) : undefined}
52
+ src={cloudAvatarImage({ kind: kind(), userId: props.userId, avatarHash: props.avatarHash })}
35
53
  icon={KIND_ICON[kind()]}
36
54
  shape={kind() === "user" ? "circle" : "square"}
37
55
  alt={props.alt}