@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
package/src/cli/apps.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type CloudCliContext, command, defineCliCommands, flag } from "./index";
1
+ import { AppDetailsSchema } from "../contracts/app-details";
2
+ import { arg, type CloudCliContext, command, defineCliCommands, flag, localizeCloudCliText, printRows, printStructured } from "./index";
2
3
 
3
4
  type VisibleApp = {
4
5
  id: string;
@@ -13,28 +14,90 @@ const listApps = async (ctx: CloudCliContext, search: string | undefined) => {
13
14
  return ctx.readJson<{ items: VisibleApp[] }>(await ctx.fetch(`/api/apps${query}`));
14
15
  };
15
16
 
16
- export default defineCliCommands({
17
- name: "apps",
18
- summary: "List Cloud apps available to the current user.",
19
- commands: [
20
- command("list", {
21
- summary: "List available Cloud apps",
22
- flags: {
23
- search: flag.string({ aliases: ["q"], description: "Filter by app name, id, or description" }),
24
- },
25
- run: async ({ ctx, flags }) => {
26
- const result = await listApps(ctx, flags.search);
27
- if (ctx.options.output === "json") {
28
- ctx.json(result);
29
- return;
30
- }
31
- ctx.table(result.items, [
32
- { key: "id", label: "ID" },
33
- { key: "name", label: "NAME" },
34
- { key: "description", label: "DESCRIPTION" },
35
- { key: "href", label: "URL" },
36
- ]);
37
- },
38
- }),
39
- ],
40
- });
17
+ const build = (locale?: string) => {
18
+ const t = (en: string, de: string) => localizeCloudCliText(locale, { en, de });
19
+ return defineCliCommands({
20
+ name: "apps",
21
+ summary: t("List Cloud apps and inspect their declarations.", "Cloud-Apps auflisten und ihre Deklarationen prüfen."),
22
+ commands: [
23
+ command("show", {
24
+ summary: t(
25
+ "Inspect one app's access, events and notifications (admin)",
26
+ "Zugriff, Events und Benachrichtigungen einer App prüfen (Admin)",
27
+ ),
28
+ args: { app: arg.required({ description: t("App ID", "App-ID") }) },
29
+ run: async ({ ctx, args }) => {
30
+ const result = AppDetailsSchema.parse(
31
+ await ctx.readJson(await ctx.fetch(`/api/app-events/apps/${encodeURIComponent(args.app)}`)),
32
+ );
33
+ if (printStructured(ctx, result)) return;
34
+ const { app, access } = result;
35
+ const printOmitted = (shown: number, total: number) => {
36
+ if (total > shown) ctx.print(t(`… ${total - shown} more omitted`, `… ${total - shown} weitere ausgelassen`));
37
+ };
38
+ ctx.print(
39
+ `${t("App", "Anwendung")}: ${app.name} (${app.id}) · ${app.online ? t("online", "erreichbar") : t("offline", "nicht erreichbar")}`,
40
+ );
41
+ if (app.description) ctx.print(app.description);
42
+ ctx.print(`\n${t("Access", "Zugriff")}: ${access.kind ?? t("unknown (offline)", "unbekannt (offline)")}`);
43
+ if (access.roles.length) ctx.print(`${t("Roles", "Rollen")}: ${access.roles.join(", ")}`);
44
+ if (access.grantCount !== null) ctx.print(`${t("Grants", "Freigaben")}: ${access.grantCount}`);
45
+ ctx.print(`${t("Platform permissions", "Plattformrechte")}: ${access.platformPermissions.join(", ") || "—"}`);
46
+ ctx.print(`\n${t("Sends", "Sendet")} (${result.sends.length}/${result.totals.sends})`);
47
+ for (const event of result.sends) {
48
+ ctx.print(`${event.type} v${event.version} · ${event.title} · ${event.subscriberCount} ${t("subscribers", "Abonnenten")}`);
49
+ ctx.print(
50
+ ` ${event.subject}${event.place ? ` @ ${event.place}` : ""} · ${t("automation", "Automatisierung")}=${event.automation ? t("yes", "ja") : t("no", "nein")}`,
51
+ );
52
+ }
53
+ printOmitted(result.sends.length, result.totals.sends);
54
+ ctx.print(`\n${t("Hears", "Hört")} (${result.hears.length}/${result.totals.hears})`);
55
+ for (const subscription of result.hears) {
56
+ ctx.print(
57
+ `${subscription.eventType} v${subscription.version} · ${subscription.state}${subscription.isNew ? ` · ${t("new", "neu")}` : ""} · ${subscription.counts.failed} ${t("failed", "fehlgeschlagen")}`,
58
+ );
59
+ ctx.print(` ${subscription.subscriptionId}${subscription.reason ? ` · ${subscription.reason}` : ""}`);
60
+ for (const failure of subscription.failedDeliveries)
61
+ ctx.print(
62
+ ` ${failure.id} · ${failure.code ?? t("failed", "fehlgeschlagen")} · ${failure.attempts} ${t("attempts", "Versuche")}`,
63
+ );
64
+ }
65
+ printOmitted(result.hears.length, result.totals.hears);
66
+ ctx.print(`\n${t("Notifications", "Benachrichtigungen")} (${result.notifications.length}/${result.totals.notifications})`);
67
+ for (const notification of result.notifications) {
68
+ ctx.print(`${notification.id} · ${notification.label} · ${notification.recipientKind}`);
69
+ ctx.print(
70
+ ` ${t("Required", "Erforderlich")}: ${notification.requiredChannels.join(", ") || "—"}; ${t("recommended", "empfohlen")}: ${notification.recommendedChannels.join(", ") || "—"}; ${t("excluded", "ausgeschlossen")}: ${notification.excludedChannels.join(", ") || "—"}`,
71
+ );
72
+ }
73
+ printOmitted(result.notifications.length, result.totals.notifications);
74
+ },
75
+ }),
76
+ command("list", {
77
+ summary: t("List available Cloud apps", "Verfügbare Cloud-Apps auflisten"),
78
+ flags: {
79
+ search: flag.string({
80
+ aliases: ["q"],
81
+ description: t("Filter by app name, id, or description", "Nach App-Name, ID oder Beschreibung filtern"),
82
+ }),
83
+ },
84
+ run: async ({ ctx, flags }) => {
85
+ const result = await listApps(ctx, flags.search);
86
+ printRows(ctx, result, result.items, [
87
+ { key: "id", label: "ID" },
88
+ { key: "name", label: "NAME" },
89
+ { key: "description", label: t("DESCRIPTION", "BESCHREIBUNG") },
90
+ { key: "href", label: "URL" },
91
+ ]);
92
+ },
93
+ }),
94
+ ],
95
+ });
96
+ };
97
+
98
+ const appsCliModule = build();
99
+ export default {
100
+ ...appsCliModule,
101
+ help: (locale?: string) => build(locale).help?.(locale) ?? "",
102
+ run: (ctx: CloudCliContext) => build(ctx.options.locale).run(ctx),
103
+ };
@@ -0,0 +1,64 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { type AvatarConfig, PersonAvatarSchema, RobotAvatarSchema } from "../contracts/avatar";
3
+ import { MAX_AVATAR_BYTES } from "../contracts/profile";
4
+
5
+ /** What `GET …/avatar` answers for a person or a service account. */
6
+ export type CliAvatarState = {
7
+ kind: "photo" | "built" | "initials";
8
+ built: AvatarConfig | null;
9
+ hasPhoto: boolean;
10
+ photo: string | null;
11
+ avatarHash: string | null;
12
+ };
13
+
14
+ /** One row per fact: which picture shows, whether a photo is stored, then every part of the built avatar. */
15
+ export const avatarRows = (state: CliAvatarState): Array<{ key: string; value: string }> => [
16
+ { key: "shows", value: state.kind === "built" ? (state.built?.kind === "robot" ? "robot" : "avatar") : state.kind },
17
+ { key: "photo", value: state.hasPhoto ? "stored" : "none" },
18
+ ...Object.entries(state.built ?? {})
19
+ .filter(([key]) => key !== "v" && key !== "kind")
20
+ .map(([key, value]) => ({ key, value: String(value) })),
21
+ ];
22
+
23
+ /**
24
+ * Applies `part=value` pairs to a built avatar and validates the result against its shape set. An unknown part or
25
+ * value names what is allowed, so a person or an agent can correct the call without guessing.
26
+ */
27
+ export const applyAvatarParts = <T extends AvatarConfig>(base: T, parts: readonly string[]): T => {
28
+ if (parts.length === 0) throw new Error("Pass at least one part=value, for example hair=curly.");
29
+ const schema = base.kind === "person" ? PersonAvatarSchema : RobotAvatarSchema;
30
+ const next: Record<string, unknown> = { ...base };
31
+ for (const part of parts) {
32
+ const [key, value, ...rest] = part.split("=");
33
+ if (!key || value === undefined || rest.length > 0) throw new Error(`Expected part=value, got "${part}".`);
34
+ const field = schema.shape[key as keyof typeof schema.shape];
35
+ if (!field || key === "v" || key === "kind") {
36
+ const known = Object.keys(schema.shape).filter((name) => name !== "v" && name !== "kind");
37
+ throw new Error(`Unknown part "${key}". Parts: ${known.join(", ")}.`);
38
+ }
39
+ const allowed = "options" in field ? (field.options as readonly string[]) : [];
40
+ if (!allowed.includes(value)) throw new Error(`Unknown ${key} "${value}". Choose one of: ${allowed.join(", ")}.`);
41
+ next[key] = value;
42
+ }
43
+ return schema.parse(next) as T;
44
+ };
45
+
46
+ const imageType = (bytes: Uint8Array): "png" | "jpeg" | "webp" | null => {
47
+ if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) return "png";
48
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return "jpeg";
49
+ if (String.fromCharCode(...bytes.slice(0, 4)) === "RIFF" && String.fromCharCode(...bytes.slice(8, 12)) === "WEBP") return "webp";
50
+ return null;
51
+ };
52
+
53
+ /** Reads a small square PNG, JPEG, or WebP as the data URL the API stores; the browser dialog crops, the CLI does not. */
54
+ export const readAvatarPhoto = async (file: string): Promise<string> => {
55
+ const bytes = new Uint8Array(await readFile(file));
56
+ const type = imageType(bytes);
57
+ if (!type) throw new Error("The photo must be a PNG, JPEG, or WebP file.");
58
+ if (bytes.length > MAX_AVATAR_BYTES) {
59
+ throw new Error(
60
+ `The photo is ${Math.ceil(bytes.length / 1024)} KiB; the limit is ${MAX_AVATAR_BYTES / 1024} KiB. Crop and shrink it first.`,
61
+ );
62
+ }
63
+ return `data:image/${type};base64,${Buffer.from(bytes).toString("base64")}`;
64
+ };
@@ -16,6 +16,7 @@ import {
16
16
  cloudResourceRefAppId,
17
17
  resolveCapabilityResourceReader,
18
18
  } from "../contracts/capabilities";
