@k2b/cloud 0.43.0 → 0.45.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 (308) hide show
  1. package/package.json +11 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/GroupCoverage.tsx +20 -5
  21. package/src/access/PermissionEditor.tsx +20 -5
  22. package/src/access/PrincipalAvatar.tsx +30 -21
  23. package/src/access/PrincipalPicker.tsx +14 -4
  24. package/src/access/ResourceApiKeys.tsx +3 -4
  25. package/src/access/app-access-messages.ts +6 -0
  26. package/src/access/messages.ts +4 -4
  27. package/src/account/EntitySearch.tsx +12 -0
  28. package/src/account/ProfilePictureDialog.tsx +699 -0
  29. package/src/account/profile-picture-messages.ts +269 -0
  30. package/src/account/ui.ts +7 -0
  31. package/src/ai/admin.ts +3 -0
  32. package/src/ai/app-frame.ts +12 -0
  33. package/src/ai/application-tasks.ts +101 -0
  34. package/src/ai/assistant-models.ts +126 -64
  35. package/src/ai/capabilities.ts +19 -4
  36. package/src/ai/chart-block.ts +63 -30
  37. package/src/ai/chat/chart-block.tsx +12 -3
  38. package/src/ai/chat/composer-adapter.ts +8 -7
  39. package/src/ai/chat/message-actions.tsx +1 -1
  40. package/src/ai/chat/message-utils.ts +2 -2
  41. package/src/ai/chat/presentation.tsx +22 -1
  42. package/src/ai/chat-quotas.ts +5 -4
  43. package/src/ai/chat-tasks.ts +2 -2
  44. package/src/ai/client/controller.ts +11 -10
  45. package/src/ai/client/projection.ts +3 -3
  46. package/src/ai/compaction.ts +3 -3
  47. package/src/ai/credentials.ts +13 -114
  48. package/src/ai/default-tool-contracts.ts +4 -3
  49. package/src/ai/dictation-runtime.ts +10 -6
  50. package/src/ai/enrich.ts +2 -2
  51. package/src/ai/executor.ts +40 -25
  52. package/src/ai/http.ts +17 -29
  53. package/src/ai/index.ts +12 -5
  54. package/src/ai/inference-calls.ts +5 -4
  55. package/src/ai/maintenance.ts +1 -1
  56. package/src/ai/memory-learning-runs.ts +5 -5
  57. package/src/ai/memory-learning.ts +3 -3
  58. package/src/ai/message-queue.ts +1 -2
  59. package/src/ai/migrate.ts +75 -42
  60. package/src/ai/model-access-migrate.ts +3 -17
  61. package/src/ai/model-access.ts +44 -85
  62. package/src/ai/model-card.tsx +134 -0
  63. package/src/ai/model-catalog.ts +359 -0
  64. package/src/ai/model-chooser-messages.ts +88 -0
  65. package/src/ai/model-chooser.tsx +577 -0
  66. package/src/ai/model-delete.ts +234 -0
  67. package/src/ai/model-fixture.ts +39 -0
  68. package/src/ai/model-icon.tsx +36 -0
  69. package/src/ai/model-overview.ts +53 -0
  70. package/src/ai/model-usage.ts +45 -0
  71. package/src/ai/openai-api-messages.ts +0 -4
  72. package/src/ai/openai-api.ts +26 -9
  73. package/src/ai/personal-agent.ts +1 -1
  74. package/src/ai/prefs.ts +12 -10
  75. package/src/ai/project-tool.ts +4 -4
  76. package/src/ai/projects-authorization.ts +53 -0
  77. package/src/ai/projects-routes.ts +58 -47
  78. package/src/ai/projects.ts +110 -88
  79. package/src/ai/protocol.ts +2 -2
  80. package/src/ai/provider-connections.ts +161 -0
  81. package/src/ai/provider-errors.ts +55 -6
  82. package/src/ai/provider-fetch.ts +4 -0
  83. package/src/ai/provider.ts +1 -1
  84. package/src/ai/quota-limits.ts +36 -0
  85. package/src/ai/quota-provider.ts +4 -2
  86. package/src/ai/quota-report.ts +7 -4
  87. package/src/ai/quotas.ts +19 -8
  88. package/src/ai/recent-resources.ts +9 -3
  89. package/src/ai/request-headers.ts +14 -87
  90. package/src/ai/resource-refs.ts +6 -2
  91. package/src/ai/routes.ts +47 -48
  92. package/src/ai/runtime.ts +50 -22
  93. package/src/ai/settings.ts +93 -580
  94. package/src/ai/skill-tool.ts +4 -4
  95. package/src/ai/skills-routes.ts +27 -26
  96. package/src/ai/skills.ts +120 -81
  97. package/src/ai/store.ts +45 -23
  98. package/src/ai/stream.ts +1 -1
  99. package/src/ai/structured-runs.ts +2 -2
  100. package/src/ai/structured.ts +21 -44
  101. package/src/ai/task-contracts.ts +25 -5
  102. package/src/ai/task-execution.ts +15 -6
  103. package/src/ai/task-model.ts +9 -0
  104. package/src/ai/transcription.ts +27 -22
  105. package/src/ai/types.ts +50 -62
  106. package/src/ai/ui.tsx +13 -0
  107. package/src/ai/usage.ts +8 -8
  108. package/src/ai/validate.ts +8 -7
  109. package/src/ai/view-block.ts +2 -2
  110. package/src/api/accounts-entities.ts +22 -3
  111. package/src/api/admin-ai-quotas.ts +34 -56
  112. package/src/api/admin-ai-skills.ts +4 -1
  113. package/src/api/admin-ai-usage.ts +1 -1
  114. package/src/api/admin-ai.ts +173 -0
  115. package/src/api/admin-core-settings.ts +68 -244
  116. package/src/api/app-approval.ts +3 -1
  117. package/src/api/app-event-delivery.ts +51 -0
  118. package/src/api/app-events.ts +123 -0
  119. package/src/api/app-resource-access.ts +48 -0
  120. package/src/api/auth/account-mismatch.ts +24 -0
  121. package/src/api/auth.ts +35 -5
  122. package/src/api/capabilities.ts +28 -4
  123. package/src/api/index.ts +7 -1
  124. package/src/api/mcp.ts +175 -2
  125. package/src/api/me.ts +6 -4
  126. package/src/api/pwa-phone.ts +75 -26
  127. package/src/api/pwa.ts +34 -8
  128. package/src/api/resource-links.ts +83 -6
  129. package/src/api/resource-resolve.ts +50 -0
  130. package/src/api/search/schemas.ts +3 -1
  131. package/src/api/search.ts +9 -7
  132. package/src/app-pwa/contracts.ts +4 -0
  133. package/src/app-pwa/core-query-error.ts +20 -0
  134. package/src/app-pwa/index.ts +1 -0
  135. package/src/app-pwa/service.ts +91 -0
  136. package/src/app-pwa/transport.ts +22 -0
  137. package/src/approvals/messages.ts +2 -2
  138. package/src/browser/cloud-id.ts +86 -0
  139. package/src/browser/resource-clipboard.ts +4 -0
  140. package/src/browser/resource-links.ts +52 -0
  141. package/src/browser/session.ts +39 -0
  142. package/src/browser/shared-text-history-validation.ts +151 -0
  143. package/src/browser/shared-text-history.ts +464 -0
  144. package/src/browser/shared-text-offline-authorized.ts +29 -0
  145. package/src/browser/shared-text-offline.ts +351 -0
  146. package/src/browser/shared-text-provider.ts +714 -0
  147. package/src/browser/shared-text-table-contract.ts +150 -0
  148. package/src/browser/shared-text.ts +19 -0
  149. package/src/capabilities/server-invoke.ts +2 -11
  150. package/src/capabilities/testing.ts +5 -2
  151. package/src/cli/access.ts +6 -5
  152. package/src/cli/admin/ai-models.ts +199 -0
  153. package/src/cli/admin/ai-quotas.ts +6 -102
  154. package/src/cli/admin/ai-usage.ts +4 -4
  155. package/src/cli/admin/app-credentials.ts +5 -3
  156. package/src/cli/admin/index.ts +5 -2
  157. package/src/cli/apps.ts +89 -26
  158. package/src/cli/capabilities.ts +22 -1
  159. package/src/cli/events.ts +320 -0
  160. package/src/config/env.ts +40 -0
  161. package/src/contracts/app-access.ts +26 -2
  162. package/src/contracts/app-details.ts +89 -0
  163. package/src/contracts/app-events.ts +51 -0
  164. package/src/contracts/app.ts +21 -5
  165. package/src/contracts/capabilities.ts +143 -3
  166. package/src/contracts/index.ts +2 -0
  167. package/src/contracts/notification-types.ts +36 -9
  168. package/src/contracts/pwa.ts +17 -1
  169. package/src/contracts/registry.ts +6 -0
  170. package/src/contracts/resource-clipboard.ts +35 -0
  171. package/src/contracts/resource-pages.ts +140 -0
  172. package/src/contracts/resource-resolution.ts +13 -0
  173. package/src/contracts/shared.ts +6 -1
  174. package/src/contracts/user-notifications.ts +2 -0
  175. package/src/events/app-event-delivery.ts +100 -0
  176. package/src/events/app-events.testing.ts +65 -0
  177. package/src/events/app-events.ts +134 -0
  178. package/src/events/core-user-created.ts +24 -0
  179. package/src/events/index.ts +9 -0
  180. package/src/events/testing.ts +2 -0
  181. package/src/index.ts +1 -1
  182. package/src/server/api/respond.ts +17 -4
  183. package/src/server/index.ts +2 -0
  184. package/src/server/middleware/app-access.ts +1 -1
  185. package/src/server/middleware/auth.ts +21 -7
  186. package/src/server/middleware/index.ts +1 -0
  187. package/src/server/middleware/invocation.ts +2 -0
  188. package/src/server/public-storage.ts +106 -0
  189. package/src/server/services/access.ts +31 -10
  190. package/src/server/services/index.ts +1 -0
  191. package/src/services/accounts/app.ts +36 -13
  192. package/src/services/accounts/entities.ts +43 -19
  193. package/src/services/accounts/group-sql.ts +23 -0
  194. package/src/services/accounts/groups.ts +19 -14
  195. package/src/services/accounts/identities.ts +70 -11
  196. package/src/services/accounts/identity-reconciliation.ts +3 -1
  197. package/src/services/accounts/local-groups.ts +69 -4
  198. package/src/services/accounts/users.ts +45 -14
  199. package/src/services/app-access.ts +25 -7
  200. package/src/services/app-details.ts +224 -0
  201. package/src/services/app-discovery.ts +2 -1
  202. package/src/services/app-event-shapes.ts +139 -0
  203. package/src/services/app-events/accept.ts +116 -0
  204. package/src/services/app-events/admin.ts +250 -0
  205. package/src/services/app-events/declarations.ts +187 -0
  206. package/src/services/app-events/deliver.ts +218 -0
  207. package/src/services/app-events.ts +92 -0
  208. package/src/services/config-conflict.ts +11 -0
  209. package/src/services/core-user-events.ts +1 -0
  210. package/src/services/freeipa-config.ts +9 -0
  211. package/src/services/geo/ledger.ts +28 -10
  212. package/src/services/geo/service.ts +43 -24
  213. package/src/services/identity/core-origin.ts +12 -0
  214. package/src/services/identity/invocation-actor.ts +20 -2
  215. package/src/services/identity/invocation-authority.ts +4 -0
  216. package/src/services/identity/invocation-token.ts +238 -56
  217. package/src/services/identity/issue-invocation.ts +6 -1
  218. package/src/services/image-metadata.ts +65 -1
  219. package/src/services/index.ts +13 -1
  220. package/src/services/ipa/sync.ts +6 -2
  221. package/src/services/ipa/users.ts +4 -1
  222. package/src/services/mandates/index.ts +2 -2
  223. package/src/services/mandates/policy.ts +20 -12
  224. package/src/services/notifications/browser.ts +4 -0
  225. package/src/services/notifications/catalog.ts +5 -2
  226. package/src/services/notifications/platform.ts +7 -2
  227. package/src/services/notifications/user.ts +18 -7
  228. package/src/services/oauth-tokens.ts +28 -2
  229. package/src/services/object-store.ts +217 -0
  230. package/src/services/providers/local/users.ts +5 -0
  231. package/src/services/pwa-devices.ts +147 -37
  232. package/src/services/session/index.ts +26 -3
  233. package/src/services/settings/core-settings.ts +71 -43
  234. package/src/services/settings/index.ts +8 -0
  235. package/src/services/settings/versions.ts +26 -0
  236. package/src/shared/ai-models.ts +274 -0
  237. package/src/shared/ai-quotas.ts +2 -2
  238. package/src/shared/ai-usage.ts +2 -2
  239. package/src/shared/email-html.ts +1 -0
  240. package/src/shared/index.ts +2 -0
  241. package/src/shared/markdown/extensions/katex.ts +8 -1
  242. package/src/shared/markdown/extensions/tables.ts +14 -1
  243. package/src/shared/markdown/index.ts +22 -11
  244. package/src/shared-text/anchors.ts +29 -0
  245. package/src/shared-text/awareness.ts +87 -0
  246. package/src/shared-text/changes.ts +27 -0
  247. package/src/shared-text/documents.ts +433 -65
  248. package/src/shared-text/events.ts +12 -2
  249. package/src/shared-text/index.ts +16 -1
  250. package/src/shared-text/log.ts +24 -5
  251. package/src/shared-text/snapshots.ts +69 -23
  252. package/src/shared-text/structural-history.ts +377 -0
  253. package/src/shared-text/table-actions.ts +432 -0
  254. package/src/shared-text/text-diff.ts +75 -0
  255. package/src/shared-text/types.ts +24 -5
  256. package/src/ssr/AdminArea.tsx +131 -0
  257. package/src/ssr/AdminLayout.tsx +9 -3
  258. package/src/ssr/AdminSidebar.tsx +7 -7
  259. package/src/ssr/AppAccessDenied.tsx +2 -0
  260. package/src/ssr/AppBadge.tsx +14 -0
  261. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  262. package/src/ssr/CloudShell.island.tsx +44 -7
  263. package/src/ssr/Layout.tsx +9 -7
  264. package/src/ssr/MobileProfileActions.tsx +2 -2
  265. package/src/ssr/ProfilePreferences.tsx +2 -1
  266. package/src/ssr/PwaLayout.tsx +16 -1
  267. package/src/ssr/PwaPartCount.island.tsx +23 -0
  268. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  269. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  270. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  271. package/src/ssr/admin-active-link.ts +16 -0
  272. package/src/ssr/admin-navigation.ts +26 -31
  273. package/src/ssr/app-badges.ts +31 -7
  274. package/src/ssr/app-launchpad.tsx +6 -1
  275. package/src/ssr/index.ts +8 -0
  276. package/src/ssr/page-trail-runtime.ts +3 -1
  277. package/src/ssr/platform-messages.ts +11 -1
  278. package/src/ssr/profile-actions.ts +3 -6
  279. package/src/ssr/pwa-messages.ts +2 -2
  280. package/src/ssr/rail-navigation.ts +5 -1
  281. package/src/ssr/settings-draft.ts +153 -0
  282. package/src/ssr/shell-data.tsx +3 -1
  283. package/src/styles/global.css +1 -0
  284. package/src/styles/profile-picture.css +270 -0
  285. package/src/styles/utilities-navigation.css +6 -0
  286. package/src/workflows/ai/policy.ts +49 -0
  287. package/src/workflows/ai/runtime.ts +131 -21
  288. package/src/workflows/ai/store.ts +283 -35
  289. package/src/workflows/ai/types.ts +8 -2
  290. package/src/workflows/ai-actions.ts +148 -120
  291. package/src/workflows/authoring.ts +31 -0
  292. package/src/workflows/contracts.ts +1 -0
  293. package/src/workflows/definition.ts +2 -0
  294. package/src/workflows/editor.ts +15 -0
  295. package/src/workflows/index.ts +2 -1
  296. package/src/workflows/language/compiler.ts +34 -3
  297. package/src/workflows/language/expressions.ts +3 -2
  298. package/src/workflows/runtime/executor.ts +134 -34
  299. package/src/workflows/runtime/index.ts +2 -0
  300. package/src/workflows/runtime/ports.ts +5 -1
  301. package/src/workflows/store/actions.ts +1 -1
  302. package/src/workflows/store/definitions.ts +5 -0
  303. package/src/workflows/store/runs.ts +5 -3
  304. package/src/workflows/store/worker.ts +7 -2
  305. package/src/ai/model-pricing.ts +0 -43
  306. package/src/ai/model-request-settings.ts +0 -98
  307. package/src/ai/settings-messages.ts +0 -129
  308. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -0,0 +1,101 @@
