@k2b/cloud 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/package.json +7 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/PermissionEditor.tsx +9 -3
  21. package/src/access/PrincipalPicker.tsx +14 -4
  22. package/src/access/ResourceApiKeys.tsx +3 -4
  23. package/src/access/app-access-messages.ts +6 -0
  24. package/src/account/ProfilePictureDialog.tsx +699 -0
  25. package/src/account/profile-picture-messages.ts +269 -0
  26. package/src/account/ui.ts +7 -0
  27. package/src/ai/admin.ts +3 -0
  28. package/src/ai/app-frame.ts +12 -0
  29. package/src/ai/application-tasks.ts +101 -0
  30. package/src/ai/assistant-models.ts +126 -64
  31. package/src/ai/capabilities.ts +19 -4
  32. package/src/ai/chart-block.ts +63 -30
  33. package/src/ai/chat/chart-block.tsx +12 -3
  34. package/src/ai/chat/composer-adapter.ts +8 -7
  35. package/src/ai/chat/message-actions.tsx +1 -1
  36. package/src/ai/chat/message-utils.ts +2 -2
  37. package/src/ai/chat/presentation.tsx +22 -1
  38. package/src/ai/chat-quotas.ts +5 -4
  39. package/src/ai/chat-tasks.ts +2 -2
  40. package/src/ai/client/controller.ts +11 -10
  41. package/src/ai/client/projection.ts +3 -3
  42. package/src/ai/compaction.ts +3 -3
  43. package/src/ai/credentials.ts +13 -114
  44. package/src/ai/default-tool-contracts.ts +4 -3
  45. package/src/ai/dictation-runtime.ts +4 -4
  46. package/src/ai/enrich.ts +2 -2
  47. package/src/ai/executor.ts +31 -22
  48. package/src/ai/http.ts +17 -29
  49. package/src/ai/index.ts +11 -5
  50. package/src/ai/inference-calls.ts +5 -4
  51. package/src/ai/maintenance.ts +1 -1
  52. package/src/ai/memory-learning-runs.ts +5 -5
  53. package/src/ai/memory-learning.ts +3 -3
  54. package/src/ai/message-queue.ts +1 -2
  55. package/src/ai/migrate.ts +75 -42
  56. package/src/ai/model-access-migrate.ts +3 -17
  57. package/src/ai/model-access.ts +44 -85
  58. package/src/ai/model-card.tsx +134 -0
  59. package/src/ai/model-catalog.ts +359 -0
  60. package/src/ai/model-chooser-messages.ts +88 -0
  61. package/src/ai/model-chooser.tsx +577 -0
  62. package/src/ai/model-delete.ts +234 -0
  63. package/src/ai/model-fixture.ts +39 -0
  64. package/src/ai/model-icon.tsx +36 -0
  65. package/src/ai/model-overview.ts +53 -0
  66. package/src/ai/model-usage.ts +45 -0
  67. package/src/ai/openai-api-messages.ts +0 -4
  68. package/src/ai/openai-api.ts +26 -9
  69. package/src/ai/personal-agent.ts +1 -1
  70. package/src/ai/prefs.ts +12 -10
  71. package/src/ai/projects-routes.ts +2 -1
  72. package/src/ai/projects.ts +13 -12
  73. package/src/ai/protocol.ts +2 -2
  74. package/src/ai/provider-connections.ts +161 -0
  75. package/src/ai/provider-errors.ts +55 -6
  76. package/src/ai/provider-fetch.ts +4 -0
  77. package/src/ai/provider.ts +1 -1
  78. package/src/ai/quota-limits.ts +36 -0
  79. package/src/ai/quota-provider.ts +4 -2
  80. package/src/ai/quota-report.ts +7 -4
  81. package/src/ai/quotas.ts +19 -8
  82. package/src/ai/recent-resources.ts +6 -0
  83. package/src/ai/request-headers.ts +14 -87
  84. package/src/ai/resource-refs.ts +6 -2
  85. package/src/ai/routes.ts +17 -18
  86. package/src/ai/runtime.ts +50 -22
  87. package/src/ai/settings.ts +93 -580
  88. package/src/ai/store.ts +45 -23
  89. package/src/ai/stream.ts +1 -1
  90. package/src/ai/structured-runs.ts +2 -2
  91. package/src/ai/structured.ts +21 -44
  92. package/src/ai/task-contracts.ts +25 -5
  93. package/src/ai/task-execution.ts +15 -6
  94. package/src/ai/task-model.ts +9 -0
  95. package/src/ai/transcription.ts +27 -22
  96. package/src/ai/types.ts +50 -62
  97. package/src/ai/ui.tsx +13 -0
  98. package/src/ai/usage.ts +8 -8
  99. package/src/ai/validate.ts +8 -7
  100. package/src/ai/view-block.ts +2 -2
  101. package/src/api/accounts-entities.ts +16 -0
  102. package/src/api/admin-ai-quotas.ts +34 -56
  103. package/src/api/admin-ai-usage.ts +1 -1
  104. package/src/api/admin-ai.ts +173 -0
  105. package/src/api/admin-core-settings.ts +68 -244
  106. package/src/api/app-approval.ts +3 -1
  107. package/src/api/app-event-delivery.ts +51 -0
  108. package/src/api/app-events.ts +123 -0
  109. package/src/api/app-resource-access.ts +48 -0
  110. package/src/api/auth/account-mismatch.ts +24 -0
  111. package/src/api/auth.ts +26 -3
  112. package/src/api/capabilities.ts +28 -4
  113. package/src/api/index.ts +7 -1
  114. package/src/api/mcp.ts +175 -2
  115. package/src/api/me.ts +6 -4
  116. package/src/api/pwa-phone.ts +75 -26
  117. package/src/api/pwa.ts +34 -8
  118. package/src/api/resource-links.ts +83 -6
  119. package/src/api/resource-resolve.ts +50 -0
  120. package/src/api/search/schemas.ts +3 -1
  121. package/src/api/search.ts +9 -7
  122. package/src/app-pwa/contracts.ts +4 -0
  123. package/src/app-pwa/core-query-error.ts +20 -0
  124. package/src/app-pwa/index.ts +1 -0
  125. package/src/app-pwa/service.ts +91 -0
  126. package/src/app-pwa/transport.ts +22 -0
  127. package/src/approvals/messages.ts +2 -2
  128. package/src/browser/cloud-id.ts +86 -0
  129. package/src/browser/resource-clipboard.ts +4 -0
  130. package/src/browser/resource-links.ts +52 -0
  131. package/src/capabilities/server-invoke.ts +2 -11
  132. package/src/capabilities/testing.ts +5 -2
  133. package/src/cli/access.ts +6 -5
  134. package/src/cli/admin/ai-models.ts +199 -0
  135. package/src/cli/admin/ai-quotas.ts +6 -102
  136. package/src/cli/admin/ai-usage.ts +4 -4
  137. package/src/cli/admin/app-credentials.ts +5 -3
  138. package/src/cli/admin/index.ts +5 -2
  139. package/src/cli/apps.ts +89 -26
  140. package/src/cli/capabilities.ts +22 -1
  141. package/src/cli/events.ts +320 -0
  142. package/src/config/env.ts +40 -0
  143. package/src/contracts/app-access.ts +26 -2
  144. package/src/contracts/app-details.ts +89 -0
  145. package/src/contracts/app-events.ts +51 -0
  146. package/src/contracts/app.ts +21 -5
  147. package/src/contracts/capabilities.ts +143 -3
  148. package/src/contracts/index.ts +2 -0
  149. package/src/contracts/notification-types.ts +36 -9
  150. package/src/contracts/pwa.ts +17 -1
  151. package/src/contracts/registry.ts +6 -0
  152. package/src/contracts/resource-clipboard.ts +35 -0
  153. package/src/contracts/resource-pages.ts +140 -0
  154. package/src/contracts/resource-resolution.ts +13 -0
  155. package/src/contracts/shared.ts +4 -0
  156. package/src/contracts/user-notifications.ts +2 -0
  157. package/src/events/app-event-delivery.ts +100 -0
  158. package/src/events/app-events.testing.ts +65 -0
  159. package/src/events/app-events.ts +134 -0
  160. package/src/events/core-user-created.ts +24 -0
  161. package/src/events/index.ts +9 -0
  162. package/src/events/testing.ts +2 -0
  163. package/src/index.ts +1 -1
  164. package/src/server/api/respond.ts +8 -3
  165. package/src/server/index.ts +1 -0
  166. package/src/server/middleware/app-access.ts +1 -1
  167. package/src/server/middleware/auth.ts +21 -7
  168. package/src/server/middleware/index.ts +1 -0
  169. package/src/server/middleware/invocation.ts +2 -0
  170. package/src/server/public-storage.ts +106 -0
  171. package/src/server/services/access.ts +8 -0
  172. package/src/services/accounts/app.ts +6 -0
  173. package/src/services/accounts/entities.ts +14 -3
  174. package/src/services/accounts/groups.ts +3 -1
  175. package/src/services/accounts/identities.ts +62 -10
  176. package/src/services/accounts/users.ts +7 -6
  177. package/src/services/app-access.ts +25 -7
  178. package/src/services/app-details.ts +224 -0
  179. package/src/services/app-discovery.ts +2 -1
  180. package/src/services/app-event-shapes.ts +139 -0
  181. package/src/services/app-events/accept.ts +116 -0
  182. package/src/services/app-events/admin.ts +250 -0
  183. package/src/services/app-events/declarations.ts +187 -0
  184. package/src/services/app-events/deliver.ts +218 -0
  185. package/src/services/app-events.ts +92 -0
  186. package/src/services/config-conflict.ts +11 -0
  187. package/src/services/core-user-events.ts +1 -0
  188. package/src/services/freeipa-config.ts +9 -0
  189. package/src/services/geo/ledger.ts +28 -10
  190. package/src/services/geo/service.ts +43 -24
  191. package/src/services/identity/core-origin.ts +12 -0
  192. package/src/services/identity/invocation-actor.ts +20 -2
  193. package/src/services/identity/invocation-authority.ts +4 -0
  194. package/src/services/identity/invocation-token.ts +238 -56
  195. package/src/services/identity/issue-invocation.ts +6 -1
  196. package/src/services/image-metadata.ts +65 -1
  197. package/src/services/index.ts +13 -1
  198. package/src/services/ipa/sync.ts +6 -2
  199. package/src/services/ipa/users.ts +4 -1
  200. package/src/services/mandates/policy.ts +19 -12
  201. package/src/services/notifications/browser.ts +4 -0
  202. package/src/services/notifications/catalog.ts +5 -2
  203. package/src/services/notifications/platform.ts +7 -2
  204. package/src/services/notifications/user.ts +18 -7
  205. package/src/services/oauth-tokens.ts +28 -2
  206. package/src/services/object-store.ts +217 -0
  207. package/src/services/providers/local/users.ts +5 -0
  208. package/src/services/pwa-devices.ts +147 -37
  209. package/src/services/session/index.ts +14 -0
  210. package/src/services/settings/core-settings.ts +71 -43
  211. package/src/services/settings/index.ts +8 -0
  212. package/src/services/settings/versions.ts +26 -0
  213. package/src/shared/ai-models.ts +274 -0
  214. package/src/shared/ai-quotas.ts +2 -2
  215. package/src/shared/ai-usage.ts +2 -2
  216. package/src/shared/index.ts +2 -0
  217. package/src/shared-text/documents.ts +25 -8
  218. package/src/shared-text/log.ts +1 -1
  219. package/src/shared-text/text-diff.ts +75 -0
  220. package/src/ssr/AdminArea.tsx +131 -0
  221. package/src/ssr/AdminLayout.tsx +9 -3
  222. package/src/ssr/AdminSidebar.tsx +7 -7
  223. package/src/ssr/AppAccessDenied.tsx +2 -0
  224. package/src/ssr/AppBadge.tsx +14 -0
  225. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  226. package/src/ssr/CloudShell.island.tsx +25 -4
  227. package/src/ssr/Layout.tsx +8 -6
  228. package/src/ssr/PwaLayout.tsx +16 -1
  229. package/src/ssr/PwaPartCount.island.tsx +23 -0
  230. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  231. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  232. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  233. package/src/ssr/admin-active-link.ts +16 -0
  234. package/src/ssr/admin-navigation.ts +25 -20
  235. package/src/ssr/app-badges.ts +31 -7
  236. package/src/ssr/index.ts +8 -0
  237. package/src/ssr/page-trail-runtime.ts +3 -1
  238. package/src/ssr/platform-messages.ts +9 -1
  239. package/src/ssr/pwa-messages.ts +2 -2
  240. package/src/ssr/rail-navigation.ts +5 -1
  241. package/src/ssr/settings-draft.ts +153 -0
  242. package/src/ssr/shell-data.tsx +2 -0
  243. package/src/styles/global.css +1 -0
  244. package/src/styles/profile-picture.css +270 -0
  245. package/src/styles/utilities-navigation.css +6 -0
  246. package/src/workflows/ai/policy.ts +49 -0
  247. package/src/workflows/ai/runtime.ts +131 -21
  248. package/src/workflows/ai/store.ts +283 -35
  249. package/src/workflows/ai/types.ts +8 -2
  250. package/src/workflows/ai-actions.ts +148 -120
  251. package/src/workflows/authoring.ts +31 -0
  252. package/src/workflows/contracts.ts +1 -0
  253. package/src/workflows/definition.ts +2 -0
  254. package/src/workflows/editor.ts +15 -0
  255. package/src/workflows/index.ts +2 -1
  256. package/src/workflows/language/compiler.ts +34 -3
  257. package/src/workflows/runtime/executor.ts +132 -33
  258. package/src/workflows/runtime/index.ts +2 -0
  259. package/src/workflows/runtime/ports.ts +5 -1
  260. package/src/workflows/store/actions.ts +1 -1
  261. package/src/workflows/store/definitions.ts +5 -0
  262. package/src/workflows/store/runs.ts +5 -3
  263. package/src/workflows/store/worker.ts +7 -2
  264. package/src/ai/model-pricing.ts +0 -43
  265. package/src/ai/model-request-settings.ts +0 -98
  266. package/src/ai/settings-messages.ts +0 -129
  267. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -0,0 +1,86 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+ import { type DropdownItem, toast, useLocale } from "@k2b/ui";
