@k2b/cloud 0.6.0-rc.0 → 0.7.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 (315) hide show
  1. package/README.md +2 -2
  2. package/package.json +30 -17
  3. package/scripts/browser-performance.ts +14 -0
  4. package/scripts/build-pdf-renderer.ts +39 -0
  5. package/scripts/build.ts +15 -35
  6. package/scripts/preload.ts +9 -12
  7. package/scripts/sync-recovery-smoke.ts +2 -1
  8. package/src/_internal/app-presentation.ts +14 -2
  9. package/src/_internal/capabilities.ts +108 -7
  10. package/src/_internal/capability-streams.ts +81 -0
  11. package/src/_internal/define-app.ts +62 -36
  12. package/src/_internal/heartbeat.ts +4 -0
  13. package/src/_internal/help-catalog.ts +27 -172
  14. package/src/_internal/help.ts +13 -76
  15. package/src/_internal/page-responses.ts +2 -0
  16. package/src/_internal/postgres-application-name.ts +30 -0
  17. package/src/_internal/process-identity.ts +15 -0
  18. package/src/_internal/process-sync.ts +1 -1
  19. package/src/_internal/registry-validation.ts +29 -25
  20. package/src/_internal/registry.ts +2 -53
  21. package/src/_internal/runtime-context.ts +5 -16
  22. package/src/_internal/server-timing.ts +13 -0
  23. package/src/_internal/status-preserving-ssr.ts +18 -6
  24. package/src/_internal/web-vitals-asset.ts +10 -0
  25. package/src/access/PermissionEditor.tsx +14 -153
  26. package/src/access/PrincipalPicker.tsx +104 -0
  27. package/src/access/ui.ts +2 -0
  28. package/src/ai/admin.ts +14 -13
  29. package/src/ai/assistant-models.ts +12 -1
  30. package/src/ai/audio-format.ts +34 -0
  31. package/src/ai/audio-tool.ts +90 -0
  32. package/src/ai/browser-code-contracts.ts +79 -0
  33. package/src/ai/browser.ts +2 -0
  34. package/src/ai/capabilities.ts +55 -36
  35. package/src/ai/capability-execution.ts +47 -12
  36. package/src/ai/chat/blocks.tsx +157 -95
  37. package/src/ai/chat/capability-result.ts +15 -0
  38. package/src/ai/chat/capability-table.tsx +2 -10
  39. package/src/ai/chat/composer-adapter.ts +48 -3
  40. package/src/ai/chat/message-actions.tsx +8 -7
  41. package/src/ai/chat/message-utils.ts +12 -1
  42. package/src/ai/chat/messages.ts +2 -0
  43. package/src/ai/chat/presentation.tsx +19 -48
  44. package/src/ai/chat/primitives.tsx +43 -1
  45. package/src/ai/chat/tool-disclosure.tsx +17 -2
  46. package/src/ai/chat/tool-groups.ts +52 -0
  47. package/src/ai/chat-quotas.ts +25 -0
  48. package/src/ai/chat-task-contracts.ts +9 -1
  49. package/src/ai/chat-tasks.ts +276 -43
  50. package/src/ai/client/controller.ts +110 -33
  51. package/src/ai/client/file-source.ts +12 -6
  52. package/src/ai/client/live-connection.ts +1 -0
  53. package/src/ai/client/projection.ts +22 -11
  54. package/src/ai/client/transport.ts +4 -0
  55. package/src/ai/code-capability-routes.ts +110 -0
  56. package/src/ai/code-capability-transport.ts +38 -0
  57. package/src/ai/code-mode-skill.ts +114 -0
  58. package/src/ai/code-runtime-tools.ts +166 -0
  59. package/src/ai/code-source-contracts.ts +202 -0
  60. package/src/ai/code-source-tools.ts +145 -0
  61. package/src/ai/compaction.ts +27 -6
  62. package/src/ai/data-analysis-skill.ts +10 -0
  63. package/src/ai/default-tools.ts +25 -2
  64. package/src/ai/dictation-runtime.ts +171 -0
  65. package/src/ai/dictations.ts +188 -0
  66. package/src/ai/draft-content.ts +34 -0
  67. package/src/ai/enrich.ts +6 -0
  68. package/src/ai/executor.ts +191 -31
  69. package/src/ai/file-content-version.ts +22 -0
  70. package/src/ai/file-context.ts +2 -1
  71. package/src/ai/file-media-type.ts +12 -0
  72. package/src/ai/file-reference-contracts.ts +15 -0
  73. package/src/ai/file-tools.ts +2 -1
  74. package/src/ai/files-store.ts +174 -71
  75. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  76. package/src/ai/grids-skill.ts +40 -10
  77. package/src/ai/http.ts +15 -8
  78. package/src/ai/index.ts +26 -2
  79. package/src/ai/inference-calls.ts +198 -0
  80. package/src/ai/live-events.ts +5 -1
  81. package/src/ai/memory-learning.ts +60 -28
  82. package/src/ai/message-queue.ts +167 -0
  83. package/src/ai/migrate.ts +187 -11
  84. package/src/ai/model-pricing.ts +43 -0
  85. package/src/ai/pdf-render-worker.ts +74 -0
  86. package/src/ai/pdf-render.fixture.ts +28 -0
  87. package/src/ai/pdf-render.ts +77 -0
  88. package/src/ai/projects-routes.ts +3 -3
  89. package/src/ai/projects.ts +20 -6
  90. package/src/ai/protocol.ts +3 -0
  91. package/src/ai/provider.ts +1 -8
  92. package/src/ai/quota-provider.ts +191 -0
  93. package/src/ai/quota-report.ts +152 -0
  94. package/src/ai/quotas-migrate.ts +45 -0
  95. package/src/ai/quotas.ts +168 -0
  96. package/src/ai/routes.ts +215 -11
  97. package/src/ai/run-timeout.ts +10 -0
  98. package/src/ai/runtime-tools.ts +3 -0
  99. package/src/ai/runtime.ts +55 -16
  100. package/src/ai/settings.ts +36 -8
  101. package/src/ai/short-id.ts +2 -2
  102. package/src/ai/skill-catalog.ts +0 -10
  103. package/src/ai/skill-search.ts +47 -0
  104. package/src/ai/skill-seeds.ts +105 -20
  105. package/src/ai/skill-tool.ts +16 -5
  106. package/src/ai/skills-routes.ts +32 -4
  107. package/src/ai/skills.ts +324 -155
  108. package/src/ai/store.ts +373 -191
  109. package/src/ai/stream.ts +5 -0
  110. package/src/ai/structured-runs.ts +4 -3
  111. package/src/ai/structured.ts +8 -1
  112. package/src/ai/system-prompt.ts +12 -2
  113. package/src/ai/timeline.ts +2 -5
  114. package/src/ai/todo-contracts.ts +20 -0
  115. package/src/ai/todo-tool.ts +16 -0
  116. package/src/ai/tool-audit.ts +30 -40
  117. package/src/ai/tools.ts +6 -0
  118. package/src/ai/transcription.ts +202 -0
  119. package/src/ai/turn-timing.ts +114 -0
  120. package/src/ai/types.ts +58 -10
  121. package/src/ai/ui.tsx +2 -0
  122. package/src/ai/usage.ts +118 -118
  123. package/src/ai/vision-tool.ts +39 -10
  124. package/src/api/admin-ai-projects.ts +2 -2
  125. package/src/api/admin-ai-quotas.ts +74 -0
  126. package/src/api/admin-ai-skills.ts +48 -5
  127. package/src/api/admin-ai-usage.ts +2 -2
  128. package/src/api/admin-core-settings.ts +25 -0
  129. package/src/api/admin-identity.ts +91 -51
  130. package/src/api/admin-rail.ts +58 -0
  131. package/src/api/announcements.ts +14 -0
  132. package/src/api/capabilities.ts +292 -43
  133. package/src/api/capability-streams.ts +262 -0
  134. package/src/api/help.ts +8 -44
  135. package/src/api/index.ts +6 -0
  136. package/src/api/mcp.ts +44 -26
  137. package/src/api/me-web-vitals.ts +35 -0
  138. package/src/api/me.ts +2 -0
  139. package/src/api/search/schemas.ts +26 -15
  140. package/src/api/search.bench.ts +1 -1
  141. package/src/api/search.ts +54 -17
  142. package/src/browser/CloudResourceSearch.tsx +640 -702
  143. package/src/browser/app-approval-vault.ts +19 -7
  144. package/src/browser/command-bridge.ts +109 -0
  145. package/src/browser/command-shortcuts.ts +76 -0
  146. package/src/browser/commands.ts +66 -0
  147. package/src/browser/navigation-search.ts +48 -0
  148. package/src/browser/notifications.ts +1 -1
  149. package/src/browser/resource-picker.tsx +5 -18
  150. package/src/browser/resource-search-dialog.ts +9 -0
  151. package/src/browser/resource-search-input.ts +46 -0
  152. package/src/browser/resource-search-messages.ts +76 -40
  153. package/src/browser/resource-search.ts +7 -0
  154. package/src/browser/search-bridge.ts +102 -0
  155. package/src/browser/search-commands.ts +59 -0
  156. package/src/browser/search.ts +13 -0
  157. package/src/browser/spotlight-position.ts +208 -0
  158. package/src/browser/testing.ts +5 -0
  159. package/src/browser/web-vitals.ts +25 -0
  160. package/src/capabilities/claims.ts +179 -0
  161. package/src/capabilities/client.ts +34 -0
  162. package/src/capabilities/command-link.ts +16 -0
  163. package/src/capabilities/executions.ts +298 -0
  164. package/src/capabilities/migrate.ts +81 -0
  165. package/src/capabilities/server.ts +114 -34
  166. package/src/capabilities/stream-body.ts +16 -0
  167. package/src/capabilities/streams.ts +28 -0
  168. package/src/cli/admin/ai-quotas.ts +161 -0
  169. package/src/cli/admin/ai-skills.ts +64 -0
  170. package/src/cli/admin/ai-usage.ts +53 -30
  171. package/src/cli/admin/app-credentials.ts +79 -0
  172. package/src/cli/admin/data.ts +17 -0
  173. package/src/cli/admin/index.ts +9 -0
  174. package/src/cli/capabilities.ts +29 -0
  175. package/src/cli/capability-streams.ts +25 -0
  176. package/src/cli/index.ts +3 -2
  177. package/src/config/env.ts +11 -8
  178. package/src/contracts/app.ts +23 -3
  179. package/src/contracts/capabilities.ts +68 -5
  180. package/src/contracts/capability-streams.ts +46 -0
  181. package/src/contracts/commands.ts +49 -0
  182. package/src/contracts/index.ts +6 -1
  183. package/src/contracts/rail-admin.ts +38 -0
  184. package/src/contracts/rail-preferences.ts +16 -0
  185. package/src/contracts/registry.ts +3 -27
  186. package/src/contracts/settings-types.ts +1 -1
  187. package/src/contracts/shared.ts +9 -5
  188. package/src/contracts/web-vitals.ts +27 -0
  189. package/src/desktop/solid.tsx +3 -3
  190. package/src/index.ts +0 -4
  191. package/src/server/index.ts +3 -0
  192. package/src/server/middleware/auth.ts +3 -2
  193. package/src/server/middleware/route-template.ts +9 -3
  194. package/src/server/middleware/runtime.ts +2 -1
  195. package/src/server/middleware/settings.ts +17 -11
  196. package/src/server/services/access-revision.ts +10 -0
  197. package/src/server/services/access.ts +1 -1
  198. package/src/server/services/index.ts +1 -0
  199. package/src/services/account-category-policy.ts +9 -0
  200. package/src/services/account-lifecycle/audit.ts +1 -1
  201. package/src/services/accounts/app.ts +12 -2
  202. package/src/services/accounts/email-write.ts +23 -0
  203. package/src/services/accounts/groups.ts +15 -4
  204. package/src/services/accounts/identities.ts +249 -0
  205. package/src/services/accounts/identity-reconciliation.ts +87 -0
  206. package/src/services/accounts/ipa-data.ts +2 -2
  207. package/src/services/accounts/local-groups.ts +2 -2
  208. package/src/services/accounts/posix.ts +45 -29
  209. package/src/services/accounts/users.ts +2 -2
  210. package/src/services/announcements/index.ts +69 -8
  211. package/src/services/audit/index.ts +1 -1
  212. package/src/services/cache-fill.ts +35 -0
  213. package/src/services/help/index.ts +109 -0
  214. package/src/services/help/maintenance.ts +25 -0
  215. package/src/services/help/store.ts +180 -0
  216. package/src/services/help/types.ts +46 -0
  217. package/src/services/identity/invocation-actor.ts +2 -2
  218. package/src/services/identity/key-config.ts +2 -1
  219. package/src/services/identity/key-ring.ts +1 -1
  220. package/src/services/index.ts +15 -0
  221. package/src/services/ipa/users.ts +268 -221
  222. package/src/services/legal-consent.ts +1 -1
  223. package/src/services/logging/index.ts +15 -13
  224. package/src/services/logging/trace.ts +2 -1
  225. package/src/services/logging/web-vitals.ts +81 -0
  226. package/src/services/mandates/index.ts +18 -4
  227. package/src/services/mandates/policy.ts +65 -2
  228. package/src/services/notifications/batches.ts +124 -57
  229. package/src/services/notifications/browser.ts +12 -0
  230. package/src/services/notifications/index.ts +0 -2
  231. package/src/services/notifications/platform.ts +0 -4
  232. package/src/services/oauth-tokens.ts +3 -3
  233. package/src/services/pdf/gotenberg.ts +107 -84
  234. package/src/services/pdf/index.ts +3 -0
  235. package/src/services/providers/local/users.ts +25 -16
  236. package/src/services/public-http.ts +101 -0
  237. package/src/services/rail-shortcuts.ts +95 -0
  238. package/src/services/rail-snapshot.ts +53 -0
  239. package/src/services/request-cache-redis.ts +28 -0
  240. package/src/services/session/index.ts +15 -2
  241. package/src/services/session/user.ts +62 -37
  242. package/src/services/settings/core-settings.ts +21 -28
  243. package/src/services/settings/defaults.ts +5 -0
  244. package/src/services/settings/store.ts +58 -84
  245. package/src/shared/ai-costs.ts +42 -0
  246. package/src/shared/ai-platform-prompt.ts +1 -1
  247. package/src/shared/ai-quotas.ts +136 -0
  248. package/src/shared/ai-usage.ts +6 -16
  249. package/src/shared/app-presentation.ts +21 -15
  250. package/src/shared/capability-messages.ts +14 -0
  251. package/src/shared/help.ts +6 -42
  252. package/src/shared/index.ts +6 -0
  253. package/src/ssr/AdminLayout.tsx +7 -5
  254. package/src/ssr/AdminSidebar.tsx +29 -15
  255. package/src/ssr/AppLaunchpad.island.tsx +24 -137
  256. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  257. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  258. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  259. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  260. package/src/ssr/Layout.tsx +17 -6
  261. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  262. package/src/ssr/LayoutFooter.tsx +1 -1
  263. package/src/ssr/LayoutHeader.tsx +46 -14
  264. package/src/ssr/LayoutHelp.tsx +23 -22
  265. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  266. package/src/ssr/LayoutRail.tsx +17 -9
  267. package/src/ssr/MobileNavigation.tsx +71 -0
  268. package/src/ssr/MobileProfileActions.tsx +49 -0
  269. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  270. package/src/ssr/RailApps.island.tsx +2 -2
  271. package/src/ssr/RailEditor.tsx +17 -2
  272. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  273. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  274. package/src/ssr/admin-navigation.ts +7 -1
  275. package/src/ssr/help.ts +29 -0
  276. package/src/ssr/islands/SearchBar.tsx +67 -0
  277. package/src/ssr/islands/index.ts +3 -1
  278. package/src/ssr/layout-context.ts +2 -2
  279. package/src/ssr/layout-help-search.ts +15 -0
  280. package/src/ssr/mobile-menu-history.ts +70 -0
  281. package/src/ssr/platform-messages.ts +16 -8
  282. package/src/ssr/profile-actions.ts +42 -0
  283. package/src/ssr/rail-context.ts +8 -4
  284. package/src/ssr/rail-messages.ts +2 -0
  285. package/src/ssr/rail-navigation.ts +3 -3
  286. package/src/ssr/workspace-navigation.ts +75 -0
  287. package/src/styles/effects.css +6 -6
  288. package/src/styles/global.css +54 -0
  289. package/src/styles/input.css +43 -41
  290. package/src/styles/resource-search.css +505 -0
  291. package/src/styles/utilities-feedback.css +13 -0
  292. package/src/styles/utilities-layout.css +1 -5
  293. package/src/styles/utilities-navigation.css +3 -39
  294. package/src/types/ambient.d.ts +5 -0
  295. package/src/workflows/ai/runtime.ts +5 -1
  296. package/src/workflows/ai/store.ts +2 -0
  297. package/src/workflows/ai-actions.ts +16 -13
  298. package/src/workflows/definition.ts +10 -5
  299. package/src/workflows/store/actions.ts +44 -20
  300. package/src/workflows/store/index.ts +2 -0
  301. package/src/workflows/store/runs.ts +36 -0
  302. package/src/workflows/store/worker-pool.ts +55 -0
  303. package/src/workflows/store/worker-runtime.ts +90 -0
  304. package/scripts/app-favicon.test.ts +0 -49
  305. package/src/ai/chat/blocks.render.test.tsx +0 -1108
  306. package/src/ai/kit-skill.ts +0 -22
  307. package/src/ai/usage-migrate.ts +0 -81
  308. package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
  309. package/src/browser/notification-target.ts +0 -27
  310. package/src/contracts/notification-live.ts +0 -66
  311. package/src/services/notifications/live.ts +0 -50
  312. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  313. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
  314. package/src/ssr/Layout.render.test.tsx +0 -148
  315. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -0,0 +1,298 @@