1
+ import { S3Client } from "bun";
2
+ import { z } from "zod";
3
+ import { readStates } from "../services/settings/store";
4
+
5
+ export const objectStoreSettingKeys = [
6
+ "storage.endpoint",
7
+ "storage.region",
8
+ "storage.bucket",
9
+ "storage.access_key_id",
10
+ "storage.secret_access_key",
11
+ "storage.max_object_bytes",
12
+ ] as const;
13
+ const bucket = z
14
+ .string()
15
+ .min(3)
16
+ .max(63)
17
+ .regex(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/);
18
+ const Config = z.object({
19
+ endpoint: z.string().min(1),
20
+ region: z.string().min(1),
21
+ bucket,
22
+ access_key_id: z.string().min(1),
23
+ secret_access_key: z.string().min(1),
24
+ max_object_bytes: z
25
+ .number()
26
+ .int()
27
+ .min(1)
28
+ .max(5 * 1024 ** 3),
29
+ });
30
+ export class ObjectStoreConfigurationError extends Error {}
31
+ export class ObjectStoreConnectionError extends Error {}
32
+ let cached: { signature: string; client: S3Client } | undefined;
33
+
34
+ export const objectStoreBackend = async () => {
35
+ // Read one coherent DB snapshot, including stored-state checks. No local TTL:
36
+ // changes and secret rotation apply to the next operation in every process.
37
+ const states = await readStates(objectStoreSettingKeys);
38
+ if (objectStoreSettingKeys.some((key) => states.get(key)?.stored === "unreadable")) {
39
+ throw new ObjectStoreConfigurationError(
40
+ "Storage settings cannot be decrypted. Check this process uses the installation APP_SECRET, then replace unreadable settings in Administration > Settings > Storage.",
41
+ );
42
+ }
43
+ const parsed = Config.safeParse(
44
+ Object.fromEntries(objectStoreSettingKeys.map((key) => [key.slice("storage.".length), states.get(key)?.value])),
45
+ );
46
+ if (!parsed.success)
47
+ throw new ObjectStoreConfigurationError(
48
+ "Storage is not configured. Set the Garage endpoint, region, bucket and credentials in Administration > Settings > Storage.",
49
+ );
50
+ const config = parsed.data;
51
+ const endpoint = URL.parse(config.endpoint);
52
+ if (
53
+ !endpoint ||
54
+ !["http:", "https:"].includes(endpoint.protocol) ||
55
+ endpoint.username ||
56
+ endpoint.password ||
57
+ endpoint.search ||
58
+ endpoint.hash ||
59
+ endpoint.pathname !== "/"
60
+ ) {
61
+ throw new ObjectStoreConfigurationError("Storage endpoint must be an HTTP(S) origin without credentials, path, query or fragment.");
62
+ }
63
+ const signature = JSON.stringify(config);
64
+ if (cached?.signature !== signature) {
65
+ const options = {
66
+ endpoint: endpoint.href,
67
+ accessKeyId: config.access_key_id,
68
+ secretAccessKey: config.secret_access_key,
69
+ region: config.region,
70
+ };
71
+ cached = { signature, client: new S3Client({ ...options, bucket: config.bucket }) };
72
+ }
73
+ return { client: cached.client, limit: config.max_object_bytes };
74
+ };
75
+
76
+ /** Internal administrator check; never an application API or a credential response. */
77
+ export const testObjectStoreConnection = async (): Promise<void> => {
78
+ const backend = await objectStoreBackend();
79
+ const file = backend.client.file(`cloud-connection-test/${crypto.randomUUID()}`);
80
+ const content = "Cloud storage connection test";
81
+ let step = "write";
82
+ try {
83
+ await file.write(content, { type: "text/plain" });
84
+ step = "read";
85
+ if ((await file.text()) !== content) throw new Error("Unexpected content");
86
+ step = "delete";
87
+ await file.delete();
88
+ if (await file.exists()) throw new Error("Object still exists");
89
+ } catch {
90
+ try {
91
+ await file.delete();
92
+ } catch {
93
+ throw new ObjectStoreConnectionError(
94
+ `Storage test failed at ${step}; cleanup failed. Remove cloud-connection-test/ objects after fixing the connection.`,
95
+ );
96
+ }
97
+ throw new ObjectStoreConnectionError(
98
+ `Storage test failed at ${step}. Check the saved settings, Garage availability and bucket permissions, then retry.`,
99
+ );
100
+ }
101
+ };
@@ -0,0 +1,14 @@
1
+ // S3's complete object key, including the area, is limited to 1,024 UTF-8 bytes.
2
+ // Reject percent signs altogether, including nested encodings, before Bun sees a key.
3
+ export const validateObjectKey = (key: string, area: string, allowPrefix = false): string => {
4
+ if (typeof key !== "string" || !key.isWellFormed() || key !== key.normalize("NFC") || /[\\%\p{Cc}\p{Cf}]/u.test(key)) {
5
+ throw new TypeError("Object keys must be NFC text without controls, backslashes, or percent escapes.");
6
+ }
7
+ if (allowPrefix && key === "") return key;
8
+ const path = allowPrefix && key.endsWith("/") ? key.slice(0, -1) : key;
9
+ if (!path || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
10
+ throw new TypeError("Object keys must be relative paths without empty, dot, or parent segments.");
11
+ }
12
+ if (Buffer.byteLength(area + key) > 1024) throw new RangeError("Object key exceeds S3's 1,024-byte limit including its area.");
13
+ return key;
14
+ };
@@ -2,20 +2,25 @@ import type { PlatformPermission } from "../contracts/outgoing-mail";
2
2
 