3
+ import { createMemo, onMount } from "solid-js";
4
+ import type { CloudResourceRef } from "../contracts/capabilities";
5
+ import { cloudResourceHref } from "../contracts/resource-href";
6
+ import { cloudOrigin } from "./resource-links";
7
+
8
+ export const cloudIdMessages = i18n.define({
9
+ baseLocale: "en",
10
+ messages: {
11
+ en: {
12
+ copy: "Copy Cloud ID",
13
+ copied: "Cloud ID copied",
14
+ copyFailed: "Could not copy the Cloud ID",
15
+ },
16
+ de: {
17
+ copy: "Cloud-ID kopieren",
18
+ copied: "Cloud-ID kopiert",
19
+ copyFailed: "Cloud-ID konnte nicht kopiert werden",
20
+ },
21
+ },
22
+ });
23
+
24
+ type ClipboardModule = typeof import("./resource-clipboard");
25
+ // The clipboard contract validates with Zod, which stays out of an island's first load. It loads once the page is
26
+ // idle, so a copy usually writes the structured reference inside the click that asked for it.
27
+ let clipboardModule: ClipboardModule | undefined;
28
+ const loadClipboard = () => import("./resource-clipboard").then((module) => (clipboardModule = module));
29
+
30
+ /** One entry of the menu; `label` names the resource when a menu offers several, such as a message and its conversation. */
31
+ export type CloudIdTarget = { ref: CloudResourceRef; label?: string };
32
+
33
+ /**
34
+ * "Copy Cloud ID" for the "…" menu of a resource. It copies the structured reference and its `/open` permalink as
35
+ * text: Cloud paste surfaces recognize the resource, and anywhere else the text opens it after sign-in. Without a
36
+ * configured Cloud URL the menu shows nothing.
37
+ */
38
+ export const useCloudIdCopy = () => {
39
+ const locale = useLocale();
40
+ const t = createMemo(() => cloudIdMessages.resolve([locale()]).t);
41
+ onMount(() => {
42
+ if (clipboardModule || !cloudOrigin()) return;
43
+ const warm = () => void loadClipboard().catch(() => undefined);
44
+ if (typeof requestIdleCallback === "function") requestIdleCallback(warm, { timeout: 2000 });
45
+ else setTimeout(warm, 0);
46
+ });
47
+
48
+ const copy = (ref: CloudResourceRef) => {
49
+ const cloudUrl = cloudOrigin();
50
+ if (!cloudUrl) return;
51
+ const failed = () => void toast.error(t().copyFailed);
52
+ const write = (module: ClipboardModule) => {
53
+ try {
54
+ const { cloudResourceClipboard } = module;
55
+ return cloudResourceClipboard
56
+ .write({ cloudUrl, ref, fallbackText: cloudResourceClipboard.url(ref, cloudUrl) })
57
+ .then(() => void toast.success(t().copied), failed);
58
+ } catch {
59
+ failed();
60
+ }
61
+ };
62
+ if (clipboardModule) return void write(clipboardModule);
63
+ // Before the clipboard code loads, copy the permalink alone: WebKit allows a write only while the click is handled.
64
+ try {
65
+ void navigator.clipboard.writeText(new URL(cloudResourceHref(ref), cloudUrl).href).then(() => void toast.success(t().copied), failed);
66
+ } catch {
67
+ failed();
68
+ }
69
+ void loadClipboard().catch(() => undefined);
70
+ };
71
+
72
+ return {
73
+ available: () => Boolean(cloudOrigin()),
74
+ copy,
75
+ /** The last section of a resource's "…" menu; empty without a resource or a configured Cloud URL. */
76
+ section: (...targets: readonly (CloudResourceRef | CloudIdTarget | null | undefined | false)[]): DropdownItem[] => {
77
+ if (!cloudOrigin()) return [];
78
+ const items = targets.flatMap((target) => {
79
+ if (!target) return [];
80
+ const { ref, label } = "ref" in target ? target : { ref: target, label: undefined };
81
+ return [{ label: label ?? t().copy, icon: "ti ti-id", immediate: true, action: () => copy(ref) }];
82
+ });
83
+ return items.length > 0 ? [{ items }] : [];
84
+ },
85
+ };
86
+ };
@@ -3,7 +3,9 @@ import {
3
3
  CLOUD_RESOURCE_CLIPBOARD_MAX_BYTES,
4
4
  CLOUD_RESOURCE_CLIPBOARD_MIME_TYPE,
5
5
  CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT,
6
+ formatCloudResourceUrl,
6
7
  parseCloudResourceClipboard,
8
+ parseCloudResourceUrl,
7
9
  serializeCloudResourceClipboard,
8
10
  } from "../contracts/resource-clipboard";
