@k2b/cloud 0.6.0 → 0.8.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 (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -0,0 +1,102 @@
1
+ import type { CloudResourceRef } from "../contracts";
2
+
3
+ export type SearchScope = { label: string; icon?: string } & (
4
+ | { ref: CloudResourceRef; appId?: never; tag?: never }
5
+ | { appId: string; tag?: string; ref?: never }
6
+ );
7
+ export type GlobalSearchOptions = { scope?: SearchScope; query?: string };
8
+ export type SearchNavigationTarget = { href: string; ref?: CloudResourceRef };
9
+ type OpenRequest = { options: GlobalSearchOptions; accepted: boolean };
10
+ type NavigationRequest = { target: SearchNavigationTarget; handled?: Promise<boolean> };
11
+
12
+ declare global {
13
+ interface WindowEventMap {
14
+ "cloud.search.open": CustomEvent<OpenRequest>;
15
+ "cloud.search.navigate": CustomEvent<NavigationRequest>;
16
+ }
17
+ }
18
+
19
+ const READY = "cloud.search.ready";
20
+ const CANCEL_OPEN = "cloud.search.cancel-open";
21
+ const REPLACE_HOST = "cloud.search.replace-host";
22
+ const REPLACE_NAVIGATION = "cloud.search.replace-navigation";
23
+
24
+ /** One pending request per document, shared through events even across separate bundles. */
25
+ export const requestGlobalSearch = (options: GlobalSearchOptions): Promise<void> => {
26
+ window.dispatchEvent(new Event(CANCEL_OPEN));
27
+ return new Promise((resolve, reject) => {
28
+ const detail: OpenRequest = { options, accepted: false };
29
+ let timer: ReturnType<typeof setTimeout> | undefined;
30
+ const cleanup = () => {
31
+ clearTimeout(timer);
32
+ window.removeEventListener(READY, attempt);
33
+ window.removeEventListener(CANCEL_OPEN, cancel);
34
+ window.removeEventListener("pagehide", cancel);
35
+ };
36
+ const cancel = () => {
37
+ cleanup();
38
+ resolve();
39
+ };
40
+ const attempt = () => {
41
+ window.dispatchEvent(new CustomEvent("cloud.search.open", { detail }));
42
+ if (detail.accepted) cancel();
43
+ };
44
+ window.addEventListener(READY, attempt);
45
+ window.addEventListener(CANCEL_OPEN, cancel);
46
+ window.addEventListener("pagehide", cancel);
47
+ // Bound the wait for hydration; never keep an invisible request indefinitely.
48
+ timer = setTimeout(() => {
49
+ cleanup();
50
+ reject(new Error("Search host unavailable"));
51
+ }, 8_000);
52
+ attempt();
53
+ });
54
+ };
55
+
56
+ export const registerGlobalSearchHost = (open: (options: GlobalSearchOptions) => void, dispose?: () => void): (() => void) => {
57
+ window.dispatchEvent(new Event(REPLACE_HOST));
58
+ const listener = (event: CustomEvent<OpenRequest>) => {
59
+ if (event.detail.accepted) return;
60
+ open(event.detail.options);
61
+ event.detail.accepted = true;
62
+ };
63
+ let active = true;
64
+ const cleanup = () => {
65
+ if (!active) return;
66
+ active = false;
67
+ dispose?.();
68
+ window.removeEventListener("cloud.search.open", listener);
69
+ window.removeEventListener(REPLACE_HOST, cleanup);
70
+ };
71
+ window.addEventListener("cloud.search.open", listener);
72
+ window.addEventListener(REPLACE_HOST, cleanup);
73
+ window.dispatchEvent(new Event(READY));
74
+ return cleanup;
75
+ };
76
+
77
+ export const registerSearchNavigation = (handler: (target: SearchNavigationTarget) => boolean | Promise<boolean>): (() => void) => {
78
+ window.dispatchEvent(new Event(REPLACE_NAVIGATION));
79
+ let active = true;
80
+ const listener = (event: CustomEvent<NavigationRequest>) => {
81
+ if (event.detail.handled) return;
82
+ event.detail.handled = Promise.resolve().then(async () => {
83
+ if (!active) return true; // An unmounted page must not start a late navigation.
84
+ const handled = await handler(event.detail.target);
85
+ return !active || handled;
86
+ });
87
+ };
88
+ const cleanup = () => {
89
+ active = false;
90
+ window.removeEventListener("cloud.search.navigate", listener);
91
+ window.removeEventListener(REPLACE_NAVIGATION, cleanup);
92
+ };
93
+ window.addEventListener("cloud.search.navigate", listener);
94
+ window.addEventListener(REPLACE_NAVIGATION, cleanup);
95
+ return cleanup;
96
+ };
97
+
98
+ export const requestSearchNavigation = async (target: SearchNavigationTarget): Promise<boolean> => {
99
+ const detail: NavigationRequest = { target };
100
+ window.dispatchEvent(new CustomEvent("cloud.search.navigate", { detail }));
101
+ return (await detail.handled) ?? false;
102
+ };
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import type { SearchItem } from "../api/search/schemas";
3
+ import { listCapabilityCatalog } from "../capabilities/client";
4
+ import { LOCALE_HEADER } from "../shared/locale";
5
+ import type { ContextAwareCommand } from "./command-bridge";
6
+
7
+ export type PaletteCommand = ContextAwareCommand & { appName?: string; keywords?: readonly string[]; context?: boolean };
8
+ export const loadSearchCommands = async (locale: string, signal: AbortSignal): Promise<PaletteCommand[]> => {
9
+ const commands: PaletteCommand[] = [];
10
+ let cursor: string | undefined;
11
+ do {
12
+ const result = await listCapabilityCatalog({ limit: 25, cursor, signal, headers: { [LOCALE_HEADER]: locale } });
13
+ if (!result.ok) throw new Error(result.error.message);
14
+ for (const app of result.data.apps) {
15
+ for (const command of app.manifest.commands) {
16
+ // Only Commands with a valid empty invocation are globally discoverable.
17
+ const input = z.fromJSONSchema(structuredClone(command.inputSchema)).safeParse({});
18
+ if (!input.success) continue;
19
+ const id = `${app.appId}.${command.localId}`;
20
+ commands.push({
21
+ id,
22
+ title: command.title,
23
+ description: command.description,
24
+ icon: command.icon ?? app.appIcon,
25
+ keywords: command.keywords,
26
+ appName: app.appName,
27
+ action: { command: id, input: input.data },
28
+ });
29
+ }
30
+ }
31
+ const next = result.data.page.hasMore ? result.data.page.nextCursor : undefined;
32
+ if (next && cursor && next <= cursor) throw new Error("Invalid Command catalog cursor");
33
+ cursor = next;
34
+ } while (cursor);
35
+ return commands;
36
+ };
37
+ export const commandSearchItem = (command: PaletteCommand, group: string): SearchItem => ({
38
+ ref: { type: "cloud.command", id: command.id },
39
+ href: "",
40
+ readable: false,
41
+ appId: command.context ? "cloud.context" : "cloud.commands",
42
+ appName: group,
43
+ appIcon: "ti ti-command",
44
+ title: command.title,
45
+ icon: command.icon ?? "ti ti-command",
46
+ preview: command.description,
47
+ });
48
+ export const matchingCommands = (commands: readonly PaletteCommand[], query: string, all: boolean): PaletteCommand[] => {
49
+ const normalized = query.toLocaleLowerCase().trim();
50
+ const matches = commands.filter((command) =>
51
+ !normalized
52
+ ? all || (command.context && !command.id.startsWith("cloud."))
53
+ : [command.title, command.description, command.appName, ...(command.keywords ?? [])]
54
+ .join(" ")
55
+ .toLocaleLowerCase()
56
+ .includes(normalized),
57
+ );
58
+ return !normalized && !all ? matches.slice(0, 3) : matches;
59
+ };
@@ -0,0 +1,13 @@
1
+ import { toast } from "@k2b/ui";
2
+ import { resourceSearchMessages } from "./resource-search-messages";
3
+ import { type GlobalSearchOptions, requestGlobalSearch } from "./search-bridge";
4
+
5
+ export type { GlobalSearchOptions, SearchNavigationTarget, SearchScope } from "./search-bridge";
6
+ export { registerSearchNavigation } from "./search-bridge";
7
+
8
+ /** Open the Cloud layout's search, optionally within one removable context. */
9
+ export const openGlobalSearch = (options: GlobalSearchOptions = {}): void => {
10
+ void requestGlobalSearch(options).catch(() => {
11
+ toast.error(resourceSearchMessages.resolve([document.documentElement.lang || "en"]).t.searchFailed);
12
+ });
13
+ };
@@ -0,0 +1,208 @@
1
+ import type { DialogRender } from "@k2b/ui";
2
+
3
+ export type SpotlightPosition = { x: number; y: number };
4
+ const STORAGE_KEY = "cloud.spotlight.position.v1";
5
+ const MARGIN = 12;
6
+ const SNAP_DISTANCE = 24;
7
+
8
+ export const readSpotlightPosition = (value: string | null): SpotlightPosition | null => {
9
+ try {
10
+ const position: unknown = JSON.parse(value ?? "null");
11
+ if (!position || typeof position !== "object" || !("x" in position) || !("y" in position)) return null;
12
+ const { x, y } = position;
13
+ if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) return null;
14
+ return { x: Math.max(0, Math.min(1, x)), y: Math.max(0, Math.min(1, y)) };
15
+ } catch {
16
+ return null;
17
+ }
18
+ };
19
+
20
+ /** Cloud owns the desktop policy and persistence; the UI host owns modality and focus. */
21
+ export const attachSpotlightPosition = (
22
+ host: HTMLElement,
23
+ context: Parameters<DialogRender<void>>[1],
24
+ messages: () => { homeHint: string; releaseHint: string },
25
+ ) => {
26
+ const { dialog } = context;
27
+ const desktop = window.matchMedia("(min-width: 64rem) and (hover: hover) and (pointer: fine)");
28
+ const enabled = () => desktop.matches && navigator.maxTouchPoints === 0;
29
+ const active = () => host.parentElement?.style.display !== "none" && dialog.classList.contains("cloud-search-dialog");
30
+ let stored: SpotlightPosition | null = null;
31
+ try {
32
+ stored = readSpotlightPosition(window.localStorage.getItem(STORAGE_KEY));
33
+ } catch {
34
+ /* Storage can be disabled; dragging still works for this opening. */
35
+ }
36
+ let anchor = { x: 0, y: 0 };
37
+ let bounds = { x: 0, y: 0 };
38
+ let position: SpotlightPosition | null = null;
39
+ let drag: {
40
+ id: number;
41
+ start: SpotlightPosition;
42
+ origin: SpotlightPosition;
43
+ previous: SpotlightPosition | null;
44
+ handle: HTMLElement;
45
+ } | null = null;
46
+
47
+ const home = document.createElement("div");
48
+ home.className = "cloud-global-search__home";
49
+ home.setAttribute("aria-hidden", "true");
50
+ const icon = document.createElement("i");
51
+ icon.className = "ti ti-search";
52
+ const hint = document.createElement("span");
53
+ const content = document.createElement("div");
54
+ content.append(icon, hint);
55
+ home.append(content);
56
+ host.append(home);
57
+ const clamp = (point: SpotlightPosition) => {
58
+ const rect = dialog.getBoundingClientRect();
59
+ return {
60
+ x: Math.max(MARGIN, Math.min(window.innerWidth - rect.width - MARGIN, point.x)),
61
+ y: Math.max(MARGIN, Math.min(window.innerHeight - rect.height - MARGIN, point.y)),
62
+ };
63
+ };
64
+ const renderPosition = () => {
65
+ if (!active()) return;
66
+ // Preserve the preferred point when growing upward to keep the panel on screen.
67
+ context.setPosition(position ? clamp(position) : null);
68
+ const rect = dialog.getBoundingClientRect();
69
+ home.style.left = `${anchor.x - rect.x - dialog.clientLeft}px`;
70
+ home.style.top = `${anchor.y - rect.y - dialog.clientTop}px`;
71
+ home.style.width = `${rect.width}px`;
72
+ home.style.height = `${rect.height}px`;
73
+ const ready = Math.hypot(rect.x - anchor.x, rect.y - anchor.y) <= SNAP_DISTANCE;
74
+ home.dataset.ready = String(ready);
75
+ icon.className = ready ? "ti ti-check" : "ti ti-search";
76
+ hint.textContent = ready ? messages().releaseHint : messages().homeHint;
77
+ };
78
+ const apply = (next: SpotlightPosition | null) => {
79
+ position = next;
80
+ context.setModal(next === null);
81
+ renderPosition();
82
+ };
83
+ const persist = () => {
84
+ stored = position
85
+ ? {
86
+ x: bounds.x ? (position.x - MARGIN) / bounds.x : 0,
87
+ y: bounds.y ? (position.y - MARGIN) / bounds.y : 0,
88
+ }
89
+ : null;
90
+ try {
91
+ if (stored) window.localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
92
+ else window.localStorage.removeItem(STORAGE_KEY);
93
+ } catch {
94
+ /* No persistence when storage is unavailable. */
95
+ }
96
+ };
97
+ const layout = () => {
98
+ if (!active()) return;
99
+ context.setPosition(null);
100
+ const rect = dialog.getBoundingClientRect();
101
+ anchor = { x: rect.x, y: rect.y };
102
+ // Vertical persistence describes the preferred top edge, independent of result height.
103
+ bounds = { x: Math.max(0, window.innerWidth - rect.width - 2 * MARGIN), y: Math.max(0, window.innerHeight - 2 * MARGIN) };
104
+ host.dataset.movable = String(enabled());
105
+ apply(enabled() && stored ? { x: MARGIN + stored.x * bounds.x, y: MARGIN + stored.y * bounds.y } : null);
106
+ };
107
+ const finish = () => {
108
+ if (!drag) return;
109
+ const handle = drag.handle;
110
+ const id = drag.id;
111
+ drag = null;
112
+ if (handle.hasPointerCapture(id)) handle.releasePointerCapture(id);
113
+ delete host.dataset.dragging;
114
+ persist();
115
+ };
116
+ const down = (event: PointerEvent) => {
117
+ const handle = event.target;
118
+ if (
119
+ !enabled() ||
120
+ !active() ||
121
+ event.pointerType !== "mouse" ||
122
+ event.button !== 0 ||
123
+ !(handle instanceof HTMLElement) ||
124
+ !handle.hasAttribute("data-search-grip")
125
+ )
126
+ return;
127
+ event.preventDefault();
128
+ const rect = dialog.getBoundingClientRect();
129
+ drag = {
130
+ id: event.pointerId,
131
+ start: { x: event.clientX, y: event.clientY },
132
+ origin: { x: rect.x, y: rect.y },
133
+ previous: position,
134
+ handle,
135
+ };
136
+ handle.setPointerCapture(event.pointerId);
137
+ };
138
+ const move = (event: PointerEvent) => {
139
+ if (!drag || event.pointerId !== drag.id) return;
140
+ const dx = event.clientX - drag.start.x;
141
+ const dy = event.clientY - drag.start.y;
142
+ if (!host.dataset.dragging && Math.hypot(dx, dy) < 3) return;
143
+ host.dataset.dragging = "true";
144
+ const next = clamp({ x: drag.origin.x + dx, y: drag.origin.y + dy });
145
+ apply(next);
146
+ // Switching native modality releases capture. Retain the same drag across that switch.
147
+ if (!drag.handle.hasPointerCapture(event.pointerId)) drag.handle.setPointerCapture(event.pointerId);
148
+ };
149
+ const up = (event: PointerEvent) => {
150
+ if (event.pointerId !== drag?.id) return;
151
+ if (host.dataset.dragging && home.dataset.ready === "true") apply(null);
152
+ finish();
153
+ };
154
+ const cancel = () => {
155
+ if (!drag) return;
156
+ apply(drag.previous);
157
+ finish();
158
+ };
159
+ const escape = (event: KeyboardEvent) => {
160
+ if (event.key !== "Escape" || !drag) return;
161
+ event.preventDefault();
162
+ event.stopPropagation();
163
+ cancel();
164
+ };
165
+ const resize = () => {
166
+ finish();
167
+ layout();
168
+ };
169
+ const reset = () => {
170
+ cancel();
171
+ apply(null);
172
+ persist();
173
+ };
174
+ host.addEventListener("pointerdown", down);
175
+ const doubleClick = (event: MouseEvent) => {
176
+ if (event.target instanceof HTMLElement && event.target.hasAttribute("data-search-grip")) reset();
177
+ };
178
+ host.addEventListener("dblclick", doubleClick);
179
+ window.addEventListener("pointermove", move);
180
+ window.addEventListener("pointerup", up);
181
+ window.addEventListener("pointercancel", cancel);
182
+ window.addEventListener("blur", finish);
183
+ window.addEventListener("keydown", escape, true);
184
+ window.addEventListener("resize", resize);
185
+ desktop.addEventListener("change", resize);
186
+ // Mount happens before the native dialog is shown by the host.
187
+ const frame = requestAnimationFrame(layout);
188
+ const observer = new MutationObserver(layout);
189
+ observer.observe(dialog, { attributes: true, attributeFilter: ["class"] });
190
+ const sizeObserver = new ResizeObserver(renderPosition);
191
+ sizeObserver.observe(dialog);
192
+ return () => {
193
+ cancelAnimationFrame(frame);
194
+ observer.disconnect();
195
+ sizeObserver.disconnect();
196
+ home.remove();
197
+ finish();
198
+ host.removeEventListener("pointerdown", down);
199
+ host.removeEventListener("dblclick", doubleClick);
200
+ window.removeEventListener("pointermove", move);
201
+ window.removeEventListener("pointerup", up);
202
+ window.removeEventListener("pointercancel", cancel);
203
+ window.removeEventListener("blur", finish);
204
+ window.removeEventListener("keydown", escape, true);
205
+ window.removeEventListener("resize", resize);
206
+ desktop.removeEventListener("change", resize);
207
+ };
208
+ };
@@ -0,0 +1,6 @@
1
+ /** Browser-only test probes. Mount and dispose application islands through their public APIs. */
2
+
3
+ export { readWorkspaceNavigation } from "../ssr/workspace-navigation";
4
+ export { collectContextAwareCommands } from "./command-bridge";
5
+ export { contextCommandsWithShortcuts } from "./command-shortcuts";
6
+ export { registerGlobalSearchHost } from "./search-bridge";
@@ -0,0 +1,25 @@
1
+ import { type Metric, onCLS, onINP, onLCP } from "web-vitals";
2
+
3
+ const script = document.querySelector<HTMLScriptElement>("script[data-cloud-web-vitals]");
4
+ const appId = script?.dataset.appId;
5
+ const routeTemplate = script?.dataset.routeTemplate;
6
+
7
+ if (appId && routeTemplate) {
8
+ const report = ({ name, value, id, navigationType }: Metric) => {
9
+ const entry = performance.getEntriesByType("navigation")[0];
10
+ const serverTiming: Record<string, number> = {};
11
+ const timings =
12
+ typeof PerformanceNavigationTiming !== "undefined" && entry instanceof PerformanceNavigationTiming ? entry.serverTiming : [];
13
+ for (const timing of timings) {
14
+ if (["auth", "settings", "runtime", "ssr_data", "ssr_finalize", "ssr_render"].includes(timing.name)) {
15
+ serverTiming[timing.name] = (serverTiming[timing.name] ?? 0) + timing.duration;
16
+ }
17
+ }
18
+ // Never serialize Metric.entries: they can contain page text, elements and private URLs.
19
+ const body = JSON.stringify({ appId, routeTemplate, name, value, id, navigationType, serverTiming });
20
+ navigator.sendBeacon("/api/me/web-vitals", new Blob([body], { type: "application/json" }));
21
+ };
22
+ onLCP(report);
23
+ onINP(report);
24
+ onCLS(report);
25
+ }
@@ -0,0 +1,179 @@
1
+ import { sql } from "bun";
2
+ import { logger } from "../services/logging";
3
+
4
+ const log = logger("capabilities:claims");
5
+
6
+ /**
7
+ * Platform-owned idempotency claims for capability Actions.
8
+ *
9
+ * Every Action that declares `idempotency: "required"` is claimed here before
10
+ * the dispatcher forwards it, and resolved once the outcome is known. Apps own
11
+ * no claim code: retry safety is the same contract for built-in and
12
+ * third-party capabilities.
13
+ */
14
+ export type CapabilityClaimScope = {
15
+ appId: string;
16
+ /** Fully qualified capability name, such as `grids.record_create`. */
17
+ capability: string;
18
+ /** Acting principal as `user:<id>`, `service_account:<id>`, or `anonymous`. */
19
+ principal: string;
20
+ /** Caller-supplied Idempotency-Key, hashed before storage. */
21
+ keyHash: string;
22
+ };
23
+
24
+ export type CapabilityClaimState = "in_flight" | "succeeded" | "uncertain";
25
+
26
+ export type CapabilityClaimOutcome =
27
+ /** The caller owns this claim and must resolve it after forwarding. */
28
+ | { state: "claimed" }
29
+ /** A definitive earlier success for the same input; return this response verbatim. */
30
+ | { state: "replay"; status: number; body: unknown }
31
+ /** An earlier success whose response exceeded the retained body cap. */
32
+ | { state: "not_retained" }
33
+ /** Same key, different input. */
34
+ | { state: "conflict" }
35
+ /** A concurrent attempt still holds the claim. */
36
+ | { state: "in_flight" }
37
+ /** An earlier attempt left the outcome unknown; the caller must verify with a Query. */
38
+ | { state: "uncertain" };
39
+
40
+ /**
41
+ * A retry window long enough to cover a human or agent noticing a lost
42
+ * response and repeating the call, without keeping response bodies around as
43
+ * de-facto durable app data. Twenty-four hours is the common industry contract
44
+ * and there is deliberately no operator setting for it.
45
+ */
46
+ export const CAPABILITY_CLAIM_RETENTION_HOURS = 24;
47
+
48
+ /**
49
+ * Responses larger than this are not retained. A replay then reports that the
50
+ * result is gone rather than returning a partial or fabricated body; the
51
+ * caller must read the current state with a Query.
52
+ */
53
+ export const CAPABILITY_CLAIM_MAX_BODY_BYTES = 256 * 1024;
54
+
55
+ /** Stable JSON with sorted object keys, so equal inputs hash equally whatever order the caller sent. */
56
+ const canonicalJson = (value: unknown): string => {
57
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
58
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
59
+ const entries = Object.entries(value as Record<string, unknown>)
60
+ .filter(([, item]) => item !== undefined)
61
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
62
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
63
+ };
64
+
65
+ const sha256 = (value: string): string => new Bun.CryptoHasher("sha256").update(value).digest("hex");
66
+
67
+ /** Hash of the validated capability input. Two calls with the same key must carry the same hash. */
68
+ export const capabilityRequestHash = (input: unknown): string => sha256(canonicalJson(input));
69
+
70
+ export const capabilityIdempotencyKeyHash = (key: string): string => sha256(key);
71
+
72
+ type ClaimRow = { state: CapabilityClaimState; request_hash: string; response_status: number | null; response_body: unknown };
73
+
74
+ /**
75
+ * Claim the scope before forwarding. Inserts `in_flight`; on conflict it reads
76
+ * the existing row and never re-forwards a call whose outcome is already known
77
+ * or still unknown.
78
+ */
79
+ export const claimCapabilityIdempotency = async (scope: CapabilityClaimScope, requestHash: string): Promise<CapabilityClaimOutcome> => {
80
+ const [inserted] = await sql<{ inserted: boolean }[]>`
81
+ INSERT INTO capabilities.idempotency_claims (app_id, capability, principal, key_hash, request_hash, state)
82
+ VALUES (${scope.appId}, ${scope.capability}, ${scope.principal}, ${scope.keyHash}, ${requestHash}, 'in_flight')
83
+ ON CONFLICT (app_id, capability, principal, key_hash) DO NOTHING
84
+ RETURNING true AS inserted
85
+ `;
86
+ if (inserted) return { state: "claimed" };
87
+
88
+ const [existing] = await sql<ClaimRow[]>`
89
+ SELECT state, request_hash, response_status, response_body
90
+ FROM capabilities.idempotency_claims
91
+ WHERE app_id = ${scope.appId} AND capability = ${scope.capability}
92
+ AND principal = ${scope.principal} AND key_hash = ${scope.keyHash}
93
+ `;
94
+ // The row can disappear between the insert and the read when a concurrent
95
+ // attempt releases a definitive failure. Retrying the claim once is safe.
96
+ if (!existing) {
97
+ const [second] = await sql<{ inserted: boolean }[]>`
98
+ INSERT INTO capabilities.idempotency_claims (app_id, capability, principal, key_hash, request_hash, state)
99
+ VALUES (${scope.appId}, ${scope.capability}, ${scope.principal}, ${scope.keyHash}, ${requestHash}, 'in_flight')
100
+ ON CONFLICT (app_id, capability, principal, key_hash) DO NOTHING
101
+ RETURNING true AS inserted
102
+ `;
103
+ return second ? { state: "claimed" } : { state: "in_flight" };
104
+ }
105
+ if (existing.request_hash !== requestHash) return { state: "conflict" };
106
+ if (existing.state === "in_flight") return { state: "in_flight" };
107
+ if (existing.state === "uncertain") return { state: "uncertain" };
108
+ if (existing.response_status === null) return { state: "not_retained" };
109
+ return { state: "replay", status: existing.response_status, body: existing.response_body };
110
+ };
111
+
112
+ /** Records a definitive success so a later retry replays it instead of acting twice. */
113
+ export const completeCapabilityClaim = async (scope: CapabilityClaimScope, status: number, body: unknown): Promise<void> => {
114
+ let serialized: string | null = null;
115
+ try {
116
+ const encoded = JSON.stringify(body);
117
+ if (encoded !== undefined && new TextEncoder().encode(encoded).byteLength <= CAPABILITY_CLAIM_MAX_BODY_BYTES) serialized = encoded;
118
+ } catch {
119
+ serialized = null;
120
+ }
121
+ await sql`
122
+ UPDATE capabilities.idempotency_claims
123
+ SET state = 'succeeded',
124
+ response_status = ${serialized === null ? null : status},
125
+ response_body = ${serialized}::text::jsonb,
126
+ updated_at = now()
127
+ WHERE app_id = ${scope.appId} AND capability = ${scope.capability}
128
+ AND principal = ${scope.principal} AND key_hash = ${scope.keyHash}
129
+ `;
130
+ };
131
+
132
+ /** Releases a claim whose failure proves nothing happened, so a corrected retry can run. */
133
+ export const releaseCapabilityClaim = async (scope: CapabilityClaimScope): Promise<void> => {
134
+ await sql`
135
+ DELETE FROM capabilities.idempotency_claims
136
+ WHERE app_id = ${scope.appId} AND capability = ${scope.capability}
137
+ AND principal = ${scope.principal} AND key_hash = ${scope.keyHash}
138
+ AND state = 'in_flight'
139
+ `;
140
+ };
141
+
142
+ /** Freezes a claim whose request left the dispatcher without a usable answer. Never deleted by a retry. */
143
+ export const markCapabilityClaimUncertain = async (scope: CapabilityClaimScope): Promise<void> => {
144
+ await sql`
145
+ UPDATE capabilities.idempotency_claims
146
+ SET state = 'uncertain', updated_at = now()
147
+ WHERE app_id = ${scope.appId} AND capability = ${scope.capability}
148
+ AND principal = ${scope.principal} AND key_hash = ${scope.keyHash}
149
+ AND state = 'in_flight'
150
+ `;
151
+ };
152
+
153
+ /** Resolving a claim must never fail the call it describes; the outcome is already decided. */
154
+ export const resolveCapabilityClaim = async (resolve: Promise<void>, scope: CapabilityClaimScope): Promise<void> =>
155
+ resolve.catch((error: unknown) => {
156
+ log.error("Capability idempotency claim could not be resolved", {
157
+ capability: scope.capability,
158
+ error: error instanceof Error ? error.message : String(error),
159
+ });
160
+ });
161
+
162
+ const PRUNE_BATCH_SIZE = 5_000;
163
+
164
+ /** Deletes expired claims in bounded batches, alongside the execution retention sweep. */
165
+ export const pruneCapabilityIdempotencyClaims = async (olderThan: Date): Promise<number> => {
166
+ let total = 0;
167
+ let deleted: number;
168
+ do {
169
+ const result = await sql`
170
+ WITH expired AS (
171
+ SELECT ctid FROM capabilities.idempotency_claims WHERE created_at < ${olderThan} LIMIT ${PRUNE_BATCH_SIZE}
172
+ )
173
+ DELETE FROM capabilities.idempotency_claims claim USING expired WHERE claim.ctid = expired.ctid
174
+ `;
175
+ deleted = result.count;
176
+ total += deleted;
177
+ } while (deleted === PRUNE_BATCH_SIZE);
178
+ return total;
179
+ };
@@ -147,3 +147,37 @@ export const reviewCapabilityAction = async <TInput = unknown>(
147
147
  return unavailable(cause, undefined, clientLocale(options.headers));
148
148
  }