19
+ import { ResolvedCloudResourceUrlSchema } from "../contracts/resource-resolution";
19
20
  import { saveCapabilityStream } from "./capability-streams";
20
21
  import { arg, type CliInputFlagValue, type CloudCliContext, command, defineCliCommands, flag, readCliInput } from "./index";
21
22
  import { cloudResourceUrl } from "./resource-links";
@@ -139,7 +140,7 @@ const invoke = async (config: {
139
140
  }
140
141
  throw error;
141
142
  }
142
- const result = capabilityResultSchema(z.unknown()).parse(body);
143
+ const result = capabilityResultSchema(z.unknown(), { consumer: true }).parse(body);
143
144
  const manifests = config.manifest ? [config.manifest] : [];
144
145
  const wantedApps = new Set((result.refs ?? []).map(cloudResourceRefAppId));
145
146
  for (const manifest of manifests) wantedApps.delete(manifest.appId);
@@ -179,6 +180,22 @@ export default defineCliCommands({
179
180
  name: "capabilities",
180
181
  summary: "Discover and invoke versioned Cloud app capabilities.",
181
182
  commands: [
183
+ command("resolve", {
184
+ summary: "Recognize a Cloud link and read its resource reference",
185
+ args: { url: arg.required({ description: "Cloud permalink or application page URL" }) },
186
+ run: async ({ ctx, args }) => {
187
+ const response = await ctx.fetch("/api/resources/resolve", {
188
+ method: "POST",
189
+ headers: { "content-type": "application/json" },
190
+ body: JSON.stringify({ url: args.url.trim() }),
191
+ });
192
+ if (!response.ok) throw new Error("Resource not found.");
193
+ const body = await readBoundedJson(response, CAPABILITY_MAX_RESULT_BYTES);
194
+ if (!body.ok) throw new Error("Resource not found.");
195
+ const result = ResolvedCloudResourceUrlSchema.parse(body.data);
196
+ printGenericResult(ctx, { ...result, permalink: cloudResourceUrl(ctx, result.ref) });
197
+ },
198
+ }),
182
199
  ...(["read", "write", "status", "abort"] as const).map((verb) =>
183
200
  command(`stream-${verb}`, {
184
201
  summary: `${verb} an authenticated binary capability stream`,
@@ -247,6 +264,8 @@ export default defineCliCommands({
247
264
  types: app.manifest.types.length,
248
265
  queries: app.manifest.queries.length,
249
266
  actions: app.manifest.actions.length,
267
+ events: app.manifest.events?.length ?? 0,
268
+ subscribes: app.manifest.subscribes?.length ?? 0,
250
269
  })),
251
270
  [
252
271
  { key: "app", label: "APP" },
@@ -254,6 +273,8 @@ export default defineCliCommands({
254
273
  { key: "types", label: "TYPES" },
255
274
  { key: "queries", label: "QUERIES" },
256
275
  { key: "actions", label: "ACTIONS" },
276
+ { key: "events", label: "EVENTS" },
277
+ { key: "subscribes", label: "SUBSCRIBES" },
257
278
  ],
258
279
  );
259
280
  if (result.page.hasMore) ctx.error(`More apps available; continue with --cursor ${result.page.nextCursor}`);
@@ -0,0 +1,320 @@
1
+ import { link, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { z } from "zod";
5
+ import { assertAppEventSize } from "../_internal/app-event-schemas";
6
+ import { APP_EVENT_MAX_BYTES, AppEventEnvelopeSchema, appEventSubscriptionId } from "../contracts/app-events";
7
+ import { CapabilityQualifiedIdSchema } from "../contracts/capabilities";
8
+ import { arg, command, confirmFlag, defineCliCommands, flag } from "./commands";
9
+ import type { CloudCliContext } from "./index";
10
+ import { localizeCloudCliText } from "./locale";
11
+ import { printRows, printStructured } from "./output";
12
+
13
+ const API = "/api/app-events";
14
+ const catalogSchema = z.object({
15
+ events: z.array(
16
+ z.object({
17
+ type: CapabilityQualifiedIdSchema,
18
+ version: z.number().int().positive(),
19
+ title: z.string(),
20
+ description: z.string(),
21
+ subject: CapabilityQualifiedIdSchema,
22
+ place: CapabilityQualifiedIdSchema.nullable(),
23
+ automation: z.boolean(),
24
+ dataSchema: z.record(z.string(), z.unknown()),
25
+ subscribers: z.array(z.unknown()),
26
+ }),
27
+ ),
28
+ });
29
+
30
+ const read = async <T>(ctx: CloudCliContext, path: string, init?: RequestInit) => ctx.readJson<T>(await ctx.fetch(`${API}${path}`, init));
31
+ const catalog = async (ctx: CloudCliContext) => catalogSchema.parse(await read(ctx, "/catalog"));
32
+
33
+ // Consume one bounded envelope at a time, including when the input has no newline.
34
+ async function* jsonLines(stream: ReadableStream<Uint8Array>, locale?: string, maxBytes?: number) {
35
+ const t = (en: string, de: string) => localizeCloudCliText(locale, { en, de });
36
+ const reader = stream.getReader();
37
+ let pending = new Uint8Array(0);
38
+ let line = 0;
39
+ let bytesRead = 0;
40
+ const decode = (bytes: Uint8Array) => {
41
+ line++;
42
+ try {
43
+ const envelope = AppEventEnvelopeSchema.parse(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
44
+ assertAppEventSize(envelope);
45
+ return envelope;
46
+ } catch (cause) {
47
+ throw new Error(t(`Invalid event fixture at line ${line}.`, `Ungültige Event-Fixture in Zeile ${line}.`), { cause });
48
+ }
49
+ };
50
+ try {
51
+ while (true) {
52
+ const { done, value } = await reader.read();
53
+ if (done) break;
54
+ bytesRead += value.byteLength;
55
+ if (maxBytes !== undefined && bytesRead > maxBytes) {
56
+ throw new Error(
57
+ t("Fixture response exceeds the requested recording budget.", "Die Fixture-Antwort überschreitet das Aufnahmebudget."),
58
+ );
59
+ }
60
+ let start = 0;
61
+ for (let index = 0; index <= value.length; index++) {
62
+ if (index < value.length && value[index] !== 10) continue;
63
+ const part = value.subarray(start, index);
64
+ if (pending.length + part.length > APP_EVENT_MAX_BYTES) {
65
+ throw new Error(
66
+ t(
67
+ `Event fixture line ${line + 1} exceeds ${APP_EVENT_MAX_BYTES} bytes.`,
68
+ `Event-Fixture in Zeile ${line + 1} überschreitet ${APP_EVENT_MAX_BYTES} Bytes.`,
69
+ ),
70
+ );
71
+ }
72
+ const next = new Uint8Array(pending.length + part.length);
73
+ next.set(pending);
74
+ next.set(part, pending.length);
75
+ pending = next;
76
+ if (index < value.length) {
77
+ if (pending.length) yield decode(pending);
78
+ else line++;
79
+ pending = new Uint8Array(0);
80
+ }
81
+ start = index + 1;
82
+ }
83
+ }
84
+ if (pending.length) yield decode(pending);
85
+ } finally {
86
+ await reader.cancel();
87
+ reader.releaseLock();
88
+ }
89
+ }
90
+
91
+ const build = (locale?: string) => {
92
+ const t = (en: string, de: string) => localizeCloudCliText(locale, { en, de });
93
+ const print = (ctx: CloudCliContext, value: unknown) => {
94
+ if (!printStructured(ctx, value)) ctx.print(JSON.stringify(value, null, 2));
95
+ };
96
+ return defineCliCommands({
97
+ name: "events",
98
+ summary: t("Inspect declared app events and test local handlers.", "Deklarierte App-Events prüfen und lokale Handler testen."),
99
+ commands: [
100
+ command("ls", {
101
+ summary: t("List declared event types", "Deklarierte Event-Typen auflisten"),
102
+ flags: {
103
+ app: flag.string({ description: t("Publisher application ID", "ID der veröffentlichenden Anwendung") }),
104
+ automation: flag.enum(["true", "false"], {
105
+ description: t("Filter by automation metadata", "Nach Automationsmetadaten filtern"),
106
+ }),
107
+ },
108
+ async run({ ctx, flags }) {
109
+ const result = await catalog(ctx);
110
+ const events = result.events.filter(
111
+ (event) =>
112
+ (!flags.app || event.type.startsWith(`${flags.app}.`)) &&
113
+ (flags.automation === undefined || event.automation === (flags.automation === "true")),
114
+ );
115
+ printRows(ctx, { events }, events, [
116
+ { key: "type", label: "TYPE" },
117
+ { key: "version", label: "VERSION" },
118
+ { key: "title", label: t("TITLE", "TITEL") },
119
+ { key: "subject", label: "SUBJECT" },
120
+ { key: "place", label: "PLACE", value: (event) => event.place ?? "—" },
121
+ { key: "automation", label: "AUTOMATION", value: (event) => (event.automation ? t("yes", "ja") : t("no", "nein")) },
122
+ ]);
123
+ },
124
+ }),
125
+ command("subscriptions", {
126
+ summary: t("Inspect subscriptions and blocked reasons", "Abonnements und Sperrgründe prüfen"),
127
+ flags: { app: flag.string({ description: t("Subscriber application ID", "ID der abonnierenden Anwendung") }) },
128
+ run: async ({ ctx, flags }) =>
129
+ print(ctx, await read(ctx, `/subscriptions${flags.app ? `?${new URLSearchParams({ app: flags.app })}` : ""}`)),
130
+ }),
131
+ command("status", {
132
+ summary: t("Show event broker status and rejected events", "Status des Event-Brokers und abgelehnte Events anzeigen"),
133
+ run: async ({ ctx }) => print(ctx, await read(ctx, "/status")),
134
+ }),
135
+ command("deliveries", {
136
+ summary: t("List event deliveries", "Event-Zustellungen auflisten"),
137
+ flags: {
138
+ app: flag.string({ description: t("Subscriber application ID", "ID der abonnierenden Anwendung") }),
139
+ state: flag.string({ description: t("Delivery state", "Zustellstatus") }),
140
+ eventType: flag.string({ description: t("Qualified event type", "Vollständiger Event-Typ") }),
141
+ offset: flag.int({ default: 0, min: 0, description: t("Pagination offset", "Startposition der Seite") }),
142
+ limit: flag.int({ default: 50, min: 1, max: 100, description: t("Maximum deliveries", "Maximale Anzahl Zustellungen") }),
143
+ },
144
+ async run({ ctx, flags }) {
145
+ const query = new URLSearchParams({ offset: String(flags.offset), limit: String(flags.limit) });
146
+ for (const key of ["app", "state", "eventType"] as const) if (flags[key]) query.set(key, flags[key]);
147
+ const result = await read<{
148
+ items: Record<string, unknown>[];
149
+ pagination: { offset: number; limit: number; total: number };
150
+ }>(ctx, `/deliveries?${query}`);
151
+ printRows(ctx, result, result.items, [
152
+ { key: "id", label: "ID" },
153
+ { key: "subscriptionId", label: "SUBSCRIPTION" },
154
+ { key: "state", label: t("STATE", "STATUS") },
155
+ { key: "attempts", label: t("ATTEMPTS", "VERSUCHE") },
156
+ { key: "heldBy", label: t("HELD BY", "WARTET AUF") },
157
+ ]);
158
+ },
159
+ }),
160
+ command("redeliver", {
161
+ summary: t("Redeliver one retained delivery", "Eine gespeicherte Zustellung erneut ausführen"),
162
+ args: { id: arg.required({ description: t("Delivery ID", "Zustellungs-ID") }) },
163
+ flags: { yes: confirmFlag() },
164
+ async run({ ctx, args, flags }) {
165
+ if (!flags.yes) {
166
+ throw new Error(
167
+ t(
168
+ "Pass --yes to redeliver; the handler may repeat effects.",
169
+ "Für die erneute Zustellung --yes angeben; der Handler kann Effekte wiederholen.",
170
+ ),
171
+ );
172
+ }
173
+ print(ctx, await read(ctx, `/deliveries/${encodeURIComponent(args.id)}/redeliver`, { method: "POST" }));
174
+ },
175
+ }),
176
+ command("acknowledge-gap", {
177
+ summary: t("Acknowledge a reconciled retention gap", "Eine abgeglichene Retention-Lücke bestätigen"),
178
+ flags: { yes: confirmFlag() },
179
+ async run({ ctx, flags }) {
180
+ if (!flags.yes)
181
+ throw new Error(
182
+ t("Pass --yes after reconciling affected subscribers.", "Nach dem Abgleich betroffener Abonnenten --yes angeben."),
183
+ );
184
+ print(ctx, await read(ctx, "/status/acknowledge", { method: "POST" }));
185
+ },
186
+ }),
187
+ command("record", {
188
+ summary: t("Export shape-only JSONL fixtures", "JSONL-Fixtures nur mit synthetischen Daten exportieren"),
189
+ args: { eventType: arg.required({ description: t("Qualified event type", "Vollständiger Event-Typ") }) },
190
+ flags: {
191
+ shapeOnly: flag.boolean({
192
+ description: t("Required: generate synthetic fixture data", "Erforderlich: synthetische Fixture-Daten erzeugen"),
193
+ }),
194
+ limit: flag.int({
195
+ default: 10,
196
+ min: 1,
197
+ max: 100,
198
+ description: t("Recent events to inspect for distinct shapes", "Neueste Events für unterschiedliche Strukturen prüfen"),
199
+ }),
200
+ out: flag.string({ description: t("Create a JSONL file", "JSONL-Datei erstellen") }),
201
+ },
202
+ async run({ ctx, args, flags }) {
203
+ if (!flags.shapeOnly) throw new Error(t("record requires --shape-only.", "record erfordert --shape-only."));
204
+ const query = new URLSearchParams({ shapeOnly: "true", limit: String(flags.limit) });
205
+ const response = await ctx.fetch(`${API}/record/${encodeURIComponent(args.eventType)}?${query}`);
206
+ if (!response.ok) {
207
+ await ctx.readJson(response);
208
+ return;
209
+ }
210
+ if (!response.body) throw new Error(t("Cloud returned no fixture stream.", "Cloud hat keinen Fixture-Stream geliefert."));
211
+ const fixtures = [];
212
+ for await (const envelope of jsonLines(response.body, locale, flags.limit! * (APP_EVENT_MAX_BYTES + 1))) {
213
+ if (fixtures.length >= flags.limit!) {
214
+ throw new Error(t("Cloud returned too many fixtures.", "Cloud hat zu viele Fixtures geliefert."));
215
+ }
216
+ if (envelope.type !== args.eventType) {
217
+ throw new Error(t("Cloud returned a different event type.", "Cloud hat einen anderen Event-Typ geliefert."));
218
+ }
219
+ fixtures.push(envelope);
220
+ }
221
+ if (flags.out) {
222
+ const path = resolve(flags.out);
223
+ const temporary = await mkdtemp(join(dirname(path), ".cloud-event-fixtures-"));
224
+ try {
225
+ const part = join(temporary, "fixtures.jsonl");
226
+ await writeFile(part, fixtures.map((event) => JSON.stringify(event)).join("\n") + (fixtures.length ? "\n" : ""), {
227
+ mode: 0o600,
228
+ });
229
+ await link(part, path);
230
+ } finally {
231
+ await rm(temporary, { recursive: true, force: true });
232
+ }
233
+ print(ctx, { path, count: fixtures.length });
234
+ } else if (ctx.options.output === "json") ctx.json(fixtures);
235
+ else for (const fixture of fixtures) await ctx.write(`${JSON.stringify(fixture)}\n`);
236
+ },
237
+ }),
238
+ command("replay", {
239
+ summary: t("Replay fixtures into a local handler", "Fixtures an einen lokalen Handler übergeben"),
240
+ args: { fixtures: arg.required({ description: t("JSONL fixture file", "JSONL-Fixture-Datei") }) },
241
+ flags: {
242
+ handler: flag.string({
243
+ required: true,
244
+ description: t("Local module path[#export], default export by default", "Lokaler Modulpfad[#export], sonst Default-Export"),
245
+ }),
246
+ },
247
+ async run({ ctx, args, flags }) {
248
+ const current = await catalog(ctx);
249
+ const declarations = new Map(current.events.map((event) => [event.type, event]));
250
+ const validators = new Map<string, z.ZodType>();
251
+ const [modulePath, exportName = "default", ...extra] = flags.handler!.split("#");
252
+ if (!modulePath || !exportName || extra.length) {
253
+ throw new Error(t("Use a local path[#export] for --handler.", "Für --handler einen lokalen Pfad[#export] angeben."));
254
+ }
255
+ const module: Record<string, unknown> = await import(pathToFileURL(resolve(modulePath)).href);
256
+ const handler = module[exportName];
257
+ if (typeof handler !== "function") {
258
+ throw new Error(t("The selected handler export must be a function.", "Der gewählte Handler-Export muss eine Funktion sein."));
259
+ }
260
+ let delivered = 0;
261
+ try {
262
+ for await (const envelope of jsonLines(Bun.file(resolve(args.fixtures)).stream(), locale)) {
263
+ const event = declarations.get(envelope.type);
264
+ if (!event || event.version !== envelope.version) {
265
+ throw new Error(
266
+ t(
267
+ `Event ${envelope.type}@${envelope.version} is absent from the current catalog.`,
268
+ `Event ${envelope.type}@${envelope.version} fehlt im aktuellen Katalog.`,
269
+ ),
270
+ );
271
+ }
272
+ if (
273
+ envelope.source !== envelope.type.split(".")[0] ||
274
+ envelope.subject.type !== event.subject ||
275
+ (envelope.place?.type ?? null) !== event.place
276
+ ) {
277
+ throw new Error(
278
+ t(
279
+ `Event ${envelope.type} references do not match the current catalog.`,
280
+ `Referenzen von Event ${envelope.type} passen nicht zum aktuellen Katalog.`,
281
+ ),
282
+ );
283
+ }
284
+ let validator = validators.get(envelope.type);
285
+ if (!validator) {
286
+ validator = z.fromJSONSchema(structuredClone(event.dataSchema));
287
+ validators.set(envelope.type, validator);
288
+ }
289
+ validator.parse(envelope.data);
290
+ await handler(envelope, {
291
+ subscriptionId: appEventSubscriptionId("replay", envelope.type, envelope.version),
292
+ eventId: envelope.id,
293
+ attempt: 1,
294
+ signal: new AbortController().signal,
295
+ });
296
+ delivered++;
297
+ }
298
+ } catch (cause) {
299
+ const detail = cause instanceof Error ? cause.message : String(cause);
300
+ throw new Error(
301
+ t(
302
+ `Replay stopped after ${delivered} completed fixtures: ${detail}`,
303
+ `Replay wurde nach ${delivered} abgeschlossenen Fixtures angehalten: ${detail}`,
304
+ ),
305
+ { cause },
306
+ );
307
+ }
308
+ print(ctx, { delivered });
309
+ },
310
+ }),
311
+ ],
312
+ });
313
+ };
314
+
315
+ const eventsCliModule = build();
316
+ export default {
317
+ ...eventsCliModule,
318
+ help: (locale?: string) => build(locale).help?.(locale) ?? "",
319
+ run: (ctx: CloudCliContext) => build(ctx.options.locale).run(ctx),
320
+ };
package/src/cli/index.ts CHANGED
@@ -96,9 +96,9 @@ export type CloudCliModule = {
96
96
 
97
97
  export * from "./access";
98
98
  export * from "./address";
99
+ export { createAppAccessCommands } from "./app-access";
99
100
  export * from "./commands";
100
101
  export * from "./locale";
101
102
  export * from "./output";
102
103
  export * from "./plugin";
103
-
104
104
  export * from "./resource-links";
package/src/config/env.ts CHANGED
@@ -10,6 +10,17 @@ const syncReplicas = z
10
10
  .enum(["1", "3", "5"], { error: "must be 1, 3, or 5" })
11
11
  .transform((value): 1 | 3 | 5 => (value === "1" ? 1 : value === "3" ? 3 : 5));
12
12
 
13
+ const budgetSchema = z
14
+ .object({
15
+ maxStepsPerSource: z.number().int().positive(),
16
+ tasksPerMinute: z.number().int().positive(),
17
+ burst: z.number().int().positive(),
18
+ sourcesPer24Hours: z.number().int().positive(),
19
+ scopeConcurrent: z.number().int().positive(),
20
+ globalConcurrent: z.number().int().positive(),
21
+ })
22
+ .strict();
23
+
13
24
  const registry = defineEnv({
14
25
  // Process
15
26
  NODE_ENV: {
@@ -66,6 +77,35 @@ const registry = defineEnv({
66
77
  doc: "Only dial the listed servers and ignore cluster-advertised peer addresses; set to `false` when every advertised address is reachable.",
67
78
  },
68
79
 
80
+ WORKFLOW_AI_BUDGET: {
81
+ schema: z
82
+ .string()
83
+ .transform((value, ctx) => {
84
+ try {
85
+ return JSON.parse(value);
86
+ } catch {
87
+ ctx.addIssue({ code: "custom", message: "must be JSON" });
88
+ return z.NEVER;
89
+ }
90
+ })
91
+ .pipe(budgetSchema),
92
+ default: { maxStepsPerSource: 2, tasksPerMinute: 10, burst: 20, sourcesPer24Hours: 2_000, scopeConcurrent: 1, globalConcurrent: 4 },
93
+ doc: "JSON scoped workflow inference budget: active or successful logical requests per source/24h, token-bucket admission per minute and burst, distinct sources per rolling 24h, outstanding tasks per scope and globally; capacity/rate overload uses bounded workflow retries, daily/source limits fail visibly, and cache hits consume no reservation.",
94
+ },
95
+
96
+ // Workflow inference: p95 ~4s in the pilot; 60s bounds a whole attempt,
97
+ // including admission and structured-output repair, not just stream idleness.
98
+ WORKFLOW_AI_ATTEMPT_TIMEOUT_MS: {
99
+ schema: z.coerce.number().int().positive(),
100
+ default: 60_000,
101
+ doc: "Absolute workflow AI attempt budget in milliseconds, including admission and repair; expiry aborts the provider and follows the bounded retry policy.",
102
+ },
103
+ WORKFLOW_AI_QUEUE_TIMEOUT_MS: {
104
+ schema: z.coerce.number().int().positive(),
105
+ default: 60_000,
106
+ doc: "Maximum workflow AI queue wait in milliseconds; recovery fails overdue or lost deliveries visibly so an operator can retry the workflow.",
107
+ },
108
+
69
109
  // Installation
70
110
  APP_URL: {
71
111
  schema: z.string(),