3
3
  /** Identity of the application started in this process, derived from its declaration. */
4
4
  let applicationId: string | undefined;
5
+ let applicationPackage: string | undefined;
5
6
  let platformPermissions: readonly PlatformPermission[] = [];
6
7
  export const getProcessPlatformPermissions = (): readonly PlatformPermission[] => platformPermissions;
7
8
 
8
9
  export const getProcessApplicationId = (): string | undefined => applicationId;
9
10
 
10
- export const bindProcessApplicationId = (id: string, permissions: readonly PlatformPermission[] = []): void => {
11
- if (applicationId !== undefined && applicationId !== id) {
11
+ export const getProcessApplicationPackage = (): string | undefined => applicationPackage;
12
+
13
+ export const bindProcessApplicationId = (id: string, permissions: readonly PlatformPermission[] = [], packageName = id): void => {
14
+ if (applicationId !== undefined && (applicationId !== id || applicationPackage !== packageName)) {
12
15
  throw new Error(`This process already hosts application "${applicationId}"`);
13
16
  }
14
17
  applicationId = id;
18
+ applicationPackage = packageName;
15
19
  platformPermissions = [...permissions];
16
20
  };
17
21
 
18
22
  export const clearProcessApplicationId = (): void => {
19
23
  applicationId = undefined;
24
+ applicationPackage = undefined;
20
25
  platformPermissions = [];
21
26
  };
@@ -1,4 +1,5 @@
1
1
  import { CLOUD_CLI_MODULE_NAME } from "../cli/plugin";
2
+ import { APP_ADMIN_AREAS } from "../contracts/app";
2
3
  import { AppAccessRuleSchema } from "../contracts/app-access";
3
4
  import { validateInstallable } from "../contracts/installable";
4
5
  import { PlatformPermissionSchema } from "../contracts/outgoing-mail";
@@ -19,6 +20,9 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
19
20
  for (const field of ["id", "name", "icon", "description", "baseUrl"] as const) {
20
21
  if (!isString(value[field]) || value[field].length === 0) return invalid(field, "a non-empty string");
21
22
  }
23
+ if (value.package !== undefined && (typeof value.package !== "string" || !/^[a-z][a-z0-9-]{0,79}$/.test(value.package))) {
24
+ return invalid("package", "a lowercase package name of 1–80 characters");
25
+ }
22
26
  try {
23
27
  const baseUrl = new URL(value.baseUrl as string);
24
28
  if (!(["http:", "https:"] as const).includes(baseUrl.protocol as "http:" | "https:") || baseUrl.username || baseUrl.password) {
@@ -100,7 +104,7 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
100
104
  (group) =>
101
105
  !isRecord(group) ||
102
106
  (group.id !== undefined && !isString(group.id)) ||
103
- (group.section !== undefined && group.section !== "ai") ||
107
+ (group.section !== undefined && !(APP_ADMIN_AREAS as readonly unknown[]).includes(group.section)) ||
104
108
  !isString(group.label) ||
105
109
  !Array.isArray(group.links) ||
106
110
  group.links.some((link) => !isRecord(link) || !isString(link.label) || !isString(link.href) || !isString(link.icon)),
@@ -141,6 +145,12 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
141
145
  if (value.pwa.requiresRoles !== undefined && !isStringArray(value.pwa.requiresRoles)) {
142
146
  return invalid("pwa.requiresRoles", "an array of strings");
143
147
  }
148
+ for (const field of ["badge", "summary"] as const) {
149
+ const path = value.pwa[field];
150
+ if (path !== undefined && (!isString(path) || !isOwnAppPath(path, value.routes))) {
151
+ return invalid(`pwa.${field}`, "a same-origin path below one of the app's routes");
152
+ }
153
+ }
144
154
  }
145
155
  if (value.widgets !== undefined) {
146
156
  if (
@@ -5,6 +5,7 @@ import { calendarSourceIssues } from "../contracts/calendar-source";
5
5
  import type { CapabilityManifest, CapabilityPresentationCatalog } from "../contracts/capabilities";
6
6
  import { fileProviderIssues } from "../contracts/file-provider";
7
7
  import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
8
+ import { resourcePageIssue } from "../contracts/resource-pages";
8
9
  import { type Role, RoleSchema } from "../contracts/shared";
9
10
  import {
10
11
  type DashboardWidgetPresentation,
@@ -333,7 +334,13 @@ export const resolveLiveCapabilityRegistryEntry = (
333
334
  if (!endpoint) return null;
334
335
  try {
335
336
  const read = parseCapabilityManifest(record.manifest, app.id);
336
- const manifest = withValidCalendarSource(withValidFileProvider(read));
337
+ const supported = withValidCalendarSource(withValidFileProvider(read));
338
+ const manifest = supported.resourcePages
339
+ ? {
340
+ ...supported,
341
+ resourcePages: supported.resourcePages.filter((page) => !resourcePageIssue(page.path, app.routes, app.id)),
342
+ }
343
+ : supported;
337
344
  const presentation = compileCapabilityPresentation(manifest, record.presentation, "reader");
338
345
  if (app.capabilities.protocolVersion !== manifest.protocolVersion || app.capabilities.manifestHash !== manifest.manifestHash) {
339
346
  return null;
@@ -71,7 +71,14 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
71
71
  ),
72
72
  legalLinks: e.legalLinks ? e.legalLinks.map((l) => ({ ...l })) : undefined,
73
73
  searchLinks: e.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
74
- pwa: e.pwa ? { href: e.pwa.href, requiresRoles: e.pwa.requiresRoles ? ([...e.pwa.requiresRoles] as Role[]) : undefined } : undefined,
74
+ pwa: e.pwa
75
+ ? {
76
+ href: e.pwa.href,
77
+ requiresRoles: e.pwa.requiresRoles ? ([...e.pwa.requiresRoles] as Role[]) : undefined,
78
+ ...(e.pwa.badge ? { badge: e.pwa.badge } : {}),
79
+ ...(e.pwa.summary ? { summary: e.pwa.summary } : {}),
80
+ }
81
+ : undefined,
75
82
  installable: e.installable ? structuredClone(e.installable) : undefined,
76
83
  openapi: e.openapi,
77
84
  };
@@ -217,6 +217,8 @@ export default function AppAccessEditor(props: AppAccessEditorProps) {
217
217
  };
218
218
  const fixedDescription = () => {
219
219
  switch (props.app.access?.kind) {
220
+ case "resources":
221
+ return t().resources;
220
222
  case "signedIn":
221
223
  return t().fixedSignedIn;
222
224
  case "noGuests":
@@ -2,16 +2,18 @@ import { query } from "@k2b/stdlib/solid";
2
2
  import { Button, InlineGuidance, useLocale } from "@k2b/ui";
3
3
  import { createSignal, createUniqueId, For, Show } from "solid-js";
4
4
  import { CloudAvatar } from "../account/Avatar";
5
- import type { BaseUser, EntityListItem, PaginationResponse } from "../contracts/shared";
5
+ import type { EntityListItem, PaginationResponse } from "../contracts/shared";
6
6
  import { accessMessages } from "./messages";
7
+ import { serviceAccountKindDisplay } from "./service-account-kind";
7
8
 
8
9
  /** Members per request. Further pages load only when the viewer asks for them. */
9
10
  const PAGE_SIZE = 20;
11
+ type GroupMember = Exclude<EntityListItem, { kind: "group" }>;
10
12
 
11
13
  type MembersPage =
12
14
  /** The directory withholds group members from this viewer (guest accounts). */
13
15
  | { visible: false; directoryGroup: boolean }
14
- | { visible: true; directoryGroup: boolean; members: BaseUser[]; total: number; page: number; hasNext: boolean };
16
+ | { visible: true; directoryGroup: boolean; members: GroupMember[]; total: number; page: number; hasNext: boolean };
15
17
 
16
18
  /** Reads the Accounts directory with the viewer's own visibility: `null` when it is withheld. */
17
19
  const listEntities = async (params: Record<string, string>, signal: AbortSignal) => {
@@ -38,7 +40,7 @@ export const createGroupCoverage = (groupId: string) => {
38
40
  const page = cursor ?? 1;
39
41
  const [list, group] = await Promise.all([
40
42
  listEntities(
41
- { kinds: "user", member_of_group_id: id, recursive: "true", page: String(page), per_page: String(PAGE_SIZE) },
43
+ { kinds: "user,service_account", member_of_group_id: id, recursive: "true", page: String(page), per_page: String(PAGE_SIZE) },
42
44
  abortSignal,
43
45
  ),
44
46
  // Only the first page needs the group itself, for the directory hint.
@@ -49,7 +51,7 @@ export const createGroupCoverage = (groupId: string) => {
49
51
  return {
50
52
  visible: true,
51
53
  directoryGroup,
52
- members: list.items.flatMap((item) => (item.kind === "user" ? [item.user] : [])),
54
+ members: list.items.filter((item) => item.kind !== "group"),
53
55
  total: list.pagination.total,
54
56
  page,
55
57
  hasNext: list.pagination.has_next,
@@ -146,7 +148,20 @@ export function GroupCoveragePanel(props: { coverage: GroupCoverage; groupName:
146
148
  <Show when={shown().length > 0}>
147
149
  <ul ref={list} tabindex="-1" aria-label={t().groupMembersOf({ name: props.groupName })} class="flex flex-col gap-1 outline-none">
148
150
  <For each={shown()}>
149
- {(user) => {
151
+ {(member) => {
152
+ if (member.kind === "service_account") {
153
+ const kind = () => serviceAccountKindDisplay(member.serviceAccount.kind, t());
154
+ return (
155
+ <li class="flex min-w-0 items-center gap-2 text-sm">
156
+ <span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
157
+ <i class={`ti ${kind().icon}`} aria-hidden="true" />
158
+ </span>
159
+ <span class="truncate">{member.serviceAccount.name}</span>
160
+ <span class="shrink-0 text-xs text-muted">{kind().label}</span>
161
+ </li>
162
+ );
163
+ }
164
+ const user = member.user;
150
165
  const name = user.displayName || user.mail || user.uid;
151
166
  return (
152
167
  <li class="flex min-w-0 items-center gap-2 text-sm">
@@ -3,7 +3,7 @@ import { announce, Button, IconButton, Placeholder, prompts, Select, SelectChip,
3
3
  import { createEffect, createMemo, createSignal, For, on, onCleanup, Show } from "solid-js";
4
4
  import { createStore, reconcile } from "solid-js/store";
5
5
  import { CloudAvatar } from "../account/Avatar";
6
- import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind } from "../contracts/shared";
6
+ import type { AccessEntry, PermissionLevel, Principal, ServiceAccountKind, ServiceAccountStatus } from "../contracts/shared";
7
7
  import { createGroupCoverage, type GroupCoverage, GroupCoveragePanel, GroupCoverageToggle } from "./GroupCoverage";
8
8
  import { isManagerEntry } from "./managers";
9
9
  import { accessMessages } from "./messages";
@@ -81,9 +81,13 @@ type PermissionEditorProps = {
81
81
  * true; resources with an explicit principal allowlist can disable it. */
82
82
  allowAuthenticated?: boolean;
83
83
 
84
- /** Allow granting service accounts. Off by default so ordinary
85
- * permission pickers stay user/group focused. */
84
+ /** Allow granting service accounts. Defaults to true, including agents.
85
+ * Disable only when the resource documents a user-backed identity requirement. */
86
86
  allowServiceAccounts?: boolean;
87
+ /** Optional directory filters for the resource's accepted credential kinds. */
88
+ serviceAccountKinds?: ServiceAccountKind[];
89
+ /** Grant selection defaults to active accounts; existing entries are unaffected. */
90
+ serviceAccountStatus?: ServiceAccountStatus;
87
91
 
88
92
  /** The principal search's placeholder, in the consumer's terms. Defaults to the principal kinds it offers. */
89
93
  placeholder?: string;
@@ -445,6 +449,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
445
449
  allowPublic={allowPublic()}
446
450
  allowAuthenticated={allowAuthenticated()}
447
451
  allowServiceAccounts={props.allowServiceAccounts}
452
+ serviceAccountKinds={props.serviceAccountKinds}
453
+ serviceAccountStatus={props.serviceAccountStatus ?? "active"}
448
454
  placeholder={props.placeholder}
449
455
  signedIn={props.signedIn}
450
456
  disabled={saving()}
@@ -563,6 +569,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
563
569
  allowPublic={allowPublic()}
564
570
  allowAuthenticated={allowAuthenticated()}
565
571
  allowServiceAccounts={props.allowServiceAccounts}
572
+ serviceAccountKinds={props.serviceAccountKinds}
573
+ serviceAccountStatus={props.serviceAccountStatus ?? "active"}
566
574
  placeholder={props.placeholder}
567
575
  signedIn={props.signedIn}
568
576
  readOnly={grantPending()}
@@ -645,7 +653,7 @@ function AccessEntryRow(props: {
645
653
  >
646
654
  <Show when={!props.expanded} fallback={<PrincipalAvatar entry={props.entry} label={displayName()} />}>
647
655
  <Show
648
- when={props.entry.principal.type === "user"}
656
+ when={props.entry.principal.type === "user" || props.entry.principal.type === "service_account"}
649
657
  fallback={
650
658
  <div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-zinc-200 text-secondary dark:bg-zinc-700">
651
659
  <i class={`ti ${getPrincipalIcon(props.entry, t())} text-sm`} />
@@ -654,8 +662,15 @@ function AccessEntryRow(props: {
654
662
  >
655
663
  <CloudAvatar
656
664
  username={displayName()}
657
- userId={props.entry.principal.type === "user" ? props.entry.principal.userId : undefined}
665
+ userId={
666
+ props.entry.principal.type === "user"
667
+ ? props.entry.principal.userId
668
+ : props.entry.principal.type === "service_account"
669
+ ? props.entry.principal.serviceAccountId
670
+ : undefined
671
+ }
658
672
  avatarHash={props.entry.avatarHash}
673
+ kind={props.entry.principal.type === "service_account" ? "service_account" : "user"}
659
674
  size="xs"
660
675
  class="h-7 w-7"
661
676
  />
@@ -2,9 +2,9 @@ import { Show } from "solid-js";
2
2
  import { CloudAvatar } from "../account/Avatar";
3
3
  import type { AccessEntry } from "../contracts/shared";
4
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;
5
+ // One hue per principal kind from the avatar palette, so a group and all signed-in people read apart at a glance.
6
+ // People use a neutral background; a service account shows its robot or photo, never a bare icon.
7
+ const hues = { group: "#22c55e", authenticated: "#3b82f6" } as const;
8
8
 
9
9
  /** The 32px avatar of an access entry in the expanded editor and on the denial page. */
10
10
  export default function PrincipalAvatar(props: {
@@ -15,8 +15,6 @@ export default function PrincipalAvatar(props: {
15
15
  switch (props.entry.principal.type) {
16
16
  case "group":
17
17
  return "ti-users-group";
18
- case "service_account":
19
- return "ti-robot";
20
18
  case "authenticated":
21
19
  return "ti-world";
22
20
  default:
@@ -25,27 +23,38 @@ export default function PrincipalAvatar(props: {
25
23
  };
26
24
  const hue = () => {
27
25
  const type = props.entry.principal.type;
28
- return type === "group" || type === "service_account" || type === "authenticated" ? hues[type] : undefined;
26
+ return type === "group" || type === "authenticated" ? hues[type] : undefined;
29
27
  };
30
28
  return (
31
29
  <Show
32
30
  when={props.entry.principal.type === "user" ? props.entry.principal : undefined}
33
31
  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>
32
+ props.entry.principal.type === "service_account" ? (
33
+ <CloudAvatar
34
+ username={props.label}
35
+ userId={props.entry.principal.serviceAccountId}
36
+ avatarHash={props.entry.avatarHash}
37
+ kind="service_account"
38
+ size="sm"
39
+ class="h-8 w-8"
40
+ />
41
+ ) : (
42
+ <span
43
+ class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--ui-surface-muted)] text-secondary"
44
+ // The avatar formula: a light share of the hue on the surface, a dark share on the text, in both themes.
45
+ style={
46
+ hue()
47
+ ? {
48
+ background: `color-mix(in srgb, ${hue()} 18%, var(--k2b-surface))`,
49
+ color: `color-mix(in srgb, ${hue()} 55%, var(--k2b-text))`,
50
+ }
51
+ : undefined
52
+ }
53
+ aria-hidden="true"
54
+ >
55
+ <i class={`ti ${icon()} text-xs`} />
56
+ </span>
57
+ )
49
58
  }
50
59
  >
51
60
  {(user) => (
@@ -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";
@@ -34,7 +34,11 @@ export default function PrincipalPicker(props: {
34
34
  existing?: Principal[];
35
35
  allowPublic?: boolean;
36
36
  allowAuthenticated?: boolean;
37
+ /** Defaults to true; disable only for a documented user-backed workflow. */
37
38
  allowServiceAccounts?: boolean;
39
+ /** Optional directory filters for the resource's accepted credential kinds. */
40
+ serviceAccountKinds?: ServiceAccountKind[];
41
+ serviceAccountStatus?: ServiceAccountStatus;
38
42
  /** Offers only the service accounts this returns `true` for, such as active accounts the resource can accept. */
39
43
  serviceAccountFilter?: (account: PrincipalPickerServiceAccount) => boolean;
40
44
  disabled?: boolean;
@@ -78,7 +82,13 @@ export default function PrincipalPicker(props: {
78
82
  if (search.length >= 2) {
79
83
  const url = new URL("/api/accounts/entities", window.location.origin);
80
84
  url.searchParams.set("search", search);
81
- 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
+ }
82
92
  url.searchParams.set("per_page", "10");
83
93
  const users = props.existing?.flatMap((p) => (p.type === "user" ? [p.userId] : [])) ?? [];
84
94
  const groups = props.existing?.flatMap((p) => (p.type === "group" ? [p.groupId] : [])) ?? [];
@@ -145,10 +155,10 @@ export default function PrincipalPicker(props: {
145
155
  placeholder={
146
156
  props.placeholder ??
147
157
  (props.allowAuthenticated !== false || props.allowPublic
148
- ? props.allowServiceAccounts
158
+ ? props.allowServiceAccounts !== false
149
159
  ? t().addAll
150
160
  : t().addAudience
151
- : props.allowServiceAccounts
161
+ : props.allowServiceAccounts !== false
152
162
  ? t().addService
153
163
  : t().addBasic)
154
164
  }
@@ -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">
@@ -55,12 +55,15 @@ export const appAccessMessages = i18n.define({
55
55
  fixedNoGuests: "Signed-in people except guests. The app sets this rule; there is nothing to configure here.",
56
56
  fixedAdmins: "Cloud administrators only. The app sets this rule; there is nothing to configure here.",
57
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",
58
60
  legacy: "This app uses its existing access checks.",
59
61
  deniedTitle: ({ name }: { name: string }) => `${name} is not available to you`,
60
62
  revokedTitle: ({ name }: { name: string }) => `Your access to ${name} was removed`,
61
63
  revokedDescription: "You had access until now. If this was a mistake, ask:",
62
64
  deniedGrants: "Only people with access may use this app. Access is managed by:",
63
65
  deniedFixed: "If you need this app, contact your Cloud administration.",
66
+ deniedResources: "Ask someone who manages resource permissions in this app for access.",
64
67
  deniedAdmins: ({ name }: { name: string }) => `Only Cloud administrators use ${name}.`,
65
68
  deniedNoGuests: ({ name }: { name: string }) => `Guests cannot use ${name}.`,
66
69
  deniedRoles: ({ name }: { name: string }) => `Only people with specific Cloud roles use ${name}.`,
@@ -134,12 +137,15 @@ export const appAccessMessages = i18n.define({
134
137
  fixedNoGuests: "Angemeldete Personen außer Gästen. Das legt die App fest; hier gibt es dazu nichts einzustellen.",
135
138
  fixedAdmins: "Nur Cloud-Admins. Das legt die App fest; hier gibt es dazu nichts einzustellen.",
136
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",
137
142
  legacy: "Diese App verwendet ihre bisherigen Zugriffsprüfungen.",
138
143
  deniedTitle: ({ name }: { name: string }) => `${name} ist für dich nicht freigegeben`,
139
144
  revokedTitle: ({ name }: { name: string }) => `Dein Zugriff auf ${name} wurde entfernt`,
140
145
  revokedDescription: "Du hattest bis eben Zugriff. Wenn das ein Versehen ist, frag:",
141
146
  deniedGrants: "Diese App nutzen nur Personen mit Freigabe. Den Zugriff verwalten:",
142
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.",
143
149
  deniedAdmins: ({ name }: { name: string }) => `${name} nutzen nur Cloud-Admins.`,
144
150
  deniedNoGuests: ({ name }: { name: string }) => `${name} nutzen nur angemeldete Personen außer Gästen.`,
145
151
  deniedRoles: ({ name }: { name: string }) => `${name} nutzen nur Personen mit bestimmten Cloud-Rollen.`,
@@ -47,8 +47,8 @@ export const accessMessages = i18n.define({
47
47
  count === 0
48
48
  ? "No one receives access through this group right now."
49
49
  : count === 1
50
- ? "1 person receives access through this group right now."
51
- : `${count.toLocaleString("en")} people receive access through this group right now.`,
50
+ ? "1 account receives access through this group right now."
51
+ : `${count.toLocaleString("en")} accounts receive access through this group right now.`,
52
52
  groupMembersLoading: "Loading members…",
53
53
  groupMembersHidden: "Your account can't see who belongs to this group.",
54
54
  groupMembersFailed: "Members couldn't be loaded.",
@@ -137,8 +137,8 @@ export const accessMessages = i18n.define({
137
137
  count === 0
138
138
  ? "Über diese Gruppe erhält gerade niemand Zugriff."
139
139
  : count === 1
140
- ? "Über diese Gruppe erhält gerade 1 Person Zugriff."
141
- : `Über diese Gruppe erhalten gerade ${count.toLocaleString("de")} Personen Zugriff.`,
140
+ ? "Über diese Gruppe erhält gerade 1 Konto Zugriff."
141
+ : `Über diese Gruppe erhalten gerade ${count.toLocaleString("de")} Konten Zugriff.`,
142
142
  groupMembersLoading: "Mitglieder werden geladen…",
143
143
  groupMembersHidden: "Dein Konto kann nicht sehen, wer zu dieser Gruppe gehört.",
144
144
  groupMembersFailed: "Die Mitglieder konnten nicht geladen werden.",
@@ -5,6 +5,7 @@ import { createSignal, For, Show } from "solid-js";
5
5
  import { accessMessages } from "../access/messages";
6
6
  import { principalLabel } from "../access/principal-label";
7
7
  import { serviceAccountKindDisplay } from "../access/service-account-kind";
8
+ import type { ServiceAccountKind, ServiceAccountStatus } from "../contracts/shared";
8
9
  import { groupDisplayName } from "../shared/account-display";
9
10
  import { CloudAvatar } from "./Avatar";
10
11
 
@@ -52,6 +53,10 @@ type EntitySearchProps = {
52
53
  includeGroups?: boolean;
53
54
  /** Surface service accounts in the result list. Off by default. */
54
55
  includeServiceAccounts?: boolean;
56
+ /** Restrict service-account kinds before pagination. Omitted or empty allows all kinds. */
57
+ serviceAccountKinds?: ServiceAccountKind[];
58
+ /** Restrict service-account status before pagination. Omitted allows both statuses. */
59
+ serviceAccountStatus?: ServiceAccountStatus;
55
60
  /** Inject a synthetic "All authenticated users" row at the top. */
56
61
  includeAuthenticated?: boolean;
57
62
  /** Inject a synthetic "Public" row at the top. */
@@ -171,6 +176,12 @@ const EntitySearch = (props: EntitySearchProps) => {
171
176
  if (props.excludeServiceAccountIds?.length) {
172
177
  url.searchParams.set("exclude_service_account_ids", props.excludeServiceAccountIds.join(","));
173
178
  }
179
+ if (props.includeServiceAccounts && props.serviceAccountKinds?.length) {
180
+ url.searchParams.set("service_account_kinds", props.serviceAccountKinds.join(","));
181
+ }
182
+ if (props.includeServiceAccounts && props.serviceAccountStatus) {
183
+ url.searchParams.set("service_account_status", props.serviceAccountStatus);
184
+ }
174
185
  if (props.onlyMembersOf?.length) {
175
186
  url.searchParams.set("user_member_of_group_ids", props.onlyMembersOf.join(","));
176
187
  }
@@ -296,6 +307,7 @@ const EntitySearch = (props: EntitySearchProps) => {
296
307
  {(serviceAccount) => (
297
308
  <ResultRow
298
309
  icon={serviceAccountKind(serviceAccount).icon}
310
+ avatar={<CloudAvatar username={serviceAccount.name} userId={serviceAccount.id} kind="service_account" size="sm" />}
299
311
  title={serviceAccount.name}
300
312
  subtitle={
301
313
  // Resource-bound accounts often share a name; their binding tells them apart.