149
149
  };
150
+
151
+ /** Resolve an interactive entry; this never runs a domain mutation. */
152
+ export const resolveCommand = async (
153
+ command: string,
154
+ input: unknown = {},
155
+ commandOptions: import("../contracts/commands").CommandOptions = {},
156
+ options: CapabilityHttpOptions & { signal?: AbortSignal } = {},
157
+ ): Promise<import("./types").CapabilityResultState<{ href: string }>> => {
158
+ const { CommandRequestSchema, CommandLinkSchema } = await import("../contracts/commands");
159
+ const separator = command.indexOf(".");
160
+ if (separator < 1) throw new Error("Expected a qualified Command id");
161
+ const appId = command.slice(0, separator);
162
+ const localId = command.slice(separator + 1);
163
+ const body = CommandRequestSchema.parse({ input, ...commandOptions });
164
+ const headers = new Headers(options.headers);
165
+ headers.set("content-type", "application/json");
166
+ try {
167
+ const response = await (options.fetch ?? globalThis.fetch)(
168
+ requestUrl(options.baseUrl ?? "/api", `/capabilities/v1/commands/${encodeURIComponent(appId)}/${encodeURIComponent(localId)}`),
169
+ {
170
+ method: "POST",
171
+ headers,
172
+ credentials: "same-origin",
173
+ body: JSON.stringify(body),
174
+ signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000),
175
+ },
176
+ );
177
+ return await readCapabilityResponse(response, CommandLinkSchema);
178
+ } catch (cause) {
179
+ return unavailable(cause, undefined, clientLocale(options.headers));
180
+ }
181
+ };
182
+
183
+ export { type CapabilityStreamVerb, transferCapabilityStream } from "./streams";
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import type { CapabilityCommandManifest } from "../contracts/capabilities";
3
+ import { CommandLinkSchema, type CommandOptions, commandPath } from "../contracts/commands";
4
+ import { publicCloudOrigin } from "../shared/app-url";
5
+
6
+ export const resolveCommandLink = (
7
+ appId: string,
8
+ command: CapabilityCommandManifest,
9
+ input: unknown,
10
+ options: CommandOptions,
11
+ appUrl: string,
12
+ ) => {
13
+ const parsed = z.fromJSONSchema(structuredClone(command.inputSchema)).parse(input);
14
+ const path = commandPath(command.path, `${appId}.${command.localId}`, parsed, options);
15
+ return CommandLinkSchema.parse({ href: new URL(path, publicCloudOrigin(appUrl)).href });
16
+ };