@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,429 @@
1
+ /** Fictional identity and same-origin transport; actual Cloud shell, editor, denial page and built UI. */
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createConfig } from "@k2b/ssr";
7
+ import tailwind from "bun-plugin-tailwind";
8
+ import { Hono } from "hono";
9
+ import { createComponent } from "solid-js";
10
+ import { Dynamic, renderToString } from "solid-js/web";
11
+ import type { AppMeta, CloudRuntime, RuntimeAppMeta } from "../contracts/app";
12
+ import type { AccessEntry, User } from "../contracts/shared";
13
+ import type { AuthContext, RequestAuthority } from "../server/middleware/auth";
14
+ import type { AppAccessChange, AppAccessSnapshot } from "../services/app-access-management";
15
+ import { preloadDisplayApps } from "../ssr/runtime";
16
+ import type { AppAccessEditorProps } from "./AppAccessEditor";
17
+
18
+ export const fixtureApp = {
19
+ id: "invoices",
20
+ name: "Rechnungen",
21
+ icon: "ti ti-receipt",
22
+ appearance: { accent: "#b45309" },
23
+ access: { kind: "grants" },
24
+ } satisfies Pick<AppMeta, "id" | "name" | "icon" | "appearance" | "access">;
25
+ /** The approved mockup's list: two managers, a group with its member count and a standalone service account. */
26
+ export const fixtureSnapshot = (mode = "normal"): AppAccessSnapshot => {
27
+ const snapshot: AppAccessSnapshot = {
28
+ appId: fixtureApp.id,
29
+ name: fixtureApp.name,
30
+ configured: true,
31
+ hasEffectiveManager: true,
32
+ inheritedSignedIn: false,
33
+ canUse: true,
34
+ ownEntryIds: ["00000001-aaaa-4aaa-8aaa-aaaaaaaaaaaa"],
35
+ entries: [
36
+ {
37
+ id: "00000001-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
38
+ principal: { type: "user", userId: "11111111-1111-4111-8111-111111111111" },
39
+ displayName: "Anna Muster",
40
+ permission: "admin",
41
+ effectiveManager: true,
42
+ createdAt: "2026-10-22T12:00:00.000Z",
43
+ },
44
+ {
45
+ id: "00000002-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
46
+ principal: { type: "user", userId: "22222222-2222-4222-8222-222222222222" },
47
+ displayName: "Eva Beispiel",
48
+ permission: "admin",
49
+ effectiveManager: true,
50
+ createdAt: "2026-10-22T12:00:00.000Z",
51
+ },
52
+ {
53
+ id: "00000003-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
54
+ principal: { type: "group", groupId: "33333333-3333-4333-8333-333333333333" },
55
+ displayName: "Buchhaltung",
56
+ permission: "read",
57
+ groupMemberCount: 4,
58
+ effectiveManager: false,
59
+ createdAt: "2026-10-22T12:00:00.000Z",
60
+ },
61
+ {
62
+ id: "00000004-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
63
+ principal: { type: "service_account", serviceAccountId: "44444444-4444-4444-8444-444444444444" },
64
+ displayName: "Monatsabschluss",
65
+ serviceAccountKind: "standalone",
66
+ permission: "read",
67
+ effectiveManager: false,
68
+ createdAt: "2026-10-22T12:00:00.000Z",
69
+ },
70
+ ],
71
+ };
72
+ // The service lists the transition's signed-in entry first and counts it as everyone's own.
73
+ const inherited = {
74
+ id: "inherited-authenticated",
75
+ principal: { type: "authenticated" },
76
+ permission: "read",
77
+ createdAt: "1970-01-01T00:00:00.000Z",
78
+ } as const;
79
+ if (mode === "denied-revoked") {
80
+ snapshot.entries = snapshot.entries.filter((entry) => !snapshot.ownEntryIds.includes(entry.id));
81
+ snapshot.ownEntryIds = [];
82
+ snapshot.canUse = false;
83
+ }
84
+ // The approved repair scene: an app in transition whose only entry is everyone signed in, so no one manages it.
85
+ if (mode === "no-manager") {
86
+ snapshot.entries = [{ ...inherited }];
87
+ snapshot.ownEntryIds = [inherited.id];
88
+ snapshot.hasEffectiveManager = false;
89
+ snapshot.inheritedSignedIn = true;
90
+ snapshot.configured = false;
91
+ }
92
+ if (mode === "sole-manager" || mode === "sole-effective-manager")
93
+ snapshot.entries = snapshot.entries.filter((entry) => entry.id !== "00000002-aaaa-4aaa-8aaa-aaaaaaaaaaaa");
94
+ if (mode === "sole-effective-manager") {
95
+ for (const entry of snapshot.entries) {
96
+ if (entry.principal.type === "group" || entry.principal.type === "service_account") {
97
+ entry.permission = "admin";
98
+ entry.effectiveManager = false;
99
+ if (entry.principal.type === "group") entry.groupMemberCount = 0;
100
+ }
101
+ }
102
+ }
103
+ if (mode === "inherited") {
104
+ snapshot.inheritedSignedIn = true;
105
+ snapshot.configured = false;
106
+ snapshot.entries.unshift({ ...inherited });
107
+ snapshot.ownEntryIds.push(inherited.id);
108
+ }
109
+ if (mode === "self-group") {
110
+ snapshot.entries[0] = {
111
+ ...snapshot.entries[0]!,
112
+ principal: { type: "group", groupId: "66666666-6666-4666-8666-666666666666" },
113
+ displayName: "Finanzteam",
114
+ groupMemberCount: 3,
115
+ };
116
+ }
117
+ return snapshot;
118
+ };
119
+ export const fixtureEntity = {
120
+ kind: "user",
121
+ user: {
122
+ id: "55555555-5555-4555-8555-555555555555",
123
+ uid: "00000005-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
124
+ displayName: "Lea Fiktiv",
125
+ mail: "lea@example.test",
126
+ avatarHash: null,
127
+ },
128
+ };
129
+
130
+ const user: User = {
131
+ id: "11111111-1111-4111-8111-111111111111",
132
+ uid: "00000001-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
133
+ displayName: "Anna Muster",
134
+ givenname: "Anna",
135
+ sn: "Muster",
136
+ mail: "anna@example.test",
137
+ roles: ["admin", "user", "local", "local/user"],
138
+ provider: "local",
139
+ profile: "user",
140
+ avatarHash: null,
141
+ accountExpires: null,
142
+ lastLoginLocal: null,
143
+ memberofGroup: [],
144
+ memberofGroupIds: [],
145
+ manages: [],
146
+ managesGroupIds: [],
147
+ ipa: null,
148
+ };
149
+
150
+ // Discovery uses real fixed-rule resolution, so it needs neither a database nor a mocked authorization result.
151
+ // The editor below still uses fictional grant transport; it is not an enforcement fixture.
152
+ const discoveryApps: RuntimeAppMeta[] = [
153
+ {
154
+ id: "home",
155
+ name: "Start",
156
+ icon: "ti ti-home",
157
+ description: "Fictional home",
158
+ routes: ["/fixture"],
159
+ access: { kind: "signedIn" },
160
+ nav: { href: "/fixture", section: "primary" },
161
+ },
162
+ {
163
+ id: "invoices",
164
+ name: "Rechnungen",
165
+ icon: "ti ti-receipt",
166
+ description: "Fictional invoices",
167
+ routes: ["/fictional/invoices"],
168
+ access: { kind: "roles", roles: ["user"] },
169
+ nav: { href: "/fictional/invoices", section: "primary" },
170
+ searchLinks: [{ label: "Offene Belege", href: "/fictional/invoices/open", keywords: ["Buchhaltung"] }],
171
+ presentation: {
172
+ baseLocale: "de",
173
+ translations: { en: { name: "Invoices", searchLinks: { "/fictional/invoices/open": "Open receipts" } } },
174
+ },
175
+ },
176
+ {
177
+ id: "hidden",
178
+ name: "Geheimarchiv",
179
+ icon: "ti ti-lock",
180
+ description: "Fictional hidden app",
181
+ routes: ["/fictional/hidden"],
182
+ access: { kind: "roles", roles: ["guest"] },
183
+ nav: { href: "/fictional/hidden", section: "primary" },
184
+ searchLinks: [{ label: "Verborgene Belege", href: "/fictional/hidden/receipts", keywords: ["Buchhaltung"] }],
185
+ },
186
+ ];
187
+
188
+ export async function appAccessBrowserFixture() {
189
+ const root = mkdtempSync(join(tmpdir(), "cloud-app-access-browser-"));
190
+ process.once("exit", () => rmSync(root, { recursive: true, force: true }));
191
+ Bun.plugin(createConfig({ dev: true, rootDir: root }).plugin());
192
+ const [{ default: AppAccessEditor }, { AppAccessDeniedContent, AppAccessDeniedFrame }, { default: Layout }, { default: AdminLayout }] =
193
+ await Promise.all([
194
+ import("./AppAccessEditor"),
195
+ import("../ssr/AppAccessDenied"),
196
+ import("../ssr/Layout"),
197
+ import("../ssr/AdminLayout"),
198
+ ]);
199
+ const { default: AppAccessBrowserFrame, AppAccessRepairFrame } = await import("./AppAccessEditor.browser-frame");
200
+ const ui = resolve(import.meta.dir, "../../../ui");
201
+ const { transformAsync } = await import(Bun.resolveSync("@babel/core", `${ui}/`));
202
+ const typescript = (await import(Bun.resolveSync("@babel/preset-typescript", `${ui}/`))).default;
203
+ const solid = (await import(Bun.resolveSync("babel-preset-solid", `${ui}/`))).default;
204
+ const bundle = await Bun.build({
205
+ entrypoints: [resolve(import.meta.dir, "AppAccessEditor.browser-harness.tsx")],
206
+ target: "browser",
207
+ conditions: ["browser"],
208
+ format: "iife",
209
+ plugins: [
210
+ {
211
+ name: "solid-app-access-fixture",
212
+ setup(builder) {
213
+ builder.onLoad({ filter: /\.tsx$/ }, async ({ path }) => {
214
+ const result = await transformAsync(await Bun.file(path).text(), {
215
+ filename: path,
216
+ babelrc: false,
217
+ configFile: false,
218
+ presets: [typescript, [solid, { generate: "dom", hydratable: false }]],
219
+ });
220
+ if (!result?.code) throw new Error(`Could not compile ${path}`);
221
+ return { contents: result.code, loader: "js" };
222
+ });
223
+ },
224
+ },
225
+ ],
226
+ });
227
+ if (!bundle.success) throw new AggregateError(bundle.logs, "App-access browser harness build failed");
228
+ const script = await bundle.outputs[0]!.text();
229
+ const styles = await Bun.build({ entrypoints: [resolve(import.meta.dir, "../../../../styles.css")], plugins: [tailwind] });
230
+ if (!styles.success) throw new AggregateError(styles.logs, "App-access stylesheet build failed");
231
+ const css = await styles.outputs[0]!.text();
232
+ // Serve the standalone UI presets directly, as the MinimalLayout browser fixture does.
233
+ const uiDist = dirname(fileURLToPath(import.meta.resolve("@k2b/ui/fonts/plex.css")));
234
+ const icons = await Bun.file(resolve(uiDist, "tabler.css")).text();
235
+ const iconFont = /tabler-icons-[\w-]+\.woff2/.exec(icons)?.[0];
236
+ if (!iconFont) throw new Error("Tabler preset is missing its font asset");
237
+ const stylesheets: Record<string, string> = {
238
+ "/public/fonts.css": (await Bun.file(resolve(uiDist, "plex.css")).text()).replaceAll("./fonts/", "/public/fonts/"),
239
+ "/public/tabler-icons.css": icons.replace(/\.\/tabler-icons-[\w-]+\.woff2(\?[^)]*)?/, "/public/tabler-icons.woff2"),
240
+ };
241
+ const pages = new Hono<{
242
+ Variables: {
243
+ runtime: CloudRuntime;
244
+ user: User;
245
+ settings: Record<string, unknown>;
246
+ actor: RequestAuthority["actor"];
247
+ accessSubject: RequestAuthority["accessSubject"];
248
+ credentialKind: AuthContext["Variables"]["credentialKind"];
249
+ credentialScopes: string[];
250
+ panelConsent: undefined;
251
+ railPreferences: undefined;
252
+ announcements: undefined;
253
+ page: { title: string; theme: "light" | "dark" };
254
+ };
255
+ }>();
256
+ const sessions = new Map<string, AppAccessSnapshot>();
257
+ let sessionId = 0;
258
+ // Repair renders where Core mounts it, so the administration navigation marks Apps as the current page.
259
+ pages.use("/fixture", async (c, next) => {
260
+ const mode = c.req.query("state");
261
+ if (mode !== "repair" && mode !== "no-manager") return next();
262
+ const url = new URL(c.req.url);
263
+ url.pathname = "/admin/apps";
264
+ url.searchParams.set("app", fixtureApp.id);
265
+ return c.redirect(`${url.pathname}${url.search}`, 302);
266
+ });
267
+ pages.on("GET", ["/fixture", "/admin/apps"], async (c) => {
268
+ const mode = c.req.query("state") ?? "normal";
269
+ const theme = c.req.query("theme") === "dark" ? "dark" : "light";
270
+ const locale = c.req.query("locale") === "en" ? "en" : "de";
271
+ c.req.raw.headers.set("x-cloud-locale", locale);
272
+ // Keep the request's theme and the embedded shell preference coherent.
273
+ c.req.raw.headers.set("cookie", `${c.req.raw.headers.get("cookie") ?? ""}; theme=${theme}`);
274
+ const discovery = mode.startsWith("discovery-");
275
+ const identity: User = { ...user, roles: mode === "discovery-denied" ? ["admin"] : user.roles };
276
+ const authority: RequestAuthority = {
277
+ actor: { kind: "user", user: identity },
278
+ accessSubject: { type: "user", userId: identity.id },
279
+ credentialKind: "session",
280
+ scopes: [],
281
+ };
282
+ c.set("actor", authority.actor);
283
+ c.set("accessSubject", authority.accessSubject);
284
+ c.set("credentialKind", authority.credentialKind);
285
+ c.set("credentialScopes", authority.scopes);
286
+ c.set("runtime", {
287
+ apps: discovery
288
+ ? discoveryApps
289
+ : [
290
+ {
291
+ ...fixtureApp,
292
+ // Shell visibility is synthetic here; grant enforcement is tested against real storage separately.
293
+ access: mode.startsWith("denied") || mode === "no-manager" ? { kind: "roles", roles: ["guest"] } : { kind: "signedIn" },
294
+ description: "Fictional browser fixture",
295
+ routes: ["/fixture"],
296
+ nav: { href: "/fixture", section: "primary" },
297
+ },
298
+ ],
299
+ });
300
+ c.set("user", identity);
301
+ await preloadDisplayApps(c);
302
+ c.set("settings", { app: { name: "Cloud", locale } });
303
+ c.set("page", { title: discovery ? "Cloud" : "Rechnungen · Zugriff", theme });
304
+ const initial = fixtureSnapshot(mode);
305
+ const repair = mode === "repair" || mode === "no-manager";
306
+ const props: AppAccessEditorProps = {
307
+ app:
308
+ mode === "fixed"
309
+ ? { ...fixtureApp, access: { kind: "roles", roles: ["admin"] } }
310
+ : mode === "inherited" || mode === "no-manager"
311
+ ? { ...fixtureApp, access: { kind: "grants", until: { kind: "signedIn" } } }
312
+ : fixtureApp,
313
+ repair,
314
+ ...(mode === "load" ? {} : { initial }),
315
+ };
316
+ const id = String(++sessionId);
317
+ sessions.set(id, initial);
318
+ const body = renderToString(() =>
319
+ createComponent(repair ? AdminLayout : Layout, {
320
+ c,
321
+ title: discovery ? "Cloud" : mode.startsWith("denied") ? fixtureApp.name : "Rechnungen · Zugriff",
322
+ fullWidth: true,
323
+ fullPage: true,
324
+ ...(repair ? { contentClass: "px-4 pt-2 pb-24 sm:px-8 sm:pt-5 sm:pb-12" } : {}),
325
+ get children() {
326
+ if (discovery) return createComponent(Dynamic, { component: "div", id: "app-access-discovery-fixture" });
327
+ if (mode.startsWith("denied"))
328
+ return createComponent(AppAccessDeniedFrame, {
329
+ get children() {
330
+ return createComponent(AppAccessDeniedContent, {
331
+ app: mode === "denied-fixed" ? { ...fixtureApp, access: { kind: "roles", roles: ["admin"] } } : fixtureApp,
332
+ // The service names only effective managers on this page.
333
+ managers: mode === "denied-fixed" ? [] : initial.entries.filter((entry) => entry.effectiveManager),
334
+ canRepair: mode === "denied-admin",
335
+ revoked: mode === "denied-revoked",
336
+ });
337
+ },
338
+ });
339
+ const editor = () =>
340
+ createComponent(Dynamic, {
341
+ component: "div",
342
+ id: "app-access-fixture",
343
+ get children() {
344
+ return createComponent(AppAccessEditor, props);
345
+ },
346
+ });
347
+ return repair
348
+ ? createComponent(AppAccessRepairFrame, {
349
+ name: fixtureApp.name,
350
+ get children() {
351
+ return editor();
352
+ },
353
+ })
354
+ : createComponent(AppAccessBrowserFrame, {
355
+ get children() {
356
+ return editor();
357
+ },
358
+ });
359
+ },
360
+ }),
361
+ );
362
+ c.header("Set-Cookie", `app-access-fixture=${id}; Path=/; SameSite=Lax`);
363
+ return c.html(
364
+ `<!doctype html><html lang="${locale}" class="${theme}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/public/fonts.css"><link rel="stylesheet" href="/public/tabler-icons.css"><style>${css}</style></head><body class="k2b-ui">${body}${mode.startsWith("denied") ? "" : `<script type="application/json" id="app-access-fixture-data">${JSON.stringify(discovery ? {} : props)}</script><script src="/fixture.js"></script>`}</body></html>`,
365
+ );
366
+ });
367
+ const server = Bun.serve({
368
+ hostname: "127.0.0.1",
369
+ port: 0,
370
+ async fetch(request) {
371
+ const url = new URL(request.url);
372
+ if (url.pathname === "/branding/logo")
373
+ return new Response(
374
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="9" fill="#18181b"/><text x="16" y="22" text-anchor="middle" font-family="sans-serif" font-size="19" fill="white">C</text></svg>',
375
+ { headers: { "Content-Type": "image/svg+xml" } },
376
+ );
377
+ if (url.pathname === "/fixture.js") return new Response(script, { headers: { "Content-Type": "text/javascript" } });
378
+ if (stylesheets[url.pathname]) return new Response(stylesheets[url.pathname], { headers: { "Content-Type": "text/css" } });
379
+ if (url.pathname === "/public/tabler-icons.woff2") return new Response(Bun.file(resolve(uiDist, iconFont)));
380
+ if (url.pathname.startsWith("/public/fonts/")) {
381
+ const fontsDir = resolve(uiDist, "fonts");
382
+ const path = resolve(fontsDir, url.pathname.slice("/public/fonts/".length));
383
+ return path.startsWith(`${fontsDir}/`) ? new Response(Bun.file(path)) : new Response(null, { status: 404 });
384
+ }
385
+ if (url.pathname === "/api/accounts/entities" && request.method === "GET")
386
+ return Response.json({
387
+ items: fixtureEntity.user.displayName.toLowerCase().includes((url.searchParams.get("search") ?? "").toLowerCase())
388
+ ? [fixtureEntity]
389
+ : [],
390
+ });
391
+ if (url.pathname === "/_cloud/app-access/invoices") {
392
+ const id = request.headers.get("cookie")?.match(/(?:^|;\s*)app-access-fixture=(\d+)/)?.[1];
393
+ const state = id ? sessions.get(id) : undefined;
394
+ if (!state) return Response.json({ code: "FIXTURE_SESSION_MISSING" }, { status: 400 });
395
+ if (request.method === "GET") return Response.json(state);
396
+ if (request.method === "POST") {
397
+ const change: AppAccessChange = await request.json();
398
+ let entry: AccessEntry | undefined;
399
+ if (change.action === "grant") {
400
+ entry = {
401
+ id: `99999999-aaaa-4aaa-8aaa-${String(state.entries.length).padStart(12, "0")}`,
402
+ principal: change.principal,
403
+ permission: change.permission,
404
+ displayName: fixtureEntity.user.displayName,
405
+ createdAt: "2026-10-22T12:00:00.000Z",
406
+ };
407
+ state.entries.push(entry);
408
+ } else if (change.action === "update") {
409
+ entry = state.entries.find((candidate) => candidate.id === change.accessId);
410
+ if (entry) entry.permission = change.permission;
411
+ } else state.entries = state.entries.filter((entry) => entry.id !== change.accessId);
412
+ state.hasEffectiveManager = state.entries.some((entry) => entry.permission === "admin");
413
+ return Response.json({ entry, canUse: true });
414
+ }
415
+ }
416
+ // Unexpected API requests fail closed and can never reach the live stack.
417
+ if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/_cloud/"))
418
+ return Response.json({ code: "UNEXPECTED_FIXTURE_REQUEST" }, { status: 503 });
419
+ return pages.fetch(request);
420
+ },
421
+ });
422
+ return {
423
+ url: `http://127.0.0.1:${server.port}/fixture`,
424
+ close() {
425
+ server.stop(true);
426
+ rmSync(root, { recursive: true, force: true });
427
+ },
428
+ };
429
+ }
@@ -0,0 +1,68 @@
1
+ import { AppWorkspace, IconButtonLink, useLocale, ViewTabs } from "@k2b/ui";
2
+ import type { JSX } from "solid-js";
3
+ import { appAccessMessages } from "./app-access-messages";
4
+
5
+ /** Fictional consuming application: the editor's placement belongs to its author. */
6
+ export default function AppAccessBrowserFrame(props: { children: JSX.Element }) {
7
+ return (
8
+ <AppWorkspace mobileSurface="flush" resizable={false}>
9
+ <AppWorkspace.Sidebar label="Rechnungen" defaultSize={240} resizable={false}>
10
+ <AppWorkspace.SidebarDesktop>
11
+ <AppWorkspace.SidebarBody>
12
+ <AppWorkspace.SidebarSection title="Rechnungen">
13
+ <AppWorkspace.SidebarItem href="#open" icon="ti ti-inbox" title="Offen">
14
+ Offen
15
+ </AppWorkspace.SidebarItem>
16
+ <AppWorkspace.SidebarItem href="#paid" icon="ti ti-circle-check" title="Bezahlt">
17
+ Bezahlt
18
+ </AppWorkspace.SidebarItem>
19
+ <AppWorkspace.SidebarItem href="#drafts" icon="ti ti-file" title="Entwürfe">
20
+ Entwürfe
21
+ </AppWorkspace.SidebarItem>
22
+ </AppWorkspace.SidebarSection>
23
+ </AppWorkspace.SidebarBody>
24
+ <AppWorkspace.SidebarSection>
25
+ <AppWorkspace.SidebarItem href="#settings" icon="ti ti-settings" title="Einstellungen" active>
26
+ Einstellungen
27
+ </AppWorkspace.SidebarItem>
28
+ </AppWorkspace.SidebarSection>
29
+ </AppWorkspace.SidebarDesktop>
30
+ </AppWorkspace.Sidebar>
31
+ <AppWorkspace.Content>
32
+ <AppWorkspace.Main class="px-4 pt-2 pb-24 sm:px-8 sm:pt-5 sm:pb-12">
33
+ <div class="mb-5">
34
+ <ViewTabs
35
+ label="Einstellungen"
36
+ current="access"
37
+ items={[
38
+ { id: "general", label: "Allgemein", href: "#general" },
39
+ { id: "numbers", label: "Nummernkreise", href: "#numbers" },
40
+ { id: "access", label: "Zugriff", href: "#access" },
41
+ ]}
42
+ />
43
+ </div>
44
+ {props.children}
45
+ </AppWorkspace.Main>
46
+ </AppWorkspace.Content>
47
+ </AppWorkspace>
48
+ );
49
+ }
50
+
51
+ /** Core's repair page header (packages/core/src/pages/admin/apps/page.tsx), so captures show the real placement. */
52
+ export function AppAccessRepairFrame(props: { name: string; children: JSX.Element }) {
53
+ const locale = useLocale(),
54
+ t = () => appAccessMessages.resolve([locale()]).t;
55
+ return (
56
+ <div class="flex flex-col gap-5">
57
+ <div class="flex items-center gap-3">
58
+ <IconButtonLink href="/admin/apps" variant="ghost" style={{ width: "36px", "min-height": "36px" }} label={t().backToApps}>
59
+ <i class="ti ti-arrow-left" />
60
+ </IconButtonLink>
61
+ <h1 class="text-lg font-semibold sm:text-xl">
62
+ {props.name} · {t().access}
63
+ </h1>
64
+ </div>
65
+ {props.children}
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,65 @@
1
+ import { LocaleProvider } from "@k2b/ui";
2
+ import { render } from "solid-js/web";
3
+ import CloudShell from "../ssr/CloudShell.island";
4
+ import ShellRuntime from "../ssr/ShellRuntime.island";
5
+ import AppAccessEditor, { type AppAccessEditorProps } from "./AppAccessEditor";
6
+
7
+ // The editor transport is synthetic. Discovery instead receives the real, server-filtered shell payload.
8
+ const data = document.querySelector<HTMLScriptElement>("#app-access-fixture-data");
9
+ if (!data) throw new Error("Missing app-access fixture data");
10
+ const root = document.querySelector<HTMLElement>("#app-access-fixture");
11
+ const discovery = document.querySelector<HTMLElement>("#app-access-discovery-fixture");
12
+ if (discovery) {
13
+ // Keep Layout's SSR frames and phone navigation; mount the same interactive shell components inside them.
14
+ const rail = document.querySelector<HTMLElement>(".layout-rail-navigation");
15
+ const header = document.querySelector<HTMLElement>(".layout-header");
16
+ if (!rail || !header) throw new Error("Missing discovery shell frames");
17
+ const railHost = document.createElement("div");
18
+ railHost.className = "contents";
19
+ rail.replaceWith(railHost);
20
+ header.replaceChildren();
21
+ render(
22
+ () => (
23
+ <LocaleProvider locale={document.documentElement.lang}>
24
+ <CloudShell slot="rail" currentPath={`${location.pathname}${location.search}`} />
25
+ </LocaleProvider>
26
+ ),
27
+ railHost,
28
+ );
29
+ render(
30
+ () => (
31
+ <LocaleProvider locale={document.documentElement.lang}>
32
+ <CloudShell slot="header" authenticated launchpad title="Cloud" />
33
+ </LocaleProvider>
34
+ ),
35
+ header,
36
+ );
37
+ render(
38
+ () => (
39
+ <LocaleProvider locale={document.documentElement.lang}>
40
+ {/* This fixture verifies shell navigation search; federated provider authorization has separate server tests. */}
41
+ <ShellRuntime commands={{ searchResources: false }} />
42
+ </LocaleProvider>
43
+ ),
44
+ discovery,
45
+ );
46
+ discovery.dataset.ready = "true";
47
+ } else {
48
+ if (!root) throw new Error("Missing app-access editor mount");
49
+ const props: AppAccessEditorProps = JSON.parse(data.textContent!);
50
+ root.replaceChildren();
51
+ render(
52
+ () => (
53
+ <LocaleProvider locale={document.documentElement.lang}>
54
+ <AppAccessEditor
55
+ {...props}
56
+ onAccessLost={() => {
57
+ root.dataset.accessLost = "true";
58
+ }}
59
+ />
60
+ </LocaleProvider>
61
+ ),
62
+ root,
63
+ );
64
+ root.dataset.ready = "true";
65
+ }