@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,380 @@
1
+ import { Button, bottomSheetAdaptiveOptions, dialogCore, IconButton, NoticeCard, useLocale } from "@k2b/ui";
2
+ import { createSignal, createUniqueId, For, onCleanup, onMount, Show } from "solid-js";
3
+ import type { AppAccessRule } from "../contracts/app-access";
4
+ import type { AccessEntry } from "../contracts/shared";
5
+ import type { AppAccessChange, AppAccessSnapshot } from "../services/app-access-management";
6
+ import { AppAccessClientError, appAccessAdapter } from "./app-access-adapter";
7
+ import { appAccessMessages } from "./app-access-messages";
8
+ import PermissionEditor, { type GrantableLevel } from "./PermissionEditor";
9
+ import { principalKey } from "./PrincipalPicker";
10
+ import { principalLabel } from "./principal-label";
11
+
12
+ export type AppAccessEditorProps = {
13
+ app: { id: string; name: string; access?: AppAccessRule };
14
+ initial?: AppAccessSnapshot;
15
+ repair?: boolean;
16
+ /** Return to the current page so its normal authorization renders the new state. */
17
+ onAccessLost?: () => void;
18
+ };
19
+
20
+ /** Application authors place this editor in their settings; Core uses it unchanged for explicit repair. */
21
+ export default function AppAccessEditor(props: AppAccessEditorProps) {
22
+ const locale = useLocale(),
23
+ t = () => appAccessMessages.resolve([locale()]).t;
24
+ const descriptionId = createUniqueId();
25
+ const adapter = appAccessAdapter(props.app.id, { repair: props.repair });
26
+ const [snapshot, setSnapshot] = createSignal(props.initial);
27
+ const [loading, setLoading] = createSignal(!props.initial);
28
+ // A problem's optional lead sentence is set in bold, as in the approved notices.
29
+ const [error, setError] = createSignal<{ lead?: string; text: string }>();
30
+ const [blocked, setBlocked] = createSignal(false);
31
+ const [forbidden, setForbidden] = createSignal(false);
32
+ const [showInherited, setShowInherited] = createSignal(true);
33
+ let section: HTMLElement | undefined;
34
+ const controller = new AbortController();
35
+ onCleanup(() => controller.abort());
36
+ /** `revoked` is false when only managing ended and use is confirmed, so the reloaded page shows no removal notice. */
37
+ const lost = (revoked = true) => {
38
+ if (!controller.signal.aborted)
39
+ (
40
+ props.onAccessLost ??
41
+ (() => {
42
+ const url = new URL(window.location.href);
43
+ if (revoked) url.searchParams.set("_cloud_access", "revoked");
44
+ else url.searchParams.delete("_cloud_access");
45
+ window.location.replace(url.href);
46
+ })
47
+ )();
48
+ };
49
+ const load = async (trigger?: HTMLButtonElement) => {
50
+ const hadFocus = trigger !== undefined && document.activeElement === trigger;
51
+ setLoading(true);
52
+ try {
53
+ const fresh = await adapter.read(controller.signal);
54
+ const restoreFocus = hadFocus && (document.activeElement === trigger || document.activeElement === document.body);
55
+ setSnapshot(fresh);
56
+ setError(undefined);
57
+ setBlocked(false);
58
+ if (restoreFocus)
59
+ queueMicrotask(() => {
60
+ if (!controller.signal.aborted && document.activeElement === document.body)
61
+ (
62
+ section?.querySelector<HTMLElement>('input[role="combobox"]') ?? section?.querySelector<HTMLElement>("button:not(:disabled)")
63
+ )?.focus();
64
+ });
65
+ } catch (failure) {
66
+ if (!controller.signal.aborted) {
67
+ if (failure instanceof AppAccessClientError && failure.status === 403) {
68
+ setForbidden(true);
69
+ setSnapshot(undefined);
70
+ setError({ text: t().forbidden });
71
+ } else setError({ lead: t().loadFailedLead, text: t().loadFailed });
72
+ }
73
+ } finally {
74
+ if (!controller.signal.aborted) setLoading(false);
75
+ }
76
+ };
77
+ onMount(() => {
78
+ if (!props.initial && props.app.access?.kind === "grants") void load();
79
+ });
80
+ const errorText = (error: AppAccessClientError, change: AppAccessChange) =>
81
+ error.code === "LAST_MANAGER"
82
+ ? t().lastManager
83
+ : error.status === 403
84
+ ? t().forbidden
85
+ : error.code === "CONFLICT" && change.action === "grant"
86
+ ? t().alreadyGranted
87
+ : error.code === "CONFLICT" || error.code === "NOT_FOUND"
88
+ ? t().listChanged
89
+ : t().saveFailed;
90
+
91
+ const perform = async (change: AppAccessChange) => {
92
+ if (blocked()) throw new Error(t().uncertain);
93
+ let result: { entry?: AccessEntry; canUse: boolean };
94
+ try {
95
+ result = await adapter.change(change, controller.signal);
96
+ } catch (failure) {
97
+ if (failure instanceof AppAccessClientError && failure.status < 500) {
98
+ if (failure.status === 403) {
99
+ setForbidden(true);
100
+ setSnapshot(undefined);
101
+ setError({ text: t().forbidden });
102
+ } else if (failure.status === 400 || failure.status === 404 || failure.status === 409) {
103
+ // The same request cannot succeed again: offer the canonical list instead of a retry.
104
+ try {
105
+ setSnapshot(await adapter.read(controller.signal));
106
+ setError({ text: errorText(failure, change) });
107
+ } catch (readFailure) {
108
+ if (readFailure instanceof AppAccessClientError && readFailure.status === 403) {
109
+ setSnapshot(undefined);
110
+ setForbidden(true);
111
+ lost();
112
+ }
113
+ setError({ text: t().uncertain });
114
+ }
115
+ setBlocked(true);
116
+ }
117
+ throw new Error(errorText(failure, change));
118
+ }
119
+ // A lost response can follow a commit. Read the canonical list before offering another write.
120
+ let fresh: AppAccessSnapshot;
121
+ try {
122
+ fresh = await adapter.read(controller.signal);
123
+ } catch (readFailure) {
124
+ if (readFailure instanceof AppAccessClientError && readFailure.status === 403) {
125
+ setSnapshot(undefined);
126
+ setForbidden(true);
127
+ setError({ text: t().forbidden });
128
+ lost();
129
+ throw new Error(t().forbidden);
130
+ }
131
+ setBlocked(true);
132
+ setError({ text: t().uncertain });
133
+ throw new Error(t().uncertain);
134
+ }
135
+ setSnapshot(fresh);
136
+ const entry =
137
+ change.action === "grant"
138
+ ? fresh.entries.find((entry) => principalKey(entry.principal) === principalKey(change.principal))
139
+ : fresh.entries.find((entry) => entry.id === change.accessId);
140
+ const applied = change.action === "revoke" ? !entry : entry?.permission === change.permission;
141
+ if (!applied) throw new Error(t().saveFailed);
142
+ result = { entry, canUse: fresh.canUse };
143
+ }
144
+ if (!result.canUse && !props.repair) lost();
145
+ else {
146
+ try {
147
+ setSnapshot(await adapter.read(controller.signal));
148
+ } catch (failure) {
149
+ // Use is confirmed here; only managing this list ended.
150
+ if (failure instanceof AppAccessClientError && failure.status === 403) lost(false);
151
+ else {
152
+ setBlocked(true);
153
+ setError({ text: t().savedRefreshFailed });
154
+ }
155
+ }
156
+ }
157
+ return result;
158
+ };
159
+ const beforeChange = async (entry: AccessEntry, next: GrantableLevel | null) => {
160
+ if (props.repair || !snapshot()?.ownEntryIds.includes(entry.id) || (next !== null && entry.permission !== "admin")) return true;
161
+ const own =
162
+ snapshot()?.entries.filter((candidate) => candidate.id !== entry.id && snapshot()?.ownEntryIds.includes(candidate.id)) ?? [];
163
+ const losesUse = next === null && own.length === 0;
164
+ const losesManagement = !own.some((candidate) => candidate.permission === "admin");
165
+ // Name only managers the platform counts, so an empty group or an expired account is never offered as the rescue.
166
+ const others = (snapshot()?.entries ?? [])
167
+ .filter((candidate) => candidate.id !== entry.id && (candidate.effectiveManager ?? candidate.permission === "admin"))
168
+ .map((candidate) => principalLabel(candidate, locale()));
169
+ const remaining = t().remainingManagers({
170
+ managers: new Intl.ListFormat(locale(), { type: "conjunction" }).format(others),
171
+ count: others.length,
172
+ });
173
+ return (
174
+ (await dialogCore.open<boolean>(
175
+ (close) => (
176
+ <div class="mx-auto w-full max-w-[440px] p-5 text-primary max-md:pb-[calc(2rem+env(safe-area-inset-bottom,0px))]">
177
+ <h2 class="mb-2 text-[17px] font-semibold">
178
+ {entry.principal.type === "authenticated"
179
+ ? t().removeAudienceTitle
180
+ : next === null
181
+ ? t().removeSelfTitle
182
+ : t().changeSelfTitle}
183
+ </h2>
184
+ <p id={descriptionId} class="mb-4 text-[15px] text-secondary">
185
+ {(entry.principal.type === "authenticated"
186
+ ? t().removeAudience
187
+ : losesUse
188
+ ? t().removeSelf
189
+ : losesManagement
190
+ ? t().removeSelfManagement
191
+ : next === null
192
+ ? t().removeSelfEntry
193
+ : t().changeSelfEntry)({ name: props.app.name })}{" "}
194
+ {remaining}
195
+ </p>
196
+ <div class="flex justify-end gap-2">
197
+ <Button variant="secondary" style={{ "min-height": "36px", "font-size": "15px" }} onClick={() => close(false)}>
198
+ {t().cancel}
199
+ </Button>
200
+ <Button variant="danger" style={{ "min-height": "36px", "font-size": "15px" }} onClick={() => close(true)}>
201
+ {next === null ? t().remove : t().change}
202
+ </Button>
203
+ </div>
204
+ </div>
205
+ ),
206
+ {
207
+ ...bottomSheetAdaptiveOptions,
208
+ role: "alertdialog",
209
+ ariaDescribedBy: descriptionId,
210
+ panelClassName: `${bottomSheetAdaptiveOptions.panelClassName} md:!w-[440px] !border-0`,
211
+ // Without ariaLabel, the dialog is labelled by its heading.
212
+ signal: controller.signal,
213
+ history: true,
214
+ },
215
+ )) === true
216
+ );
217
+ };
218
+ const fixedDescription = () => {
219
+ switch (props.app.access?.kind) {
220
+ case "resources":
221
+ return t().resources;
222
+ case "signedIn":
223
+ return t().fixedSignedIn;
224
+ case "noGuests":
225
+ return t().fixedNoGuests;
226
+ case "roles":
227
+ return props.app.access.roles.length === 1 && props.app.access.roles[0] === "admin" ? t().fixedAdmins : t().fixedRoles;
228
+ default:
229
+ return t().legacy;
230
+ }
231
+ };
232
+ return (
233
+ <section ref={section} class="w-full max-w-[720px]" aria-label={t().access}>
234
+ <h2 class="mb-1 text-[15px] font-semibold">{t().whoUses({ name: props.app.name })}</h2>
235
+ <Show
236
+ when={props.app.access?.kind === "grants"}
237
+ fallback={<p class="max-w-[60ch] text-[15px] text-secondary">{fixedDescription()}</p>}
238
+ >
239
+ <p class="mb-4 max-w-[60ch] text-[15px] text-secondary">{t().description({ name: props.app.name })}</p>
240
+ <div class="flex flex-col gap-4">
241
+ <Show when={props.repair}>
242
+ <NoticeCard tone="info" icon="ti ti-shield-check">
243
+ <p>{t().repairNotice({ name: props.app.name })}</p>
244
+ </NoticeCard>
245
+ </Show>
246
+ <Show when={props.repair && snapshot() && !snapshot()!.hasEffectiveManager}>
247
+ <NoticeCard tone="warning" icon="ti ti-alert-triangle">
248
+ <p>
249
+ <strong class="font-semibold">{t().noManagerLead}</strong> {t().noManager}
250
+ </p>
251
+ </NoticeCard>
252
+ </Show>
253
+ {/* Repair already explains itself; the transition note is for the app's own managers. */}
254
+ <Show when={!props.repair && showInherited() && snapshot()?.inheritedSignedIn}>
255
+ <NoticeCard tone="info" icon="ti ti-info-circle">
256
+ <div class="flex items-start gap-2">
257
+ <p class="flex-1">{t().inherited({ name: props.app.name })}</p>
258
+ <IconButton
259
+ variant="ghost"
260
+ size="sm"
261
+ class="!text-inherit"
262
+ onClick={() => {
263
+ setShowInherited(false);
264
+ // The button leaves with its notice; the search is where editing continues.
265
+ queueMicrotask(() => section?.querySelector<HTMLElement>('input[role="combobox"]')?.focus());
266
+ }}
267
+ label={t().closeNotice}
268
+ >
269
+ <i class="ti ti-x text-[18px]" />
270
+ </IconButton>
271
+ </div>
272
+ </NoticeCard>
273
+ </Show>
274
+ <Show when={error()}>
275
+ {(problem) => (
276
+ <NoticeCard tone="danger" icon="ti ti-alert-circle" role="alert">
277
+ <div class="flex flex-wrap items-start gap-2">
278
+ <p class="flex-1">
279
+ <Show when={problem().lead}>
280
+ {(lead) => (
281
+ <>
282
+ <strong class="font-semibold">{lead()}</strong>{" "}
283
+ </>
284
+ )}
285
+ </Show>
286
+ {problem().text}
287
+ </p>
288
+ <Show when={!forbidden()}>
289
+ <Button
290
+ variant="secondary"
291
+ style={{ "min-height": "36px", "font-size": "15px" }}
292
+ loading={loading()}
293
+ onClick={(event) => void load(event.currentTarget)}
294
+ >
295
+ {t().reload}
296
+ </Button>
297
+ </Show>
298
+ </div>
299
+ </NoticeCard>
300
+ )}
301
+ </Show>
302
+ <Show
303
+ when={snapshot()}
304
+ fallback={
305
+ <Show when={loading()}>
306
+ <div aria-busy="true" class="flex flex-col gap-1">
307
+ <For each={[0, 1, 2, 3]}>
308
+ {() => (
309
+ // The same grid as a loaded row, so the list keeps its place when the entries arrive.
310
+ <div class="grid min-h-[52px] grid-cols-[32px_minmax(0,1fr)_36px] items-center gap-x-3 gap-y-1.5 py-2 sm:flex sm:px-2 sm:py-1.5">
311
+ <span class="h-8 w-8 animate-pulse rounded-full bg-[var(--k2b-surface-muted)]" />
312
+ <span class="flex min-w-0 flex-1 flex-col gap-1.5">
313
+ <span class="h-3.5 w-2/5 animate-pulse rounded bg-[var(--k2b-surface-muted)]" />
314
+ <span class="h-3 w-1/4 animate-pulse rounded bg-[var(--k2b-surface-muted)]" />
315
+ </span>
316
+ <span class="col-start-2 row-start-2 h-9 animate-pulse rounded-[var(--ui-radius-control)] bg-[var(--k2b-surface-muted)] sm:w-44 sm:flex-none" />
317
+ <span class="w-9 shrink-0" />
318
+ </div>
319
+ )}
320
+ </For>
321
+ </div>
322
+ </Show>
323
+ }
324
+ >
325
+ {(state) => (
326
+ <PermissionEditor
327
+ expanded
328
+ initialEntries={state().entries}
329
+ effectiveEntries={(entries) =>
330
+ entries.filter((entry) => state().entries.find((candidate) => candidate.id === entry.id)?.effectiveManager !== false)
331
+ }
332
+ // Everyone matches all signed-in people, so that row is nobody's own marker.
333
+ ownEntryIds={state().ownEntryIds.filter((id) =>
334
+ state().entries.some((entry) => entry.id === id && entry.principal.type !== "authenticated"),
335
+ )}
336
+ placeholder={t().add}
337
+ signedIn={{ label: t().signedIn, description: t().signedInDescription }}
338
+ entryDetail={(entry) => {
339
+ const count = state().entries.find((candidate) => candidate.id === entry.id)?.groupMemberCount;
340
+ return entry.principal.type === "group" && count !== undefined ? t().group({ count }) : undefined;
341
+ }}
342
+ canEdit={!blocked()}
343
+ allowPublic={false}
344
+ allowAuthenticated
345
+ allowServiceAccounts
346
+ beforeChange={beforeChange}
347
+ allowedLevels={(principal) => {
348
+ const use = { level: "read" as const, label: t().use };
349
+ if (principal.type === "authenticated") return [use];
350
+ const manage = { level: "admin" as const, label: t().manage };
351
+ // New entries start at the first level; without a manager, the first change must add one.
352
+ return state().hasEffectiveManager ? [use, manage] : [manage, use];
353
+ }}
354
+ grantAccess={async (principal, permission) => {
355
+ if (permission !== "read" && permission !== "admin") throw new Error("Unsupported app permission");
356
+ const result = await perform({ action: "grant", principal, permission });
357
+ if (!result.entry) throw new Error(t().uncertain);
358
+ return result.entry;
359
+ }}
360
+ updateAccess={async (accessId, permission) => {
361
+ if (permission !== "read" && permission !== "admin") throw new Error("Unsupported app permission");
362
+ await perform({ action: "update", accessId, permission });
363
+ }}
364
+ revokeAccess={async (accessId) => {
365
+ await perform({ action: "revoke", accessId });
366
+ }}
367
+ />
368
+ )}
369
+ </Show>
370
+ </div>
371
+ <dl class="mt-6 grid gap-x-4 gap-y-1 text-sm sm:grid-cols-[max-content_1fr]">
372
+ <dt class="font-medium">{t().use}</dt>
373
+ <dd class="mb-2 text-secondary sm:mb-0">{t().useDescription}</dd>
374
+ <dt class="font-medium">{t().manage}</dt>
375
+ <dd class="text-secondary">{t().manageDescription}</dd>
376
+ </dl>
377
+ </Show>
378
+ </section>
379
+ );
380
+ }
@@ -1,4 +1,4 @@
1
- import { LocaleProvider } from "@k2b/ui";
1
+ import { LocaleProvider, prompts } from "@k2b/ui";
2
2
  import { createSignal, onCleanup, Show } from "solid-js";
