@k2b/cloud 0.43.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 (267) hide show
  1. package/package.json +7 -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/PermissionEditor.tsx +9 -3
  21. package/src/access/PrincipalPicker.tsx +14 -4
  22. package/src/access/ResourceApiKeys.tsx +3 -4
  23. package/src/access/app-access-messages.ts +6 -0
  24. package/src/account/ProfilePictureDialog.tsx +699 -0
  25. package/src/account/profile-picture-messages.ts +269 -0
  26. package/src/account/ui.ts +7 -0
  27. package/src/ai/admin.ts +3 -0
  28. package/src/ai/app-frame.ts +12 -0
  29. package/src/ai/application-tasks.ts +101 -0
  30. package/src/ai/assistant-models.ts +126 -64
  31. package/src/ai/capabilities.ts +19 -4
  32. package/src/ai/chart-block.ts +63 -30
  33. package/src/ai/chat/chart-block.tsx +12 -3
  34. package/src/ai/chat/composer-adapter.ts +8 -7
  35. package/src/ai/chat/message-actions.tsx +1 -1
  36. package/src/ai/chat/message-utils.ts +2 -2
  37. package/src/ai/chat/presentation.tsx +22 -1
  38. package/src/ai/chat-quotas.ts +5 -4
  39. package/src/ai/chat-tasks.ts +2 -2
  40. package/src/ai/client/controller.ts +11 -10
  41. package/src/ai/client/projection.ts +3 -3
  42. package/src/ai/compaction.ts +3 -3
  43. package/src/ai/credentials.ts +13 -114
  44. package/src/ai/default-tool-contracts.ts +4 -3
  45. package/src/ai/dictation-runtime.ts +4 -4
  46. package/src/ai/enrich.ts +2 -2
  47. package/src/ai/executor.ts +31 -22
  48. package/src/ai/http.ts +17 -29
  49. package/src/ai/index.ts +11 -5
  50. package/src/ai/inference-calls.ts +5 -4
  51. package/src/ai/maintenance.ts +1 -1
  52. package/src/ai/memory-learning-runs.ts +5 -5
  53. package/src/ai/memory-learning.ts +3 -3
  54. package/src/ai/message-queue.ts +1 -2
  55. package/src/ai/migrate.ts +75 -42
  56. package/src/ai/model-access-migrate.ts +3 -17
  57. package/src/ai/model-access.ts +44 -85
  58. package/src/ai/model-card.tsx +134 -0
  59. package/src/ai/model-catalog.ts +359 -0
  60. package/src/ai/model-chooser-messages.ts +88 -0
  61. package/src/ai/model-chooser.tsx +577 -0
  62. package/src/ai/model-delete.ts +234 -0
  63. package/src/ai/model-fixture.ts +39 -0
  64. package/src/ai/model-icon.tsx +36 -0
  65. package/src/ai/model-overview.ts +53 -0
  66. package/src/ai/model-usage.ts +45 -0
  67. package/src/ai/openai-api-messages.ts +0 -4
  68. package/src/ai/openai-api.ts +26 -9
  69. package/src/ai/personal-agent.ts +1 -1
  70. package/src/ai/prefs.ts +12 -10
  71. package/src/ai/projects-routes.ts +2 -1
  72. package/src/ai/projects.ts +13 -12
  73. package/src/ai/protocol.ts +2 -2
  74. package/src/ai/provider-connections.ts +161 -0
  75. package/src/ai/provider-errors.ts +55 -6
  76. package/src/ai/provider-fetch.ts +4 -0
  77. package/src/ai/provider.ts +1 -1
  78. package/src/ai/quota-limits.ts +36 -0
  79. package/src/ai/quota-provider.ts +4 -2
  80. package/src/ai/quota-report.ts +7 -4
  81. package/src/ai/quotas.ts +19 -8
  82. package/src/ai/recent-resources.ts +6 -0
  83. package/src/ai/request-headers.ts +14 -87
  84. package/src/ai/resource-refs.ts +6 -2
  85. package/src/ai/routes.ts +17 -18
  86. package/src/ai/runtime.ts +50 -22
  87. package/src/ai/settings.ts +93 -580
  88. package/src/ai/store.ts +45 -23
  89. package/src/ai/stream.ts +1 -1
  90. package/src/ai/structured-runs.ts +2 -2
  91. package/src/ai/structured.ts +21 -44
  92. package/src/ai/task-contracts.ts +25 -5
  93. package/src/ai/task-execution.ts +15 -6
  94. package/src/ai/task-model.ts +9 -0
  95. package/src/ai/transcription.ts +27 -22
  96. package/src/ai/types.ts +50 -62
  97. package/src/ai/ui.tsx +13 -0
  98. package/src/ai/usage.ts +8 -8
  99. package/src/ai/validate.ts +8 -7
  100. package/src/ai/view-block.ts +2 -2
  101. package/src/api/accounts-entities.ts +16 -0
  102. package/src/api/admin-ai-quotas.ts +34 -56
  103. package/src/api/admin-ai-usage.ts +1 -1
  104. package/src/api/admin-ai.ts +173 -0
  105. package/src/api/admin-core-settings.ts +68 -244
  106. package/src/api/app-approval.ts +3 -1
  107. package/src/api/app-event-delivery.ts +51 -0
  108. package/src/api/app-events.ts +123 -0
  109. package/src/api/app-resource-access.ts +48 -0
  110. package/src/api/auth/account-mismatch.ts +24 -0
  111. package/src/api/auth.ts +26 -3
  112. package/src/api/capabilities.ts +28 -4
  113. package/src/api/index.ts +7 -1
  114. package/src/api/mcp.ts +175 -2
  115. package/src/api/me.ts +6 -4
  116. package/src/api/pwa-phone.ts +75 -26
  117. package/src/api/pwa.ts +34 -8
  118. package/src/api/resource-links.ts +83 -6
  119. package/src/api/resource-resolve.ts +50 -0
  120. package/src/api/search/schemas.ts +3 -1
  121. package/src/api/search.ts +9 -7
  122. package/src/app-pwa/contracts.ts +4 -0
  123. package/src/app-pwa/core-query-error.ts +20 -0
  124. package/src/app-pwa/index.ts +1 -0
  125. package/src/app-pwa/service.ts +91 -0
  126. package/src/app-pwa/transport.ts +22 -0
  127. package/src/approvals/messages.ts +2 -2
  128. package/src/browser/cloud-id.ts +86 -0
  129. package/src/browser/resource-clipboard.ts +4 -0
  130. package/src/browser/resource-links.ts +52 -0
  131. package/src/capabilities/server-invoke.ts +2 -11
  132. package/src/capabilities/testing.ts +5 -2
  133. package/src/cli/access.ts +6 -5
  134. package/src/cli/admin/ai-models.ts +199 -0
  135. package/src/cli/admin/ai-quotas.ts +6 -102
  136. package/src/cli/admin/ai-usage.ts +4 -4
  137. package/src/cli/admin/app-credentials.ts +5 -3
  138. package/src/cli/admin/index.ts +5 -2
  139. package/src/cli/apps.ts +89 -26
  140. package/src/cli/capabilities.ts +22 -1
  141. package/src/cli/events.ts +320 -0
  142. package/src/config/env.ts +40 -0
  143. package/src/contracts/app-access.ts +26 -2
  144. package/src/contracts/app-details.ts +89 -0
  145. package/src/contracts/app-events.ts +51 -0
  146. package/src/contracts/app.ts +21 -5
  147. package/src/contracts/capabilities.ts +143 -3
  148. package/src/contracts/index.ts +2 -0
  149. package/src/contracts/notification-types.ts +36 -9
  150. package/src/contracts/pwa.ts +17 -1
  151. package/src/contracts/registry.ts +6 -0
  152. package/src/contracts/resource-clipboard.ts +35 -0
  153. package/src/contracts/resource-pages.ts +140 -0
  154. package/src/contracts/resource-resolution.ts +13 -0
  155. package/src/contracts/shared.ts +4 -0
  156. package/src/contracts/user-notifications.ts +2 -0
  157. package/src/events/app-event-delivery.ts +100 -0
  158. package/src/events/app-events.testing.ts +65 -0
  159. package/src/events/app-events.ts +134 -0
  160. package/src/events/core-user-created.ts +24 -0
  161. package/src/events/index.ts +9 -0
  162. package/src/events/testing.ts +2 -0
  163. package/src/index.ts +1 -1
  164. package/src/server/api/respond.ts +8 -3
  165. package/src/server/index.ts +1 -0
  166. package/src/server/middleware/app-access.ts +1 -1
  167. package/src/server/middleware/auth.ts +21 -7
  168. package/src/server/middleware/index.ts +1 -0
  169. package/src/server/middleware/invocation.ts +2 -0
  170. package/src/server/public-storage.ts +106 -0
  171. package/src/server/services/access.ts +8 -0
  172. package/src/services/accounts/app.ts +6 -0
  173. package/src/services/accounts/entities.ts +14 -3
  174. package/src/services/accounts/groups.ts +3 -1
  175. package/src/services/accounts/identities.ts +62 -10
  176. package/src/services/accounts/users.ts +7 -6
  177. package/src/services/app-access.ts +25 -7
  178. package/src/services/app-details.ts +224 -0
  179. package/src/services/app-discovery.ts +2 -1
  180. package/src/services/app-event-shapes.ts +139 -0
  181. package/src/services/app-events/accept.ts +116 -0
  182. package/src/services/app-events/admin.ts +250 -0
  183. package/src/services/app-events/declarations.ts +187 -0
  184. package/src/services/app-events/deliver.ts +218 -0
  185. package/src/services/app-events.ts +92 -0
  186. package/src/services/config-conflict.ts +11 -0
  187. package/src/services/core-user-events.ts +1 -0
  188. package/src/services/freeipa-config.ts +9 -0
  189. package/src/services/geo/ledger.ts +28 -10
  190. package/src/services/geo/service.ts +43 -24
  191. package/src/services/identity/core-origin.ts +12 -0
  192. package/src/services/identity/invocation-actor.ts +20 -2
  193. package/src/services/identity/invocation-authority.ts +4 -0
  194. package/src/services/identity/invocation-token.ts +238 -56
  195. package/src/services/identity/issue-invocation.ts +6 -1
  196. package/src/services/image-metadata.ts +65 -1
  197. package/src/services/index.ts +13 -1
  198. package/src/services/ipa/sync.ts +6 -2
  199. package/src/services/ipa/users.ts +4 -1
  200. package/src/services/mandates/policy.ts +19 -12
  201. package/src/services/notifications/browser.ts +4 -0
  202. package/src/services/notifications/catalog.ts +5 -2
  203. package/src/services/notifications/platform.ts +7 -2
  204. package/src/services/notifications/user.ts +18 -7
  205. package/src/services/oauth-tokens.ts +28 -2
  206. package/src/services/object-store.ts +217 -0
  207. package/src/services/providers/local/users.ts +5 -0
  208. package/src/services/pwa-devices.ts +147 -37
  209. package/src/services/session/index.ts +14 -0
  210. package/src/services/settings/core-settings.ts +71 -43
  211. package/src/services/settings/index.ts +8 -0
  212. package/src/services/settings/versions.ts +26 -0
  213. package/src/shared/ai-models.ts +274 -0
  214. package/src/shared/ai-quotas.ts +2 -2
  215. package/src/shared/ai-usage.ts +2 -2
  216. package/src/shared/index.ts +2 -0
  217. package/src/shared-text/documents.ts +25 -8
  218. package/src/shared-text/log.ts +1 -1
  219. package/src/shared-text/text-diff.ts +75 -0
  220. package/src/ssr/AdminArea.tsx +131 -0
  221. package/src/ssr/AdminLayout.tsx +9 -3
  222. package/src/ssr/AdminSidebar.tsx +7 -7
  223. package/src/ssr/AppAccessDenied.tsx +2 -0
  224. package/src/ssr/AppBadge.tsx +14 -0
  225. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  226. package/src/ssr/CloudShell.island.tsx +25 -4
  227. package/src/ssr/Layout.tsx +8 -6
  228. package/src/ssr/PwaLayout.tsx +16 -1
  229. package/src/ssr/PwaPartCount.island.tsx +23 -0
  230. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  231. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  232. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  233. package/src/ssr/admin-active-link.ts +16 -0
  234. package/src/ssr/admin-navigation.ts +25 -20
  235. package/src/ssr/app-badges.ts +31 -7
  236. package/src/ssr/index.ts +8 -0
  237. package/src/ssr/page-trail-runtime.ts +3 -1
  238. package/src/ssr/platform-messages.ts +9 -1
  239. package/src/ssr/pwa-messages.ts +2 -2
  240. package/src/ssr/rail-navigation.ts +5 -1
  241. package/src/ssr/settings-draft.ts +153 -0
  242. package/src/ssr/shell-data.tsx +2 -0
  243. package/src/styles/global.css +1 -0
  244. package/src/styles/profile-picture.css +270 -0
  245. package/src/styles/utilities-navigation.css +6 -0
  246. package/src/workflows/ai/policy.ts +49 -0
  247. package/src/workflows/ai/runtime.ts +131 -21
  248. package/src/workflows/ai/store.ts +283 -35
  249. package/src/workflows/ai/types.ts +8 -2
  250. package/src/workflows/ai-actions.ts +148 -120
  251. package/src/workflows/authoring.ts +31 -0
  252. package/src/workflows/contracts.ts +1 -0
  253. package/src/workflows/definition.ts +2 -0
  254. package/src/workflows/editor.ts +15 -0
  255. package/src/workflows/index.ts +2 -1
  256. package/src/workflows/language/compiler.ts +34 -3
  257. package/src/workflows/runtime/executor.ts +132 -33
  258. package/src/workflows/runtime/index.ts +2 -0
  259. package/src/workflows/runtime/ports.ts +5 -1
  260. package/src/workflows/store/actions.ts +1 -1
  261. package/src/workflows/store/definitions.ts +5 -0
  262. package/src/workflows/store/runs.ts +5 -3
  263. package/src/workflows/store/worker.ts +7 -2
  264. package/src/ai/model-pricing.ts +0 -43
  265. package/src/ai/model-request-settings.ts +0 -98
  266. package/src/ai/settings-messages.ts +0 -129
  267. 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":
@@ -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;
@@ -563,6 +567,8 @@ export default function PermissionEditor(props: PermissionEditorProps) {
563
567
  allowPublic={allowPublic()}
564
568
  allowAuthenticated={allowAuthenticated()}
565
569
  allowServiceAccounts={props.allowServiceAccounts}
570
+ serviceAccountKinds={props.serviceAccountKinds}
571
+ serviceAccountStatus={props.serviceAccountStatus ?? "active"}
566
572
  placeholder={props.placeholder}
567
573
  signedIn={props.signedIn}
568
574
  readOnly={grantPending()}
@@ -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.`,