9
11
 
@@ -65,6 +67,8 @@ export const cloudResourceClipboard = {
65
67
  webFormat: CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT,
66
68
  parse: parseCloudResourceClipboard,
67
69
  serialize: serializeCloudResourceClipboard,
70
+ url: formatCloudResourceUrl,
71
+ parseUrl: parseCloudResourceUrl,
68
72
  write: writeCloudResourceClipboard,
69
73
  read: readCloudResourceClipboard,
70
74
  } as const;
@@ -0,0 +1,52 @@
1
+ import type { CoreApiType } from "../api";
2
+ import { localCloudResourceUrl } from "../contracts/resource-pages";
3
+ import type { ResolvedCloudResourceUrl } from "../contracts/resource-resolution";
4
+ import { createApiClient } from "../server/api-client";
5
+ import { readShellData } from "../ssr/shell-data";
6
+
7
+ export type { ResolvedCloudResourceUrl } from "../contracts/resource-resolution";
8
+
9
+ /** Available on Layout and AdminLayout pages; MinimalLayout does not install shell data. */
10
+ export const cloudOrigin = (): string | null => readShellData()?.cloudOrigin ?? null;
11
+
12
+ /** Recognize one pasted Cloud link. Unsupported, unreadable, and unavailable links all return null. */
13
+ export const resolveCloudUrl = async (text: string, options: { signal?: AbortSignal } = {}): Promise<ResolvedCloudResourceUrl | null> => {
14
+ const url = text.trim();
15
+ if (!localCloudResourceUrl(url, cloudOrigin())) return null;
16
+ try {
17
+ const response = await createApiClient<CoreApiType>().resources.resolve.$post({ json: { url } }, { init: { signal: options.signal } });
18
+ if (!response.ok) return null;
19
+ // Validate only after a successful response; Zod stays out of the initial browser graph.
20
+ const { ResolvedCloudResourceUrlSchema } = await import("../contracts/resource-resolution");
21
+ const result = ResolvedCloudResourceUrlSchema.safeParse(await response.json());
22
+ return result.success ? result.data : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
27
+
28
+ // Ctrl+Shift+V (Cmd+Shift+V, also with Option, which changes the key's character) asks for plain text; that paste
29
+ // keeps the link as it is.
30
+ let plainPasteAt = -Infinity;
31
+ if (typeof window !== "undefined")
32
+ window.addEventListener(
33
+ "keydown",
34
+ (event) => {
35
+ if (event.shiftKey && (event.ctrlKey || event.metaKey) && (event.code === "KeyV" || event.key.toLowerCase() === "v"))
36
+ plainPasteAt = event.timeStamp;
37
+ },
38
+ true,
39
+ );
40
+
41
+ /**
42
+ * The Cloud link a paste carries: one absolute URL on this Cloud, as copied from the address bar or "Copy Cloud ID",
43
+ * and nothing else. Returns null for other text, other origins, and a paste as plain text. Recognize the link with
44
+ * `resolveCloudUrl()`.
45
+ */
46
+ export const pastedCloudLink = (event: ClipboardEvent): string | null => {
47
+ const plain = event.timeStamp - plainPasteAt < 1_000;
48
+ plainPasteAt = -Infinity;
49
+ const text = (event.clipboardData?.getData("text/plain") ?? "").trim();
50
+ if (plain || text.startsWith("/")) return null;
51
+ return localCloudResourceUrl(text, cloudOrigin()) ? text : null;
52
+ };
@@ -1,6 +1,5 @@
1
1
  import { z } from "zod";
2
2
  import { dispatchCapability, loadCapabilityCatalogPage } from "../api/capabilities";
3
- import { env } from "../config/env";
4
3
  import {
5
4
  CAPABILITY_FRAMEWORK_ERROR_CODES,
6
5
  CAPABILITY_MAX_CATALOG_BYTES,
@@ -9,8 +8,7 @@ import {
9
8
  capabilityResultSchema,
10
9
  } from "../contracts/capabilities";
11
10
  import type { RequestAuthority } from "../server";
12
- import { get } from "../services/settings";
13
- import { publicCloudOrigin } from "../shared/app-url";
11
+ import { coreOrigin } from "../services/identity/core-origin";
14
12
  import { capabilityMessages } from "../shared/capability-messages";
15
13
  import { LOCALE_HEADER } from "../shared/locale";
16
14
  import { readCapabilityResponse } from "./response";
@@ -41,14 +39,7 @@ export type CapabilityCaller = {
41
39
  mandate?: { id: string; revision: number; callingAppId: string };
42
40
  };
43
41
 
44
- export const coreOrigin = async (internal = false): Promise<string> => {
45
- const configured = env.CLOUD_CORE_INTERNAL_ORIGIN;
46
- if (internal && !configured) throw new Error("CLOUD_CORE_INTERNAL_ORIGIN is required for mandate-backed invocation");
47
- const origin = configured || publicCloudOrigin(await get<string>("app.url"));
48
- const url = new URL(origin);
49
- if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("CLOUD_CORE_INTERNAL_ORIGIN must use http or https");
50
- return url.origin;
51
- };
42
+ export { coreOrigin } from "../services/identity/core-origin";
52
43
 
53
44
  export const callerRequest = async (
54
45
  caller: CapabilityCaller,
@@ -17,8 +17,11 @@ import {
17
17
  } from "../contracts/capabilities";
18
18
 
19
19
  /** Compile and validate the public manifest an app would register at startup. */
20
- export const compileCapabilityManifest = (appId: string, definitions: CapabilityDefinitions): CapabilityManifest =>
21
- compileCapabilities(appId, definitions).manifest;
20
+ export const compileCapabilityManifest = (
21
+ appId: string,
22
+ definitions: CapabilityDefinitions,
23
+ routes?: readonly string[],
24
+ ): CapabilityManifest => compileCapabilities(appId, definitions, routes).manifest;
22
25
 
23
26
  /** Fail a provider compatibility test when a published local id changed incompatibly. */
24
27
  export const assertCapabilityManifestEvolution = (previous: CapabilityManifest, next: CapabilityManifest): void => {
package/src/cli/access.ts CHANGED
@@ -49,6 +49,7 @@ export type AccessCommandAdapter<TResource extends AccessResource> = {
49
49
  scopes?: readonly [string, ...string[]];
50
50
  allowPublic?: boolean;
51
51
  allowAuthenticated?: boolean;
52
+ /** Defaults to true; false preserves an explicitly restricted resource contract. */
52
53
  allowServiceAccounts?: boolean;
53
54
  examples?: {
54
55
  list?: readonly string[];
@@ -217,7 +218,7 @@ export const resolveAccessPrincipal = async (
217
218
  "--group",
218
219
  ...(options.allowAuthenticated === false ? [] : ["--authenticated"]),
219
220
  ...(options.allowPublic === true ? ["--public"] : []),
220
- ...(options.allowServiceAccounts === true ? ["--service-account"] : []),
221
+ ...(options.allowServiceAccounts !== false ? ["--service-account"] : []),
221
222
  ];
222
223
  throw new Error(`Pass exactly one principal flag: ${allowedFlags.join(", ")}.`);
223
224
  }
@@ -225,8 +226,8 @@ export const resolveAccessPrincipal = async (
225
226
  if (flags.authenticated && options.allowAuthenticated === false) {
226
227
  throw new Error("This resource does not allow authenticated-audience grants.");
227
228
  }
228
- if (flags.serviceAccount && options.allowServiceAccounts !== true) {
229
- throw new Error("Service-account grants are hidden by default. Enable allowServiceAccounts for this access command.");
229
+ if (flags.serviceAccount && options.allowServiceAccounts === false) {
230
+ throw new Error("This resource does not allow service-account access grants.");
230
231
  }
231
232
  if (flags.authenticated) return { type: "authenticated" };
232
233
  if (flags.public) return { type: "public" };
@@ -348,11 +349,11 @@ const accessIdFlag = {
348
349
 
349
350
  export const createAccessCommands = <TResource extends AccessResource>(adapter: AccessCommandAdapter<TResource>) => {
350
351
  const allowed = normalizeAllowedPermissions(adapter.allowedPermissions);
351
- const searchableKinds = adapter.allowServiceAccounts ? PRINCIPAL_KINDS : (["user", "group"] as const);
352
+ const searchableKinds = adapter.allowServiceAccounts !== false ? PRINCIPAL_KINDS : (["user", "group"] as const);
352
353
  const principalFlagOptions = {
353
354
  includePublicFlag: adapter.allowPublic === true,
354
355
  includeAuthenticatedFlag: adapter.allowAuthenticated !== false,
355
- includeServiceAccountFlag: adapter.allowServiceAccounts === true,
356
+ includeServiceAccountFlag: adapter.allowServiceAccounts !== false,
356
357
  };
357
358
  const resolve = (ctx: CloudCliContext, args: string[]) => adapter.resolveResource(ctx, args);
358
359
  const scopeFlag: { scope?: ReturnType<typeof flag.string> } = adapter.scopes
@@ -0,0 +1,199 @@
1
+ import { z } from "zod";
2
+ import {
3
+ AI_MODEL_SLOTS,
4
+ AiDeleteModelSchema,
5
+ AiModelAccessDraftSchema,
6
+ AiModelInputSchema,
7
+ AiModelPatchSchema,
8
+ AiProviderInputSchema,
9
+ AiProviderPatchSchema,
10
+ AiVariantInputSchema,
11
+ AiVariantPatchSchema,
12
+ } from "../../shared/ai-models";
13
+ import { arg, type CloudCliContext, command, confirmFlag, flag, printStructured, readCliInput } from "../index";
14
+ import { apiGet, apiJson, readJsonInput } from "./shared";
15
+
16
+ const base = "/api/admin/core/ai";
17
+ const print = (ctx: CloudCliContext, result: unknown) => {
18
+ if (!printStructured(ctx, result)) ctx.print(JSON.stringify(result, null, 2));
19
+ };
20
+ const confirm = (yes: boolean) => {
21
+ if (!yes) throw new Error("AI configuration changes require --yes.");
22
+ };
23
+ const id = { id: arg.required({ valueLabel: "id" }) };
24
+ const input = flag.input({ required: true, description: "JSON input: --input, --input-file or --stdin" });
25
+ const yes = confirmFlag("Confirm AI configuration change");
26
+ const endpoint = (area: string, id: string) => `${base}/${area}/${encodeURIComponent(id)}`;
27
+ export const aiModelCommands = [
28
+ command("ai providers list", {
29
+ summary: "List provider connections and their status",
30
+ async run({ ctx }) {
31
+ print(ctx, await apiGet(ctx, `${base}/providers`));
32
+ },
33
+ }),
34
+ command("ai providers show", {
35
+ summary: "Show a provider; credential and header values stay private",
36
+ args: id,
37
+ async run({ ctx, args }) {
38
+ print(ctx, await apiGet(ctx, endpoint("providers", args.id)));
39
+ },
40
+ }),
41
+ command("ai providers add", {
42
+ summary: "Create a provider connection",
43
+ flags: { input, yes },
44
+ async run({ ctx, flags }) {
45
+ confirm(flags.yes);
46
+ print(
47
+ ctx,
48
+ await apiJson(ctx, "POST", `${base}/providers`, AiProviderInputSchema.parse(await readJsonInput<unknown>(flags.input, "provider"))),
49
+ );
50
+ },
51
+ }),
52
+ command("ai providers update", {
53
+ summary: "Update a provider with optimistic concurrency",
54
+ args: id,
55
+ flags: { input, yes },
56
+ async run({ ctx, args, flags }) {
57
+ confirm(flags.yes);
58
+ const patch = AiProviderPatchSchema.parse(await readJsonInput<unknown>(flags.input, "provider patch"));
59
+ const current = await apiGet<{ revision: number }>(ctx, endpoint("providers", args.id));
60
+ print(ctx, await apiJson(ctx, "PATCH", endpoint("providers", args.id), { ...patch, revision: current.revision }));
61
+ },
62
+ }),
63
+ command("ai providers test", {
64
+ summary: "Test a saved provider connection (10 second timeout)",
65
+ args: id,
66
+ async run({ ctx, args }) {
67
+ print(ctx, await apiJson(ctx, "POST", `${endpoint("providers", args.id)}/test`, {}));
68
+ },
69
+ }),
70
+ command("ai providers key", {
71
+ summary: "Replace a provider key from protected input",
72
+ args: id,
73
+ flags: { key: flag.input({ required: true, description: "API key: use --key-file or --stdin" }), yes },
74
+ async run({ ctx, args, flags }) {
75
+ confirm(flags.yes);
76
+ const apiKey = await readCliInput(flags.key, { required: true, label: "API key" });
77
+ const current = await apiGet<{ revision: number }>(ctx, endpoint("providers", args.id));
78
+ print(
79
+ ctx,
80
+ await apiJson(ctx, "PUT", `${endpoint("providers", args.id)}/credential`, {
81
+ apiKey: apiKey?.trim() ?? "",
82
+ revision: current.revision,
83
+ }),
84
+ );
85
+ },
86
+ }),
87
+ command("ai providers models", {
88
+ summary: "List the models advertised by a provider",
89
+ args: id,
90
+ async run({ ctx, args }) {
91
+ print(ctx, await apiGet(ctx, `${endpoint("providers", args.id)}/available-models`));
92
+ },
93
+ }),
94
+ command("ai models list", {
95
+ summary: "List configured models",
96
+ async run({ ctx }) {
97
+ print(ctx, await apiGet(ctx, `${base}/models`));
98
+ },
99
+ }),
100
+ command("ai models show", {
101
+ summary: "Show a model, variants and access",
102
+ args: id,
103
+ async run({ ctx, args }) {
104
+ print(ctx, await apiGet(ctx, endpoint("models", args.id)));
105
+ },
106
+ }),
107
+ command("ai models add", {
108
+ summary: "Add a model to a provider",
109
+ args: { provider: arg.required() },
110
+ flags: { input, yes },
111
+ async run({ ctx, args, flags }) {
112
+ confirm(flags.yes);
113
+ const data = z
114
+ .union([AiModelInputSchema, z.array(AiModelInputSchema).min(1)])
115
+ .parse(await readJsonInput<unknown>(flags.input, "models"));
116
+ print(ctx, await apiJson(ctx, "POST", `${endpoint("providers", args.provider)}/models`, data));
117
+ },
118
+ }),
119
+ command("ai models update", {
120
+ summary: "Update one model and its shared prices",
121
+ args: id,
122
+ flags: { input, yes },
123
+ async run({ ctx, args, flags }) {
124
+ confirm(flags.yes);
125
+ const data = AiModelPatchSchema.extend({ access: AiModelAccessDraftSchema.optional() }).parse(
126
+ await readJsonInput<unknown>(flags.input, "model patch"),
127
+ );
128
+ const current = await apiGet<{ revision: number }>(ctx, endpoint("models", args.id));
129
+ print(ctx, await apiJson(ctx, "PATCH", endpoint("models", args.id), { ...data, revision: current.revision }));
130
+ },
131
+ }),
132
+ command("ai variants add", {
133
+ summary: "Create a named model variant",
134
+ args: { model: arg.required() },
135
+ flags: { input, yes },
136
+ async run({ ctx, args, flags }) {
137
+ confirm(flags.yes);
138
+ print(
139
+ ctx,
140
+ await apiJson(
141
+ ctx,
142
+ "POST",
143
+ `${endpoint("models", args.model)}/variants`,
144
+ AiVariantInputSchema.parse(await readJsonInput<unknown>(flags.input, "variant")),
145
+ ),
146
+ );
147
+ },
148
+ }),
149
+ command("ai variants update", {
150
+ summary: "Update a variant's overrides",
151
+ args: id,
152
+ flags: { input, yes },
153
+ async run({ ctx, args, flags }) {
154
+ confirm(flags.yes);
155
+ const data = AiVariantPatchSchema.parse(await readJsonInput<unknown>(flags.input, "variant patch"));
156
+ const current = await apiGet<{ variants: { id: string; revision: number }[] }>(ctx, endpoint("models", args.id.split(":")[0]!));
157
+ const variant = current.variants.find((v) => v.id === args.id);
158
+ if (!variant) throw new Error("Variant not found.");
159
+ print(ctx, await apiJson(ctx, "PATCH", endpoint("variants", args.id), { ...data, revision: variant.revision }));
160
+ },
161
+ }),
162
+ ...(["providers", "models", "variants"] as const).map((area) =>
163
+ command(`ai ${area} delete`, {
164
+ summary: "Delete with explicit assignment replacements and retargeting choice",
165
+ args: id,
166
+ flags: { input, yes },
167
+ async run({ ctx, args, flags }) {
168
+ confirm(flags.yes);
169
+ const data = AiDeleteModelSchema.parse(await readJsonInput<unknown>(flags.input, "delete consequences"));
170
+ print(ctx, await apiJson(ctx, "DELETE", endpoint(area, args.id), data));
171
+ },
172
+ }),
173
+ ),
174
+ command("ai classes show", {
175
+ summary: "Show class and tool assignments, fallback chains and recent callers",
176
+ async run({ ctx }) {
177
+ print(ctx, await apiGet(ctx, `${base}/assignments`));
178
+ },
179
+ }),
180
+ command("ai classes set", {
181
+ summary: "Assign a class or tool slot to a variant or none",
182
+ args: { slot: arg.required(), variant: arg.required() },
183
+ flags: { yes },
184
+ async run({ ctx, args, flags }) {
185
+ confirm(flags.yes);
186
+ const slot = z.enum(AI_MODEL_SLOTS).parse(args.slot);
187
+ const current = await apiGet<{ assignments: { slot: string; revision: number }[] }>(ctx, `${base}/assignments`);
188
+ const assignment = current.assignments.find((a) => a.slot === slot);
189
+ if (!assignment) throw new Error("Assignment missing.");
190
+ print(
191
+ ctx,
192
+ await apiJson(ctx, "PUT", `${base}/assignments/${slot}`, {
193
+ variantId: args.variant === "none" ? null : args.variant,
194
+ revision: assignment.revision,
195
+ }),
196
+ );
197
+ },
198
+ }),
199
+ ];
@@ -1,9 +1,5 @@
1
- import { type AiModelPricing, AiModelPricingSchema } from "../../shared/ai-costs";
2
- import {
3
- type AiModelRequestSettings,
4
- AiModelRequestSettingsSchema,
5
- AiModelRequestSettingsUpdateSchema,
6
- } from "../../shared/ai-model-request-settings";
1
+ import type { AiModelPricing } from "../../shared/ai-costs";
2
+
7
3
  import {
8
4
  type AiQuotaConfig,
9
5
  AiQuotaConfigSchema,
@@ -12,7 +8,7 @@ import {
12
8
  AiQuotaReportQuerySchema,
13
9
  AiQuotaResetSchema,
14
10
  } from "../../shared/ai-quotas";
15
- import { type CloudCliContext, cliText, command, confirmFlag, flag, printRows, printStructured, readCliInput } from "../index";
11
+ import { type CloudCliContext, cliText, command, confirmFlag, flag, printRows, printStructured } from "../index";
16
12
  import { apiGet, apiJson, queryString, readJsonInput } from "./shared";
17
13
 
18
14
  const path = "/api/admin/core/ai-quotas";
@@ -28,98 +24,6 @@ const confirm = (ctx: CloudCliContext, yes: boolean) => {
28
24
  };
29
25
 
30
26
  export const aiQuotaCommands = [
31
- command("ai models settings get", {
32
- summary: "Read model thinking level, extra parameters and configured header names",
33
- flags: { id: flag.string({ required: true, description: "Model profile ID" }) },
34
- async run({ ctx, flags }) {
35
- print(ctx, AiModelRequestSettingsSchema.parse(await apiGet(ctx, `${path}/models/${encodeURIComponent(flags.id!)}/settings`)));
36
- },
37
- }),
38
- command("ai models settings set", {
39
- summary: "Patch one model's request settings; header values are write-only",
40
- flags: {
41
- id: flag.string({ required: true, description: "Model profile ID" }),
42
- thinkingLevel: flag.string({ name: "thinking-level", description: "Provider-specific thinking level" }),
43
- clearThinkingLevel: flag.boolean({ name: "clear-thinking-level", description: "Use the model's default thinking level" }),
44
- extraBody: flag.input({ name: "extra-body", description: "Extra parameters JSON: --extra-body, --extra-body-file or --stdin" }),
45
- clearExtraBody: flag.boolean({ name: "clear-extra-body", description: "Remove extra parameters" }),
46
- headers: flag.input({
47
- stdinName: "headers-stdin",
48
- description: "Header patch JSON; use --headers-file or --headers-stdin for secrets",
49
- }),
50
- clearHeaders: flag.boolean({ name: "clear-headers", description: "Remove all configured extra headers before applying the patch" }),
51
- yes: confirmFlag("Confirm model request settings change"),
52
- },
53
- async run({ ctx, flags }) {
54
- if (!flags.yes) throw new Error("Model request settings changes require --yes.");
55
- if (flags.thinkingLevel !== undefined && flags.clearThinkingLevel)
56
- throw new Error("Choose --thinking-level or --clear-thinking-level.");
57
- if (flags.extraBody.provided && flags.clearExtraBody) throw new Error("Choose --extra-body or --clear-extra-body.");
58
- if (
59
- !flags.extraBody.provided &&
60
- !flags.headers.provided &&
61
- flags.thinkingLevel === undefined &&
62
- !flags.clearThinkingLevel &&
63
- !flags.clearExtraBody &&
64
- !flags.clearHeaders
65
- )
66
- throw new Error("Select at least one model request setting to change.");
67
- if (flags.extraBody.source === "stdin" && flags.headers.source === "stdin")
68
- throw new Error("Read one JSON input from stdin and supply the other from a file.");
69
- const parseInput = async (input: typeof flags.headers, label: string): Promise<unknown> => {
70
- const raw = await readCliInput(input, { required: true, label });
71
- try {
72
- return JSON.parse(raw!);
73
- } catch {
74
- throw new Error(`Invalid ${label} JSON.`);
75
- }
76
- };
77
- const patch: Record<string, unknown> = {};
78
- if (flags.thinkingLevel !== undefined || flags.clearThinkingLevel)
79
- patch.reasoningEffort = flags.clearThinkingLevel ? null : flags.thinkingLevel;
80
- if (flags.extraBody.provided || flags.clearExtraBody)
81
- patch.extraBody = flags.clearExtraBody ? null : await parseInput(flags.extraBody, "extra parameters");
82
- if (flags.headers.provided) patch.requestHeaders = await parseInput(flags.headers, "extra headers");
83
- if (flags.clearHeaders) patch.clearRequestHeaders = true;
84
- // Validate locally before reading the current revision or sending secrets.
85
- const valid = AiModelRequestSettingsUpdateSchema.parse({ ...patch, expected: "pending" });
86
- const current = await apiGet<AiModelRequestSettings>(ctx, `${path}/models/${encodeURIComponent(flags.id!)}/settings`);
87
- print(
88
- ctx,
89
- AiModelRequestSettingsSchema.parse(
90
- await apiJson(ctx, "PUT", `${path}/models/${encodeURIComponent(flags.id!)}/settings`, { ...valid, expected: current.revision }),
91
- ),
92
- );
93
- },
94
- }),
95
- command("ai models pricing get", {
96
- summary: "List configured model input/output reference prices per million tokens",
97
- async run({ ctx }) {
98
- print(ctx, await apiGet(ctx, `${path}/models`));
99
- },
100
- }),
101
- command("ai models pricing set", {
102
- summary: "Set one model's reference prices; JSON null removes prices and makes it unlimited",
103
- flags: {
104
- id: flag.string({ required: true, description: "Model profile ID" }),
105
- pricing: flag.input({
106
- required: true,
107
- description: "{inputPerMillion,outputPerMillion} or null: --pricing, --pricing-file or --stdin",
108
- }),
109
- yes: confirmFlag("Confirm model price change"),
110
- },
111
- async run({ ctx, flags }) {
112
- confirm(ctx, flags.yes);
113
- const pricing = AiModelPricingSchema.nullable().parse(await readJsonInput<unknown>(flags.pricing, "model pricing"));
114
- const current = await apiGet<{ models: { id: string; pricing?: AiModelPricing }[] }>(ctx, `${path}/models`);
115
- const model = current.models.find((model) => model.id === flags.id);
116
- if (!model) throw new Error("Model not found.");
117
- print(
118
- ctx,
119
- await apiJson(ctx, "PUT", `${path}/models/${encodeURIComponent(flags.id!)}/pricing`, { pricing, expected: model.pricing ?? null }),
120
- );
121
- },
122
- }),
123
27
  command("ai quotas background status", {
124
28
  summary: "Show rolling 24-hour background costs and emergency-stop state",
125
29
  async run({ ctx }) {
@@ -153,7 +57,7 @@ export const aiQuotaCommands = [
153
57
  },
154
58
  }),
155
59
  command("ai quotas models", {
156
- summary: "List configured model profiles; only priced chat models support cost rules",
60
+ summary: "List configured models; only priced chat models support cost rules",
157
61
  async run({ ctx }) {
158
62
  const result = await apiGet<{ models: { id: string; label: string; pricing?: AiModelPricing }[] }>(ctx, `${path}/models`);
159
63
  printRows(
@@ -170,7 +74,7 @@ export const aiQuotaCommands = [
170
74
  range: flag.enum(["24h", "7d", "30d", "90d"] as const, { default: "30d" }),
171
75
  until: flag.string({ description: "ISO period end; reuse query.until for consistent pagination" }),
172
76
  search: flag.string({ description: "Search identity labels" }),
173
- model: flag.string({ description: "Model profile ID" }),
77
+ model: flag.string({ description: "Model ID" }),
174
78
  status: flag.enum(["all", "available", "exhausted", "unknown", "unlimited", "disabled"] as const, { default: "all" }),
175
79
  sort: flag.enum(["label", "cost", "lastUsed"] as const, { default: "lastUsed" }),
176
80
  direction: flag.enum(["asc", "desc"] as const, { default: "desc" }),
@@ -216,7 +120,7 @@ export const aiQuotaCommands = [
216
120
  summary: "Reset one identity and quota scope; reuse request ID when retrying",
217
121
  flags: {
218
122
  ...identity,
219
- scope: flag.string({ required: true, description: "Model profile ID or * (quote the wildcard)" }),
123
+ scope: flag.string({ required: true, description: "Model ID or * (quote the wildcard)" }),
220
124
  requestId: flag.string({ required: true, description: "UUID for this reset; reuse for retries, use a new UUID for a new reset" }),
221
125
  yes: confirmFlag("Confirm reset of this identity and quota scope"),
222
126
  },
@@ -7,7 +7,7 @@ const filters = {
7
7
  range: flag.enum(AI_USAGE_RANGES, { default: "30d", description: "Response/run period" }),
8
8
  until: flag.string({ description: "Fixed ISO end time; reuse report.query.until across pages" }),
9
9
  userId: flag.string({ name: "user", description: "User UUID or unassigned" }),
10
- modelProfileId: flag.string({ name: "model", description: "Model profile ID" }),
10
+ modelId: flag.string({ name: "model", description: "Model profile ID" }),
11
11
  providerModel: flag.string({ name: "provider-model", description: "Actual provider model" }),
12
12
  workflowId: flag.string({ name: "workflow", description: "Workflow definition UUID" }),
13
13
  workflowRunId: flag.string({ name: "workflow-run", description: "Workflow run UUID" }),
@@ -75,7 +75,7 @@ export const aiUsageCommands = [
75
75
  { key: "task" },
76
76
  { key: "status" },
77
77
  { key: "userLabel" },
78
- { key: "modelProfileId" },
78
+ { key: "modelId" },
79
79
  { key: "cost" },
80
80
  { key: "unit" },
81
81
  { key: "error" },
@@ -85,7 +85,7 @@ export const aiUsageCommands = [
85
85
  ctx.table(report.feedback.items, [
86
86
  { key: "id" },
87
87
  { key: "userLabel" },
88
- { key: "modelProfileId" },
88
+ { key: "modelId" },
89
89
  { key: "rating" },
90
90
  { key: "comment" },
91
91
  ]);
@@ -125,7 +125,7 @@ export const aiUsageCommands = [
125
125
  command("ai usage facets", {
126
126
  summary: "Find user IDs, model profiles, provider models, apps or tasks for filtering",
127
127
  flags: {
128
- field: flag.enum(["userId", "modelProfileId", "providerModel", "appId", "task"] as const, { required: true }),
128
+ field: flag.enum(["userId", "modelId", "providerModel", "appId", "task"] as const, { required: true }),
129
129
  search: flag.string(),
130
130
  range: filters.range,
131
131
  },
@@ -51,7 +51,7 @@ export const appCredentialCommands = [
51
51
  },
52
52
  async run({ ctx, args, flags }) {
53
53
  if (!flags.yes)
54
- throw new Error(cliText(ctx, { en: "Creating an app credential requires --yes.", de: "App-Zugang erstellen erfordert --yes." }));
54
+ throw new Error(cliText(ctx, { en: "Creating an app credential requires --yes.", de: "App-Schlüssel erstellen erfordert --yes." }));
55
55
  const result = z.object({ credential: metadata, token: z.string() }).parse(
56
56
  await apiJson(ctx, "POST", path(args.app), {
57
57
  name: flags.name,
@@ -68,10 +68,12 @@ export const appCredentialCommands = [
68
68
  flags: { yes: confirmFlag("Revoke this app credential") },
69
69
  async run({ ctx, args, flags }) {
70
70
  if (!flags.yes)
71
- throw new Error(cliText(ctx, { en: "Revoking an app credential requires --yes.", de: "App-Zugang widerrufen erfordert --yes." }));
71
+ throw new Error(
72
+ cliText(ctx, { en: "Revoking an app credential requires --yes.", de: "App-Schlüssel widerrufen erfordert --yes." }),
73
+ );
72
74
  const credentialId = z.uuid().parse(args.credential);
73
75
  const result = await apiJson(ctx, "DELETE", `${path(args.app)}/${credentialId}`);
74
- if (!printStructured(ctx, result)) ctx.print(cliText(ctx, { en: "Credential revoked.", de: "Zugang widerrufen." }));
76
+ if (!printStructured(ctx, result)) ctx.print(cliText(ctx, { en: "Credential revoked.", de: "Schlüssel widerrufen." }));
75
77
  },
76
78
  }),
77
79
  ];