3
3
  import { render } from "solid-js/web";
4
4
  import type { AccessEntry } from "../contracts/shared";
@@ -6,6 +6,8 @@ import PermissionEditor from "./PermissionEditor";
6
6
 
7
7
  const host = document.getElementById("root");
8
8
  if (!host) throw new Error("Missing harness root");
9
+ const confirmation = new URLSearchParams(location.search).get("confirmation");
10
+ const nextEvent = (name: string) => new Promise<void>((resolve) => document.addEventListener(name, () => resolve(), { once: true }));
9
11
  render(() => {
10
12
  let saved: AccessEntry[] = ["Mara Lind", "Lea Sommer", "Jonas Weber"].map((displayName, index) => ({
11
13
  id: `access-${index}`,
@@ -32,6 +34,21 @@ render(() => {
32
34
  {(entries) => (
33
35
  <PermissionEditor
34
36
  initialEntries={entries()}
37
+ beforeChange={
38
+ confirmation
39
+ ? async () => {
40
+ document.documentElement.dataset.confirming = "true";
41
+ if (confirmation === "delayed") await nextEvent("open-access-confirmation");
42
+ const confirmed = await prompts.confirm("Change this person's access?", { title: "Confirm access", history: true });
43
+ // Keep the caller pending until the test has observed the dialog's focus-return frame.
44
+ const settled = nextEvent("settle-access-confirmation");
45
+ document.documentElement.dataset.confirmationClosed = "true";
46
+ await settled;
47
+ delete document.documentElement.dataset.confirming;
48
+ return confirmed === true;
49
+ }
50
+ : undefined
51
+ }
35
52
  // Equal-width labels isolate pending-state layout from a normal change in label width.
36
53
  allowedLevels={[
37
54
  { level: "read", label: "Level 1" },
@@ -43,6 +60,7 @@ render(() => {
43
60
  }}
44
61
  updateAccess={(id, permission) =>
45
62
  new Promise<void>((resolve) => {
63
+ document.documentElement.dataset.writes = String(Number(document.documentElement.dataset.writes ?? 0) + 1);
46
64
  // Existing tests use this mark and event to control the save.
47
65
  document.documentElement.dataset.saving = "true";
48
66
  document.addEventListener(
@@ -56,7 +74,9 @@ render(() => {
56
74
  );
57
75
  })
58
76
  }
59
- revokeAccess={async () => {}}
77
+ revokeAccess={async () => {
78
+ document.documentElement.dataset.writes = String(Number(document.documentElement.dataset.writes ?? 0) + 1);
79
+ }}
60
80
  />
61
81
  )}
62
82
  </Show>