1
+ import { sql } from "bun";
2
+ import type { CapabilityOrigin } from "../contracts/capabilities";
3
+ import { logger } from "../services/logging";
4
+ import { CAPABILITY_CLAIM_RETENTION_HOURS, pruneCapabilityIdempotencyClaims } from "./claims";
5
+
6
+ export {
7
+ CAPABILITY_CLAIM_MAX_BODY_BYTES,
8
+ type CapabilityClaimOutcome,
9
+ type CapabilityClaimScope,
10
+ type CapabilityClaimState,
11
+ capabilityIdempotencyKeyHash,
12
+ capabilityRequestHash,
13
+ claimCapabilityIdempotency,
14
+ completeCapabilityClaim,
15
+ markCapabilityClaimUncertain,
16
+ releaseCapabilityClaim,
17
+ resolveCapabilityClaim,
18
+ } from "./claims";
19
+ export { migrateCloudCapabilities } from "./migrate";
20
+ export { CAPABILITY_CLAIM_RETENTION_HOURS };
21
+
22
+ const log = logger("capabilities:executions");
23
+
24
+ export type CapabilityExecutionStatus = "succeeded" | "failed" | "denied" | "invalid_input" | "timed_out" | "rejected";
25
+
26
+ export type CapabilityValueMeta =
27
+ | { type: "null" }
28
+ | { type: "array"; length: number }
29
+ | { type: "object"; keys: string[]; omittedKeys: number }
30
+ | { type: "string"; length: number }
31
+ | { type: "number"; value: number }
32
+ | { type: "boolean"; value: boolean }
33
+ | { type: "unknown" };
34
+
35
+ const META_KEY_LIMIT = 20;
36
+
37
+ /** Shape-only projection of one capability input or result. Never stores values of strings, arrays, or nested objects. */
38
+ export const capabilityValueMeta = (value: unknown): CapabilityValueMeta => {
39
+ if (value === null) return { type: "null" };
40
+ if (Array.isArray(value)) return { type: "array", length: value.length };
41
+ if (typeof value === "string") return { type: "string", length: value.length };
42
+ if (typeof value === "number") return { type: "number", value };
43
+ if (typeof value === "boolean") return { type: "boolean", value };
44
+ if (typeof value === "object") {
45
+ const keys = Object.keys(value as Record<string, unknown>);
46
+ return { type: "object", keys: keys.slice(0, META_KEY_LIMIT), omittedKeys: Math.max(0, keys.length - META_KEY_LIMIT) };
47
+ }
48
+ return { type: "unknown" };
49
+ };
50
+
51
+ export type CapabilityExecutionInput = {
52
+ /** Correlation id shared with the invocation token, the app audit row, and request logs. */
53
+ requestId: string;
54
+ origin: CapabilityOrigin;
55
+ appId: string;
56
+ /** Fully qualified capability name, such as `contacts.list`. */
57
+ capability: string;
58
+ kind: "query" | "action";
59
+ destructive: boolean;
60
+ actorKind?: "user" | "service_account" | null;
61
+ actorId?: string | null;
62
+ userId?: string | null;
63
+ /** Recorded only when the access subject differs from the acting principal. */
64
+ accessSubject?: { type: "user" | "service_account"; id: string } | null;
65
+ status: CapabilityExecutionStatus;
66
+ errorCode?: string | null;
67
+ inputMeta?: CapabilityValueMeta | null;
68
+ outputMeta?: CapabilityValueMeta | null;
69
+ idempotencyKey?: string | null;
70
+ /** True when the dispatcher replayed a stored idempotent result instead of forwarding. */
71
+ replayed?: boolean;
72
+ startedAt: Date;
73
+ completedAt: Date;
74
+ };
75
+
76
+ export type CapabilityExecution = {
77
+ id: string;
78
+ requestId: string;
79
+ origin: CapabilityOrigin;
80
+ appId: string;
81
+ capability: string;
82
+ kind: "query" | "action";
83
+ destructive: boolean;
84
+ actorKind: "user" | "service_account" | null;
85
+ actorId: string | null;
86
+ userId: string | null;
87
+ accessSubjectType: "user" | "service_account" | null;
88
+ accessSubjectId: string | null;
89
+ status: CapabilityExecutionStatus;
90
+ errorCode: string | null;
91
+ inputMeta: CapabilityValueMeta | null;
92
+ outputMeta: CapabilityValueMeta | null;
93
+ idempotencyKey: string | null;
94
+ replayed: boolean;
95
+ startedAt: string;
96
+ completedAt: string;
97
+ durationMs: number;
98
+ };
99
+
100
+ /** Insert one execution record. Called only by the capability dispatcher and the surfaces that deny before it. */
101
+ export const recordCapabilityExecution = async (input: CapabilityExecutionInput): Promise<string> => {
102
+ const durationMs = Math.max(0, Math.round(input.completedAt.getTime() - input.startedAt.getTime()));
103
+ const [row] = await sql<{ id: string }[]>`
104
+ INSERT INTO capabilities.executions (
105
+ request_id, origin, app_id, capability, kind, destructive,
106
+ actor_kind, actor_id, user_id, access_subject_type, access_subject_id,
107
+ status, error_code, input_meta, output_meta, idempotency_key, replayed,
108
+ started_at, completed_at, duration_ms
109
+ ) VALUES (
110
+ ${input.requestId}, ${input.origin}, ${input.appId}, ${input.capability}, ${input.kind}, ${input.destructive},
111
+ ${input.actorKind ?? null}, ${input.actorId ?? null}, ${input.userId ?? null},
112
+ ${input.accessSubject?.type ?? null}, ${input.accessSubject?.id ?? null},
113
+ ${input.status}, ${input.errorCode ?? null},
114
+ ${input.inputMeta ? JSON.stringify(input.inputMeta) : null}::text::jsonb,
115
+ ${input.outputMeta ? JSON.stringify(input.outputMeta) : null}::text::jsonb,
116
+ ${input.idempotencyKey ?? null}, ${input.replayed ?? false}, ${input.startedAt}, ${input.completedAt}, ${durationMs}
117
+ )
118
+ RETURNING id::text AS id
119
+ `;
120
+ return row!.id;
121
+ };
122
+
123
+ export type CapabilityExecutionFilter = {
124
+ appId?: string;
125
+ capability?: string;
126
+ origin?: CapabilityOrigin;
127
+ status?: CapabilityExecutionStatus;
128
+ userId?: string;
129
+ destructive?: boolean;
130
+ requestId?: string;
131
+ since?: Date;
132
+ until?: Date;
133
+ };
134
+
135
+ const DEFAULT_PAGE_SIZE = 50;
136
+ const MAX_PAGE_SIZE = 200;
137
+
138
+ const filterSql = (filter: CapabilityExecutionFilter) => sql`
139
+ (${filter.appId ?? null}::text IS NULL OR execution.app_id = ${filter.appId ?? null})
140
+ AND (${filter.capability ?? null}::text IS NULL OR execution.capability = ${filter.capability ?? null})
141
+ AND (${filter.origin ?? null}::text IS NULL OR execution.origin = ${filter.origin ?? null})
142
+ AND (${filter.status ?? null}::text IS NULL OR execution.status = ${filter.status ?? null})
143
+ AND (${filter.userId ?? null}::uuid IS NULL OR execution.user_id = ${filter.userId ?? null}::uuid)
144
+ AND (${filter.requestId ?? null}::text IS NULL OR execution.request_id = ${filter.requestId ?? null})
145
+ AND (${filter.destructive ?? null}::boolean IS NULL OR execution.destructive = ${filter.destructive ?? null})
146
+ AND (${filter.since ?? null}::timestamptz IS NULL OR execution.started_at >= ${filter.since ?? null})
147
+ AND (${filter.until ?? null}::timestamptz IS NULL OR execution.started_at <= ${filter.until ?? null})
148
+ `;
149
+
150
+ const columns = () => sql`
151
+ execution.id::text AS id, execution.request_id AS "requestId", execution.origin, execution.app_id AS "appId",
152
+ execution.capability, execution.kind, execution.destructive,
153
+ execution.actor_kind AS "actorKind", execution.actor_id::text AS "actorId", execution.user_id::text AS "userId",
154
+ execution.access_subject_type AS "accessSubjectType", execution.access_subject_id::text AS "accessSubjectId",
155
+ execution.status, execution.error_code AS "errorCode", execution.input_meta AS "inputMeta",
156
+ execution.output_meta AS "outputMeta", execution.idempotency_key AS "idempotencyKey", execution.replayed,
157
+ to_json(execution.started_at) #>> '{}' AS "startedAt", to_json(execution.completed_at) #>> '{}' AS "completedAt",
158
+ execution.duration_ms AS "durationMs"
159
+ `;
160
+
161
+ /** The driver returns jsonb as text; execution rows are a public contract, so parse once here. */
162
+ const meta = (value: unknown): CapabilityValueMeta | null =>
163
+ typeof value === "string" ? (JSON.parse(value) as CapabilityValueMeta) : ((value as CapabilityValueMeta | null) ?? null);
164
+
165
+ const toExecution = (row: CapabilityExecution): CapabilityExecution => ({
166
+ ...row,
167
+ inputMeta: meta(row.inputMeta),
168
+ outputMeta: meta(row.outputMeta),
169
+ });
170
+
171
+ const encodeCursor = (row: CapabilityExecution): string => `${row.startedAt}|${row.id}`;
172
+ const decodeCursor = (cursor: string): { startedAt: string; id: string } | null => {
173
+ const separator = cursor.lastIndexOf("|");
174
+ if (separator <= 0) return null;
175
+ return { startedAt: cursor.slice(0, separator), id: cursor.slice(separator + 1) };
176
+ };
177
+
178
+ /** Newest first, keyset paginated on (started_at, id). The page size is always bounded. */
179
+ export const listCapabilityExecutions = async (
180
+ input: CapabilityExecutionFilter & { limit?: number; cursor?: string } = {},
181
+ ): Promise<{ items: CapabilityExecution[]; nextCursor?: string }> => {
182
+ const limit = Math.min(MAX_PAGE_SIZE, Math.max(1, Math.trunc(input.limit ?? DEFAULT_PAGE_SIZE)));
183
+ const cursor = input.cursor ? decodeCursor(input.cursor) : null;
184
+ const rows = await sql<CapabilityExecution[]>`
185
+ SELECT ${columns()}
186
+ FROM capabilities.executions execution
187
+ WHERE ${filterSql(input)}
188
+ AND (${cursor?.startedAt ?? null}::timestamptz IS NULL
189
+ OR (execution.started_at, execution.id) < (${cursor?.startedAt ?? null}::timestamptz, ${cursor?.id ?? null}::uuid))
190
+ ORDER BY execution.started_at DESC, execution.id DESC
191
+ LIMIT ${limit + 1}
192
+ `;
193
+ const items = rows.slice(0, limit).map(toExecution);
194
+ const last = items.at(-1);
195
+ return { items, ...(rows.length > limit && last ? { nextCursor: encodeCursor(last) } : {}) };
196
+ };
197
+
198
+ export type CapabilityExecutionGroup = { id: string; executions: number; failed: number; denied: number; destructive: number };
199
+ export type CapabilityExecutionSummary = {
200
+ executions: number;
201
+ failed: number;
202
+ denied: number;
203
+ destructive: number;
204
+ avgDurationMs: number | null;
205
+ p95DurationMs: number | null;
206
+ apps: CapabilityExecutionGroup[];
207
+ capabilities: CapabilityExecutionGroup[];
208
+ };
209
+
210
+ const MAX_SUMMARY_GROUPS = 50;
211
+
212
+ const groupCounts = () => sql`
213
+ count(*)::int AS executions,
214
+ count(*) FILTER (WHERE execution.status IN ('failed', 'timed_out', 'invalid_input'))::int AS failed,
215
+ count(*) FILTER (WHERE execution.status IN ('denied', 'rejected'))::int AS denied,
216
+ count(*) FILTER (WHERE execution.destructive)::int AS destructive
217
+ `;
218
+
219
+ /** Counts and latency for one time range. Group lists are bounded to the busiest entries. */
220
+ export const summarizeCapabilityExecutions = async (filter: CapabilityExecutionFilter = {}): Promise<CapabilityExecutionSummary> => {
221
+ const scope = filterSql(filter);
222
+ const [totals, apps, capabilities] = await Promise.all([
223
+ sql<Omit<CapabilityExecutionSummary, "apps" | "capabilities">[]>`
224
+ SELECT ${groupCounts()},
225
+ avg(execution.duration_ms)::double precision AS "avgDurationMs",
226
+ percentile_cont(0.95) WITHIN GROUP (ORDER BY execution.duration_ms)::double precision AS "p95DurationMs"
227
+ FROM capabilities.executions execution WHERE ${scope}
228
+ `,
229
+ sql<CapabilityExecutionGroup[]>`
230
+ SELECT execution.app_id AS id, ${groupCounts()}
231
+ FROM capabilities.executions execution WHERE ${scope}
232
+ GROUP BY execution.app_id ORDER BY executions DESC, id LIMIT ${MAX_SUMMARY_GROUPS}
233
+ `,
234
+ sql<CapabilityExecutionGroup[]>`
235
+ SELECT execution.capability AS id, ${groupCounts()}
236
+ FROM capabilities.executions execution WHERE ${scope}
237
+ GROUP BY execution.capability ORDER BY executions DESC, id LIMIT ${MAX_SUMMARY_GROUPS}
238
+ `,
239
+ ]);
240
+ const summary = totals[0]!;
241
+ return { ...summary, apps, capabilities };
242
+ };
243
+
244
+ const PRUNE_BATCH_SIZE = 5_000;
245
+
246
+ /** Deletes expired rows in bounded batches so one call cannot hold a long transaction. */
247
+ export const pruneCapabilityExecutions = async (olderThan: Date): Promise<number> => {
248
+ let total = 0;
249
+ let deleted: number;
250
+ do {
251
+ const result = await sql`
252
+ WITH expired AS (
253
+ SELECT ctid FROM capabilities.executions WHERE started_at < ${olderThan} LIMIT ${PRUNE_BATCH_SIZE}
254
+ )
255
+ DELETE FROM capabilities.executions execution USING expired WHERE execution.ctid = expired.ctid
256
+ `;
257
+ deleted = result.count;
258
+ total += deleted;
259
+ } while (deleted === PRUNE_BATCH_SIZE);
260
+ return total;
261
+ };
262
+
263
+ /**
264
+ * Execution history is operational evidence, not durable app data. Ninety days
265
+ * covers a quarterly review of what agents did without an unbounded table; there
266
+ * is no operator setting for it yet, so the window is a platform constant.
267
+ */
268
+ export const CAPABILITY_EXECUTION_RETENTION_DAYS = 90;
269
+ const PRUNE_INTERVAL_MS = 6 * 60 * 60 * 1000;
270
+
271
+ let pruneTimer: ReturnType<typeof setInterval> | null = null;
272
+ let pruneRunning = false;
273
+
274
+ /** Starts the retention sweep. Mirrors the mandate maintenance loop and is started once per Cloud runtime. */
275
+ export const startCapabilityExecutionMaintenance = (): (() => void) => {
276
+ if (pruneTimer) return () => undefined;
277
+ const prune = async (): Promise<void> => {
278
+ if (pruneRunning) return;
279
+ pruneRunning = true;
280
+ try {
281
+ const removed = await pruneCapabilityExecutions(new Date(Date.now() - CAPABILITY_EXECUTION_RETENTION_DAYS * 86_400_000));
282
+ if (removed > 0) log.info("Pruned expired capability executions", { removed });
283
+ const claims = await pruneCapabilityIdempotencyClaims(new Date(Date.now() - CAPABILITY_CLAIM_RETENTION_HOURS * 3_600_000));
284
+ if (claims > 0) log.info("Pruned expired capability idempotency claims", { removed: claims });
285
+ } catch (error) {
286
+ log.error("Capability retention sweep failed", { error: error instanceof Error ? error.message : String(error) });
287
+ } finally {
288
+ pruneRunning = false;
289
+ }
290
+ };
291
+ void prune();
292
+ pruneTimer = setInterval(() => void prune(), PRUNE_INTERVAL_MS);
293
+ pruneTimer.unref?.();
294
+ return () => {
295
+ if (pruneTimer) clearInterval(pruneTimer);
296
+ pruneTimer = null;
297
+ };
298
+ };
@@ -0,0 +1,81 @@
1
+ import { sql } from "bun";
2
+
3
+ /**
4
+ * Platform-owned capability execution history.
5
+ *
6
+ * Every capability invocation that leaves Cloud goes through
7
+ * `dispatchCapability`, and that dispatcher is the only writer of this table.
8
+ * The row records shape and outcome, never payloads, and shares `request_id`
9
+ * with the invocation token and the owning app's audit rows.
10
+ */
11
+ export const migrateCloudCapabilities = async (): Promise<void> => {
12
+ await sql`CREATE SCHEMA IF NOT EXISTS capabilities`.simple();
13
+
14
+ await sql`
15
+ CREATE TABLE IF NOT EXISTS capabilities.executions (
16
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17
+ request_id TEXT NOT NULL,
18
+ origin TEXT NOT NULL,
19
+ app_id TEXT NOT NULL,
20
+ capability TEXT NOT NULL,
21
+ kind TEXT NOT NULL,
22
+ destructive BOOLEAN NOT NULL DEFAULT false,
23
+ actor_kind TEXT,
24
+ actor_id UUID,
25
+ user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
26
+ access_subject_type TEXT,
27
+ access_subject_id UUID,
28
+ status TEXT NOT NULL,
29
+ error_code TEXT,
30
+ input_meta JSONB,
31
+ output_meta JSONB,
32
+ idempotency_key TEXT,
33
+ started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
34
+ completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
35
+ duration_ms INTEGER NOT NULL DEFAULT 0,
36
+ CONSTRAINT capabilities_executions_origin_check CHECK (origin IN ('assistant', 'mcp', 'http', 'app')),
37
+ CONSTRAINT capabilities_executions_kind_check CHECK (kind IN ('query', 'action')),
38
+ CONSTRAINT capabilities_executions_actor_kind_check CHECK (actor_kind IS NULL OR actor_kind IN ('user', 'service_account')),
39
+ CONSTRAINT capabilities_executions_subject_check CHECK (access_subject_type IS NULL OR access_subject_type IN ('user', 'service_account')),
40
+ CONSTRAINT capabilities_executions_status_check CHECK (status IN ('succeeded', 'failed', 'denied', 'invalid_input', 'timed_out', 'rejected'))
41
+ )
42
+ `.simple();
43
+
44
+ await sql`CREATE INDEX IF NOT EXISTS idx_capabilities_executions_started ON capabilities.executions(started_at DESC, id DESC)`.simple();
45
+ await sql`
46
+ CREATE INDEX IF NOT EXISTS idx_capabilities_executions_app_started
47
+ ON capabilities.executions(app_id, started_at DESC, id DESC)
48
+ `.simple();
49
+ await sql`CREATE INDEX IF NOT EXISTS idx_capabilities_executions_request ON capabilities.executions(request_id)`.simple();
50
+ await sql`
51
+ CREATE INDEX IF NOT EXISTS idx_capabilities_executions_user_started
52
+ ON capabilities.executions(user_id, started_at DESC, id DESC)
53
+ WHERE user_id IS NOT NULL
54
+ `.simple();
55
+
56
+ await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS replayed BOOLEAN NOT NULL DEFAULT false`.simple();
57
+
58
+ // Platform-owned idempotency claims. One row per
59
+ // (app, capability, principal, key); the dispatcher claims before it
60
+ // forwards and resolves the row once the outcome is known. Response bodies
61
+ // are retained only to replay a lost answer and expire with the claim.
62
+ await sql`
63
+ CREATE TABLE IF NOT EXISTS capabilities.idempotency_claims (
64
+ app_id TEXT NOT NULL,
65
+ capability TEXT NOT NULL,
66
+ principal TEXT NOT NULL,
67
+ key_hash TEXT NOT NULL,
68
+ request_hash TEXT NOT NULL,
69
+ state TEXT NOT NULL,
70
+ response_status INTEGER,
71
+ response_body JSONB,
72
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
73
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
74
+ PRIMARY KEY (app_id, capability, principal, key_hash),
75
+ CONSTRAINT capabilities_claims_state_check CHECK (state IN ('in_flight', 'succeeded', 'uncertain'))
76
+ )
77
+ `.simple();
78
+ await sql`CREATE INDEX IF NOT EXISTS idx_capabilities_claims_created ON capabilities.idempotency_claims(created_at)`.simple();
79
+
80
+ console.log(" ✓ capability execution history");
81
+ };
@@ -1,8 +1,14 @@
1
1
  import { z } from "zod";
2
2
  import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
3
3
  import { getApp, getCapability } from "../_internal/registry";
4
- import { loadCapabilityCatalogPage } from "../api/capabilities";
5
- import { CAPABILITY_FRAMEWORK_ERROR_CODES, CapabilityActionReviewSchema, capabilityResultSchema } from "../contracts/capabilities";
4
+ import { dispatchCapability, loadCapabilityCatalogPage } from "../api/capabilities";
5
+ import {
6
+ CAPABILITY_FRAMEWORK_ERROR_CODES,
7
+ CapabilityActionReviewSchema,
8
+ type CapabilityOrigin,
9
+ capabilityResultSchema,
10
+ } from "../contracts/capabilities";
11
+ import type { RequestAuthority } from "../server";
6
12
  import { get } from "../services/settings";
7
13
  import { resolveAppPresentation } from "../shared/app-presentation";
8
14
  import { publicCloudOrigin } from "../shared/app-url";
@@ -29,6 +35,12 @@ export type {
29
35
  } from "./types";
30
36
 
31
37
  export type CapabilityCaller = {
38
+ /** Core-only in-process authority; other applications must use a Core transport. */
39
+ authority?: RequestAuthority;
40
+ /** Trusted server transport to Core. Never supplied by a browser or sandbox. */
41
+ transport?: (request: Request) => Promise<Response>;
42
+ origin?: CapabilityOrigin;
43
+
32
44
  authorization?: string | null;
33
45
  cookie?: string | null;
34
46
  requestId?: string | null;
@@ -108,31 +120,49 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
108
120
  const path = caller.mandate
109
121
  ? "/api/_internal/identity/v1/invoke"
110
122
  : `/api/capabilities/v1/${invocation.kind === "query" ? "queries" : "actions"}/${encodeURIComponent(invocation.appId)}/${encodeURIComponent(invocation.capabilityId)}`;
111
- const response = await fetch(await callerRequest(caller, path, invocation.idempotencyKey, invocation.signal), {
112
- body: JSON.stringify(
113
- caller.mandate
114
- ? {
115
- kind: invocation.kind,
116
- targetApp: invocation.appId,
117
- capabilityId: invocation.capabilityId,
118
- input: invocation.input,
119
- mandateId: caller.mandate.id,
120
- mandateRevision: caller.mandate.revision,
121
- ...(invocation.idempotencyKey ? { idempotencyKey: invocation.idempotencyKey } : {}),
122
- ...(caller.requestId || caller.traceparent || caller.tracestate || caller.locale
123
- ? {
124
- metadata: {
125
- ...(caller.requestId ? { requestId: caller.requestId } : {}),
126
- ...(caller.traceparent ? { traceparent: caller.traceparent } : {}),
127
- ...(caller.tracestate ? { tracestate: caller.tracestate } : {}),
128
- ...(caller.locale ? { locale: caller.locale } : {}),
129
- },
130
- }
131
- : {}),
132
- }
133
- : { input: invocation.input },
134
- ),
135
- });
123
+ const request = await callerRequest(caller, path, invocation.idempotencyKey, invocation.signal);
124
+ if (caller.authority) {
125
+ if (caller.authorization || caller.cookie || caller.mandate || caller.transport)
126
+ throw new Error("Use exactly one capability authority source");
127
+ const outcome = await dispatchCapability({
128
+ request,
129
+ kind: invocation.kind === "query" ? "queries" : "actions",
130
+ appId: invocation.appId,
131
+ capabilityId: invocation.capabilityId,
132
+ input: invocation.input,
133
+ authority: caller.authority,
134
+ locale: caller.locale ?? undefined,
135
+ origin: caller.origin ?? "app",
136
+ });
137
+ return readCapabilityResponse(outcome, capabilityResultSchema(dataSchema, { consumer: true }));
138
+ }
139
+ const response = await (caller.transport ?? fetch)(
140
+ new Request(request, {
141
+ body: JSON.stringify(
142
+ caller.mandate
143
+ ? {
144
+ kind: invocation.kind,
145
+ targetApp: invocation.appId,
146
+ capabilityId: invocation.capabilityId,
147
+ input: invocation.input,
148
+ mandateId: caller.mandate.id,
149
+ mandateRevision: caller.mandate.revision,
150
+ ...(invocation.idempotencyKey ? { idempotencyKey: invocation.idempotencyKey } : {}),
151
+ ...(caller.requestId || caller.traceparent || caller.tracestate || caller.locale
152
+ ? {
153
+ metadata: {
154
+ ...(caller.requestId ? { requestId: caller.requestId } : {}),
155
+ ...(caller.traceparent ? { traceparent: caller.traceparent } : {}),
156
+ ...(caller.tracestate ? { tracestate: caller.tracestate } : {}),
157
+ ...(caller.locale ? { locale: caller.locale } : {}),
158
+ },
159
+ }
160
+ : {}),
161
+ }
162
+ : { input: invocation.input },
163
+ ),
164
+ }),
165
+ );
136
166
  return readCapabilityResponse(response, capabilityResultSchema(dataSchema, { consumer: true }));
137
167
  } catch (cause) {
138
168
  return unavailable(cause, invocation, caller.locale ?? undefined);
@@ -199,17 +229,67 @@ export const reviewCapabilityAction = async <TInput = unknown>(
199
229
  caller: CapabilityCaller,
200
230
  ): Promise<CapabilityReviewClientResult> => {
201
231
  try {
202
- const response = await fetch(
203
- await callerRequest(
204
- caller,
205
- `/api/capabilities/v1/actions/${encodeURIComponent(invocation.appId)}/${encodeURIComponent(invocation.capabilityId)}/review`,
206
- undefined,
207
- invocation.signal,
232
+ if (caller.authority) {
233
+ if (caller.authorization || caller.cookie || caller.mandate || caller.transport)
234
+ throw new Error("Use exactly one capability authority source");
235
+ const request = await callerRequest(caller, "/api/capabilities/review", undefined, invocation.signal);
236
+ const outcome = await dispatchCapability({
237
+ request,
238
+ kind: "actions",
239
+ review: true,
240
+ appId: invocation.appId,
241
+ capabilityId: invocation.capabilityId,
242
+ input: invocation.input,
243
+ authority: caller.authority,
244
+ locale: caller.locale ?? undefined,
245
+ origin: caller.origin ?? "app",
246
+ });
247
+ return readCapabilityResponse(outcome, CapabilityActionReviewSchema);
248
+ }
249
+ const response = await (caller.transport ?? fetch)(
250
+ new Request(
251
+ await callerRequest(
252
+ caller,
253
+ `/api/capabilities/v1/actions/${encodeURIComponent(invocation.appId)}/${encodeURIComponent(invocation.capabilityId)}/review`,
254
+ undefined,
255
+ invocation.signal,
256
+ ),
257
+ { body: JSON.stringify({ input: invocation.input }) },
208
258
  ),
209
- { body: JSON.stringify({ input: invocation.input }) },
210
259
  );
211
260
  return readCapabilityResponse(response, CapabilityActionReviewSchema);
212
261
  } catch (cause) {
213
262
  return unavailable(cause, undefined, caller.locale ?? undefined);
214
263
  }
215
264
  };
265
+
266
+ /** Authenticated continuation of a capability stream, with no app-specific URL handling. */
267
+ export async function transferCapabilityStream(
268
+ stream: import("../contracts/capabilities").CapabilityStream,
269
+ verb: import("./streams").CapabilityStreamVerb,
270
+ caller: CapabilityCaller,
271
+ body?: BodyInit,
272
+ ): Promise<Response> {
273
+ if (caller.mandate) throw new Error("Capability streams do not support background mandates yet");
274
+ const path = `/api/capabilities/v1/streams/${verb}`;
275
+ const headers = new Headers();
276
+ headers.set("x-cloud-stream-id", stream.id);
277
+ headers.set("content-type", "application/octet-stream");
278
+ if (caller.locale) headers.set(LOCALE_HEADER, caller.locale);
279
+ if (caller.authorization) headers.set("authorization", caller.authorization);
280
+ if (caller.cookie) headers.set("cookie", caller.cookie);
281
+ const request = new Request(new URL(path, await coreOrigin()), {
282
+ method: "POST",
283
+ headers,
284
+ body,
285
+ signal: caller.signal,
286
+ // @ts-expect-error Streaming server request bodies require duplex.
287
+ duplex: body instanceof ReadableStream ? "half" : undefined,
288
+ });
289
+ if (caller.authority) {
290
+ if (caller.cookie || caller.authorization || caller.transport) throw new Error("Use one authority source");
291
+ const { dispatchCapabilityStream } = await import("../api/capability-streams");
292
+ return dispatchCapabilityStream(request, caller.authority, verb);
293
+ }
294
+ return caller.transport ? caller.transport(request) : fetch(request, { redirect: "error" });
295
+ }
@@ -0,0 +1,16 @@
1
+ /** Enforce the declared byte count while preserving backpressure and cancellation. */
2
+ export function exactStream(body: ReadableStream<Uint8Array>, size: number): ReadableStream<Uint8Array> {
3
+ let count = 0;
4
+ return body.pipeThrough(
5
+ new TransformStream<Uint8Array, Uint8Array>({
6
+ transform(chunk, controller) {
7
+ count += chunk.byteLength;
8
+ if (count > size) throw new Error("Stream exceeds declared size");
9
+ controller.enqueue(chunk);
10
+ },
11
+ flush() {
12
+ if (count !== size) throw new Error("Stream ended before declared size");
13
+ },
14
+ }),
15
+ );
16
+ }
@@ -0,0 +1,28 @@
1
+ import { type CapabilityStream, CapabilityStreamSchema } from "../contracts/capabilities";
2
+ import type { CapabilityHttpOptions } from "./types";
3
+
4
+ export type CapabilityStreamVerb = "read" | "write" | "status" | "abort";
5
+ /** Binary bodies never pass through the JSON capability envelope. */
6
+ export async function transferCapabilityStream(
7
+ stream: CapabilityStream,
8
+ verb: CapabilityStreamVerb,
9
+ options: CapabilityHttpOptions & {
10
+ body?: BodyInit;
11
+ signal?: AbortSignal;
12
+ } = {},
13
+ ): Promise<Response> {
14
+ const ref = CapabilityStreamSchema.parse(stream);
15
+ const headers = new Headers(options.headers);
16
+ headers.set("x-cloud-stream-id", ref.id);
17
+ headers.set("content-type", "application/octet-stream");
18
+ return (options.fetch ?? fetch)(`${(options.baseUrl ?? "/api").replace(/\/$/, "")}/capabilities/v1/streams/${verb}`, {
19
+ method: "POST",
20
+ headers,
21
+ body: options.body,
22
+ signal: options.signal,
23
+ credentials: "same-origin",
24
+ redirect: "error",
25
+ // @ts-expect-error Server clients may supply a ReadableStream.
26
+ duplex: options.body instanceof ReadableStream ? "half" : undefined,
27
+ });
28
+ }