@xemahq/ui-kernel 0.9.1 → 0.10.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 (284) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/biome-host/biome-builders.d.ts.map +1 -1
  3. package/dist/lib/biome-host/biome-builders.js +3 -1
  4. package/dist/lib/biome-host/biome-builders.js.map +1 -1
  5. package/dist/lib/biome-host/biome-registry.d.ts.map +1 -1
  6. package/dist/lib/biome-host/biome-registry.js.map +1 -1
  7. package/dist/lib/biome-host/biome-scope-hooks.d.ts +11 -0
  8. package/dist/lib/biome-host/biome-scope-hooks.d.ts.map +1 -0
  9. package/dist/lib/biome-host/biome-scope-hooks.js +25 -0
  10. package/dist/lib/biome-host/biome-scope-hooks.js.map +1 -0
  11. package/dist/lib/biome-host/biome-scope.d.ts +1 -10
  12. package/dist/lib/biome-host/biome-scope.d.ts.map +1 -1
  13. package/dist/lib/biome-host/biome-scope.js +1 -24
  14. package/dist/lib/biome-host/biome-scope.js.map +1 -1
  15. package/dist/lib/biome-host/biome-scoped-query.d.ts +1 -1
  16. package/dist/lib/biome-host/biome-scoped-query.d.ts.map +1 -1
  17. package/dist/lib/biome-host/biome-scoped-query.js +1 -1
  18. package/dist/lib/biome-host/biome-scoped-query.js.map +1 -1
  19. package/dist/lib/biome-host/create-biome-orval-config.js +2 -2
  20. package/dist/lib/biome-host/create-biome-orval-config.js.map +1 -1
  21. package/dist/lib/biome-host/errors.js.map +1 -1
  22. package/dist/lib/biome-host/index.d.ts +1 -2
  23. package/dist/lib/biome-host/index.d.ts.map +1 -1
  24. package/dist/lib/biome-host/index.js +1 -2
  25. package/dist/lib/biome-host/index.js.map +1 -1
  26. package/dist/lib/biome-host/session-contributions.d.ts +9 -4
  27. package/dist/lib/biome-host/session-contributions.d.ts.map +1 -1
  28. package/dist/lib/capabilities/capability-context.d.ts +9 -0
  29. package/dist/lib/capabilities/capability-context.d.ts.map +1 -0
  30. package/dist/lib/capabilities/capability-context.js +13 -0
  31. package/dist/lib/capabilities/capability-context.js.map +1 -0
  32. package/dist/lib/capabilities/capability-provider.d.ts +2 -8
  33. package/dist/lib/capabilities/capability-provider.d.ts.map +1 -1
  34. package/dist/lib/capabilities/capability-provider.js +1 -12
  35. package/dist/lib/capabilities/capability-provider.js.map +1 -1
  36. package/dist/lib/capabilities/index.d.ts +1 -0
  37. package/dist/lib/capabilities/index.d.ts.map +1 -1
  38. package/dist/lib/capabilities/index.js +1 -0
  39. package/dist/lib/capabilities/index.js.map +1 -1
  40. package/dist/lib/capabilities/use-capability.d.ts.map +1 -1
  41. package/dist/lib/capabilities/use-capability.js +1 -1
  42. package/dist/lib/capabilities/use-capability.js.map +1 -1
  43. package/dist/lib/system-bus/deeplink.js.map +1 -1
  44. package/dist/lib/system-bus/host-ports.d.ts +2 -1
  45. package/dist/lib/system-bus/host-ports.d.ts.map +1 -1
  46. package/dist/lib/system-bus/intent-registry.d.ts.map +1 -1
  47. package/dist/lib/system-bus/intent-registry.js.map +1 -1
  48. package/dist/offline/index.d.ts +2 -1
  49. package/dist/offline/index.d.ts.map +1 -1
  50. package/dist/offline/index.js +2 -1
  51. package/dist/offline/index.js.map +1 -1
  52. package/dist/offline/offline-status-badge.js +1 -1
  53. package/dist/offline/offline-status-badge.js.map +1 -1
  54. package/dist/offline/outbox.js.map +1 -1
  55. package/dist/offline/react-hooks.d.ts +7 -0
  56. package/dist/offline/react-hooks.d.ts.map +1 -0
  57. package/dist/offline/react-hooks.js +25 -0
  58. package/dist/offline/react-hooks.js.map +1 -0
  59. package/dist/offline/react.d.ts +0 -4
  60. package/dist/offline/react.d.ts.map +1 -1
  61. package/dist/offline/react.js +2 -24
  62. package/dist/offline/react.js.map +1 -1
  63. package/dist/offline/storage.d.ts.map +1 -1
  64. package/dist/offline/storage.js +8 -5
  65. package/dist/offline/storage.js.map +1 -1
  66. package/dist/registry/index.d.ts +1 -0
  67. package/dist/registry/index.d.ts.map +1 -1
  68. package/dist/registry/index.js +1 -0
  69. package/dist/registry/index.js.map +1 -1
  70. package/dist/registry/lib/primitives/SessionEventCard.d.ts.map +1 -1
  71. package/dist/registry/lib/primitives/SessionEventCard.js +1 -1
  72. package/dist/registry/lib/primitives/SessionEventCard.js.map +1 -1
  73. package/dist/registry/lib/session-context-builder.d.ts +10 -2
  74. package/dist/registry/lib/session-context-builder.d.ts.map +1 -1
  75. package/dist/registry/lib/session-context-builder.js +14 -7
  76. package/dist/registry/lib/session-context-builder.js.map +1 -1
  77. package/dist/registry/lib/session-context-provider.d.ts +1 -3
  78. package/dist/registry/lib/session-context-provider.d.ts.map +1 -1
  79. package/dist/registry/lib/session-context-provider.js +2 -15
  80. package/dist/registry/lib/session-context-provider.js.map +1 -1
  81. package/dist/registry/lib/session-context.d.ts +5 -0
  82. package/dist/registry/lib/session-context.d.ts.map +1 -0
  83. package/dist/registry/lib/session-context.js +15 -0
  84. package/dist/registry/lib/session-context.js.map +1 -0
  85. package/dist/registry/lib/session-selectors.d.ts.map +1 -1
  86. package/dist/registry/lib/session-selectors.js +3 -1
  87. package/dist/registry/lib/session-selectors.js.map +1 -1
  88. package/dist/ui/branding/app-branding-context.d.ts +9 -0
  89. package/dist/ui/branding/app-branding-context.d.ts.map +1 -0
  90. package/dist/ui/branding/app-branding-context.js +10 -0
  91. package/dist/ui/branding/app-branding-context.js.map +1 -0
  92. package/dist/ui/branding/app-branding-provider.d.ts +0 -6
  93. package/dist/ui/branding/app-branding-provider.d.ts.map +1 -1
  94. package/dist/ui/branding/app-branding-provider.js +1 -8
  95. package/dist/ui/branding/app-branding-provider.js.map +1 -1
  96. package/dist/ui/capture/schema-field-input.js +1 -1
  97. package/dist/ui/capture/schema-field-input.js.map +1 -1
  98. package/dist/ui/capture/use-prefill.d.ts.map +1 -1
  99. package/dist/ui/capture/use-prefill.js +3 -1
  100. package/dist/ui/capture/use-prefill.js.map +1 -1
  101. package/dist/ui/capture/use-wizard.d.ts +1 -1
  102. package/dist/ui/capture/use-wizard.d.ts.map +1 -1
  103. package/dist/ui/capture/use-wizard.js +1 -1
  104. package/dist/ui/capture/use-wizard.js.map +1 -1
  105. package/dist/ui/capture/wizard.d.ts.map +1 -1
  106. package/dist/ui/capture/wizard.js.map +1 -1
  107. package/dist/ui/index.d.ts +5 -1
  108. package/dist/ui/index.d.ts.map +1 -1
  109. package/dist/ui/index.js +5 -1
  110. package/dist/ui/index.js.map +1 -1
  111. package/dist/ui/primitives/alert-dialog.js +1 -1
  112. package/dist/ui/primitives/alert-dialog.js.map +1 -1
  113. package/dist/ui/primitives/badge-variants.d.ts +4 -0
  114. package/dist/ui/primitives/badge-variants.d.ts.map +1 -0
  115. package/dist/ui/primitives/badge-variants.js +15 -0
  116. package/dist/ui/primitives/badge-variants.js.map +1 -0
  117. package/dist/ui/primitives/badge.d.ts +3 -5
  118. package/dist/ui/primitives/badge.d.ts.map +1 -1
  119. package/dist/ui/primitives/badge.js +2 -15
  120. package/dist/ui/primitives/badge.js.map +1 -1
  121. package/dist/ui/primitives/button-variants.d.ts +5 -0
  122. package/dist/ui/primitives/button-variants.d.ts.map +1 -0
  123. package/dist/ui/primitives/button-variants.js +24 -0
  124. package/dist/ui/primitives/button-variants.js.map +1 -0
  125. package/dist/ui/primitives/button.d.ts +3 -6
  126. package/dist/ui/primitives/button.d.ts.map +1 -1
  127. package/dist/ui/primitives/button.js +2 -24
  128. package/dist/ui/primitives/button.js.map +1 -1
  129. package/dist/ui/primitives/form-stepper.d.ts +1 -13
  130. package/dist/ui/primitives/form-stepper.d.ts.map +1 -1
  131. package/dist/ui/primitives/form-stepper.js +1 -6
  132. package/dist/ui/primitives/form-stepper.js.map +1 -1
  133. package/dist/ui/primitives/form-stepper.types.d.ts +14 -0
  134. package/dist/ui/primitives/form-stepper.types.d.ts.map +1 -0
  135. package/dist/ui/primitives/form-stepper.types.js +7 -0
  136. package/dist/ui/primitives/form-stepper.types.js.map +1 -0
  137. package/dist/ui/primitives/preview-device-frame.js +1 -1
  138. package/dist/ui/primitives/preview-device-frame.js.map +1 -1
  139. package/dist/ui/primitives/tag-multi-select.d.ts.map +1 -1
  140. package/dist/ui/primitives/tag-multi-select.js +1 -1
  141. package/dist/ui/primitives/tag-multi-select.js.map +1 -1
  142. package/dist/vista/index.d.ts +4 -0
  143. package/dist/vista/index.d.ts.map +1 -1
  144. package/dist/vista/index.js +4 -0
  145. package/dist/vista/index.js.map +1 -1
  146. package/dist/vista/surfaces/FilesSurface.d.ts +1 -12
  147. package/dist/vista/surfaces/FilesSurface.d.ts.map +1 -1
  148. package/dist/vista/surfaces/FilesSurface.js +2 -46
  149. package/dist/vista/surfaces/FilesSurface.js.map +1 -1
  150. package/dist/vista/surfaces/FormSurface.d.ts.map +1 -1
  151. package/dist/vista/surfaces/FormSurface.js +39 -12
  152. package/dist/vista/surfaces/FormSurface.js.map +1 -1
  153. package/dist/vista/surfaces/elicitation-transport.d.ts +12 -3
  154. package/dist/vista/surfaces/elicitation-transport.d.ts.map +1 -1
  155. package/dist/vista/surfaces/elicitation-transport.js +96 -3
  156. package/dist/vista/surfaces/elicitation-transport.js.map +1 -1
  157. package/dist/vista/surfaces/file-surface-utils.d.ts +13 -0
  158. package/dist/vista/surfaces/file-surface-utils.d.ts.map +1 -0
  159. package/dist/vista/surfaces/file-surface-utils.js +46 -0
  160. package/dist/vista/surfaces/file-surface-utils.js.map +1 -0
  161. package/dist/vista/surfaces/index.d.ts +4 -3
  162. package/dist/vista/surfaces/index.d.ts.map +1 -1
  163. package/dist/vista/surfaces/index.js +4 -81
  164. package/dist/vista/surfaces/index.js.map +1 -1
  165. package/dist/vista/surfaces/register-surfaces.d.ts +2 -0
  166. package/dist/vista/surfaces/register-surfaces.d.ts.map +1 -0
  167. package/dist/vista/surfaces/register-surfaces.js +80 -0
  168. package/dist/vista/surfaces/register-surfaces.js.map +1 -0
  169. package/dist/vista/surfaces/text-surfaces.js +1 -1
  170. package/dist/vista/surfaces/text-surfaces.js.map +1 -1
  171. package/dist/vista/vista-code-renderer-context.d.ts +9 -0
  172. package/dist/vista/vista-code-renderer-context.d.ts.map +1 -0
  173. package/dist/vista/vista-code-renderer-context.js +6 -0
  174. package/dist/vista/vista-code-renderer-context.js.map +1 -0
  175. package/dist/vista/vista-code-renderer.d.ts +1 -6
  176. package/dist/vista/vista-code-renderer.d.ts.map +1 -1
  177. package/dist/vista/vista-code-renderer.js +1 -5
  178. package/dist/vista/vista-code-renderer.js.map +1 -1
  179. package/dist/vista/vista-file-context.d.ts +8 -0
  180. package/dist/vista/vista-file-context.d.ts.map +1 -0
  181. package/dist/vista/vista-file-context.js +6 -0
  182. package/dist/vista/vista-file-context.js.map +1 -0
  183. package/dist/vista/vista-file-source.d.ts +1 -6
  184. package/dist/vista/vista-file-source.d.ts.map +1 -1
  185. package/dist/vista/vista-file-source.js +2 -5
  186. package/dist/vista/vista-file-source.js.map +1 -1
  187. package/dist/vista/vista-form-context.d.ts +13 -0
  188. package/dist/vista/vista-form-context.d.ts.map +1 -0
  189. package/dist/vista/vista-form-context.js +6 -0
  190. package/dist/vista/vista-form-context.js.map +1 -0
  191. package/dist/vista/vista-form-host.d.ts +1 -10
  192. package/dist/vista/vista-form-host.d.ts.map +1 -1
  193. package/dist/vista/vista-form-host.js +1 -5
  194. package/dist/vista/vista-form-host.js.map +1 -1
  195. package/dist/vista/vista-open-context.d.ts +1 -3
  196. package/dist/vista/vista-open-context.d.ts.map +1 -1
  197. package/dist/vista/vista-open-context.js +1 -5
  198. package/dist/vista/vista-open-context.js.map +1 -1
  199. package/dist/vista/vista-open-state.d.ts +5 -0
  200. package/dist/vista/vista-open-state.d.ts.map +1 -0
  201. package/dist/vista/vista-open-state.js +6 -0
  202. package/dist/vista/vista-open-state.js.map +1 -0
  203. package/dist/vista/vista-payloads.d.ts +2 -3
  204. package/dist/vista/vista-payloads.d.ts.map +1 -1
  205. package/dist/vista/vista-payloads.js +18 -21
  206. package/dist/vista/vista-payloads.js.map +1 -1
  207. package/dist/vista/vista-tools.d.ts.map +1 -1
  208. package/dist/vista/vista-tools.js.map +1 -1
  209. package/package.json +3 -3
  210. package/src/lib/biome-host/biome-builders.ts +4 -1
  211. package/src/lib/biome-host/biome-registry.ts +0 -1
  212. package/src/lib/biome-host/biome-scope-hooks.ts +39 -0
  213. package/src/lib/biome-host/biome-scope.tsx +1 -68
  214. package/src/lib/biome-host/biome-scoped-query.ts +4 -1
  215. package/src/lib/biome-host/create-biome-orval-config.ts +2 -2
  216. package/src/lib/biome-host/errors.ts +1 -1
  217. package/src/lib/biome-host/index.ts +1 -2
  218. package/src/lib/biome-host/session-contributions.ts +21 -18
  219. package/src/lib/capabilities/capability-context.ts +25 -0
  220. package/src/lib/capabilities/capability-provider.tsx +5 -34
  221. package/src/lib/capabilities/index.ts +1 -0
  222. package/src/lib/capabilities/use-capability.ts +4 -1
  223. package/src/lib/system-bus/deeplink.ts +1 -1
  224. package/src/lib/system-bus/host-ports.ts +2 -1
  225. package/src/lib/system-bus/intent-registry.ts +1 -2
  226. package/src/offline/index.ts +4 -2
  227. package/src/offline/offline-status-badge.tsx +1 -1
  228. package/src/offline/outbox.test.ts +23 -16
  229. package/src/offline/outbox.ts +1 -1
  230. package/src/offline/react-hooks.ts +37 -0
  231. package/src/offline/react.tsx +1 -40
  232. package/src/offline/storage.ts +11 -6
  233. package/src/registry/index.ts +1 -0
  234. package/src/registry/lib/primitives/SessionEventCard.tsx +4 -2
  235. package/src/registry/lib/session-context-builder.ts +33 -17
  236. package/src/registry/lib/session-context-provider.tsx +8 -25
  237. package/src/registry/lib/session-context.ts +21 -0
  238. package/src/registry/lib/session-selectors.ts +3 -1
  239. package/src/ui/branding/app-branding-context.ts +20 -0
  240. package/src/ui/branding/app-branding-provider.tsx +5 -27
  241. package/src/ui/capture/schema-field-input.tsx +7 -1
  242. package/src/ui/capture/use-prefill.ts +4 -1
  243. package/src/ui/capture/use-wizard.ts +2 -2
  244. package/src/ui/capture/wizard.tsx +2 -1
  245. package/src/ui/index.ts +7 -2
  246. package/src/ui/primitives/alert-dialog.tsx +1 -1
  247. package/src/ui/primitives/badge-variants.ts +21 -0
  248. package/src/ui/primitives/badge.tsx +3 -19
  249. package/src/ui/primitives/button-variants.ts +31 -0
  250. package/src/ui/primitives/button.tsx +3 -28
  251. package/src/ui/primitives/form-stepper.tsx +1 -16
  252. package/src/ui/primitives/form-stepper.types.ts +15 -0
  253. package/src/ui/primitives/preview-device-frame.tsx +1 -1
  254. package/src/ui/primitives/tag-multi-select.tsx +3 -4
  255. package/src/vista/form-payload.spec.ts +71 -25
  256. package/src/vista/index.ts +4 -0
  257. package/src/vista/surfaces/FilesSurface.tsx +2 -68
  258. package/src/vista/surfaces/FormSurface.tsx +125 -26
  259. package/src/vista/surfaces/elicitation-transport.spec.ts +205 -23
  260. package/src/vista/surfaces/elicitation-transport.ts +192 -14
  261. package/src/vista/surfaces/file-surface-utils.ts +62 -0
  262. package/src/vista/surfaces/index.ts +24 -0
  263. package/src/vista/surfaces/{index.tsx → register-surfaces.tsx} +1 -21
  264. package/src/vista/surfaces/text-surfaces.tsx +1 -1
  265. package/src/vista/vista-code-renderer-context.ts +16 -0
  266. package/src/vista/vista-code-renderer.tsx +4 -16
  267. package/src/vista/vista-file-context.ts +14 -0
  268. package/src/vista/vista-file-source.tsx +5 -28
  269. package/src/vista/vista-form-context.ts +20 -0
  270. package/src/vista/vista-form-host.tsx +11 -36
  271. package/src/vista/vista-open-context.tsx +4 -14
  272. package/src/vista/vista-open-state.ts +11 -0
  273. package/src/vista/vista-payloads.ts +44 -46
  274. package/src/vista/vista-tools.ts +2 -2
  275. package/dist/lib/biome-host/response-envelope.d.ts +0 -3
  276. package/dist/lib/biome-host/response-envelope.d.ts.map +0 -1
  277. package/dist/lib/biome-host/response-envelope.js +0 -21
  278. package/dist/lib/biome-host/response-envelope.js.map +0 -1
  279. package/dist/lib/biome-host/session-profiles.d.ts +0 -18
  280. package/dist/lib/biome-host/session-profiles.d.ts.map +0 -1
  281. package/dist/lib/biome-host/session-profiles.js +0 -35
  282. package/dist/lib/biome-host/session-profiles.js.map +0 -1
  283. package/src/lib/biome-host/response-envelope.ts +0 -69
  284. package/src/lib/biome-host/session-profiles.ts +0 -99
@@ -3,8 +3,8 @@
3
3
  //
4
4
  // This module is the ONLY code that ever reads a captured field value out of
5
5
  // the form surface, and it does exactly one thing with it: put it in the body
6
- // of a `fetch` to the ABSOLUTE service URL carried by the (already validated)
7
- // surface payload, bearing the SUBJECT's own JWT.
6
+ // of a `fetch` to an endpoint derived from the HOST-CONFIGURED trusted
7
+ // elicitation-service base URL, bearing the SUBJECT's own JWT.
8
8
  //
9
9
  // Three properties make "the agent cannot receive the value" structural:
10
10
  //
@@ -15,15 +15,17 @@
15
15
  // • the response is PROJECTED, not passed through: `toOutcome` constructs a
16
16
  // fresh object with exactly three keys, so a service that echoed a secret
17
17
  // back could still not leak one through this boundary;
18
- // • the URL is absolute and comes from the payload validator, which rejects
19
- // anything that is not `http(s)://…` — there is no host-relative path, so
20
- // the request cannot be aimed at the app's own BFF or the chat transport.
18
+ // • the payload carries only an opaque ref. It has NO network authority. This
19
+ // module validates the host-configured base URL and derives the exact
20
+ // status/submit paths before a bearer token or field value reaches fetch.
21
21
  //
22
22
  // Errors carry the SERVICE's message verbatim (that is how the backend's hard
23
23
  // 400 on an empty credential form becomes readable UI copy) and never the
24
24
  // request body.
25
25
  // ═══════════════════════════════════════════════════════════════════════════
26
- import type { VistaFormOutcome } from '../vista-form-host';
26
+ import { isValidVistaFormRef } from '../vista-payloads';
27
+
28
+ import type { VistaFormOutcome } from '../vista-form-context';
27
29
 
28
30
  /** A non-2xx answer from the owning service, carrying its message verbatim. */
29
31
  export class VistaFormSubmitError extends Error {
@@ -36,6 +38,19 @@ export class VistaFormSubmitError extends Error {
36
38
  }
37
39
  }
38
40
 
41
+ /** Invalid trusted-host transport configuration or unsafe path input. */
42
+ export class VistaFormTransportConfigError extends Error {
43
+ constructor(message: string) {
44
+ super(message);
45
+ this.name = 'VistaFormTransportConfigError';
46
+ }
47
+ }
48
+
49
+ export interface TrustedElicitationEndpoints {
50
+ readonly statusUrl: string;
51
+ readonly submitUrl: string;
52
+ }
53
+
39
54
  type UnknownRecord = Readonly<Record<string, unknown>>;
40
55
 
41
56
  function asRecord(value: unknown): UnknownRecord | null {
@@ -69,7 +84,9 @@ export function extractServiceMessage(body: unknown, status: number): string {
69
84
  : (record.message ?? asRecord(record.error)?.message ?? record.error);
70
85
  if (typeof candidate === 'string' && candidate.length > 0) return candidate;
71
86
  if (Array.isArray(candidate)) {
72
- const joined = candidate.filter((m): m is string => typeof m === 'string').join(' ');
87
+ const joined = candidate
88
+ .filter((m): m is string => typeof m === 'string')
89
+ .join(' ');
73
90
  if (joined.length > 0) return joined;
74
91
  }
75
92
  if (typeof body === 'string' && body.length > 0) return body;
@@ -108,7 +125,10 @@ function toOutcome(body: unknown, ref: string): VistaFormOutcome | null {
108
125
  }
109
126
 
110
127
  /** The `Authorization`/`Content-Type` pair every request here carries. */
111
- function authHeaders(accessToken: string, json: boolean): Record<string, string> {
128
+ function authHeaders(
129
+ accessToken: string,
130
+ json: boolean,
131
+ ): Record<string, string> {
112
132
  const headers: Record<string, string> = {
113
133
  Authorization: `Bearer ${accessToken}`,
114
134
  Accept: 'application/json',
@@ -117,6 +137,146 @@ function authHeaders(accessToken: string, json: boolean): Record<string, string>
117
137
  return headers;
118
138
  }
119
139
 
140
+ function browserOrigin(): string | undefined {
141
+ return typeof globalThis.location === 'undefined'
142
+ ? undefined
143
+ : globalThis.location.origin;
144
+ }
145
+
146
+ function isLoopbackHostname(hostname: string): boolean {
147
+ return (
148
+ hostname === 'localhost' ||
149
+ hostname.endsWith('.localhost') ||
150
+ hostname === '127.0.0.1' ||
151
+ hostname === '[::1]'
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Parse the host's deploy-time trust anchor. It must be a canonical absolute
157
+ * HTTP(S) URL without credentials/query/fragment. HTTPS is mandatory except
158
+ * for loopback or same-host HTTP when the page itself is also served over HTTP.
159
+ */
160
+ function trustedApiBaseUrl(
161
+ raw: string,
162
+ activeBrowserOrigin: string | undefined,
163
+ ): URL {
164
+ if (raw.length === 0 || raw.trim() !== raw) {
165
+ throw new VistaFormTransportConfigError(
166
+ 'The trusted elicitation API base URL is empty or contains whitespace.',
167
+ );
168
+ }
169
+
170
+ let url: URL;
171
+ try {
172
+ url = new URL(raw);
173
+ } catch {
174
+ throw new VistaFormTransportConfigError(
175
+ 'The trusted elicitation API base URL must be absolute.',
176
+ );
177
+ }
178
+
179
+ if (
180
+ (url.protocol !== 'https:' && url.protocol !== 'http:') ||
181
+ url.username.length > 0 ||
182
+ url.password.length > 0 ||
183
+ url.search.length > 0 ||
184
+ url.hash.length > 0
185
+ ) {
186
+ throw new VistaFormTransportConfigError(
187
+ 'The trusted elicitation API base URL must use HTTP(S) and contain no credentials, query, or fragment.',
188
+ );
189
+ }
190
+
191
+ const canonicalPath =
192
+ url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
193
+ const canonical = `${url.origin}${canonicalPath}`;
194
+ const suppliedWithoutOneTrailingSlash = raw.endsWith('/')
195
+ ? raw.slice(0, -1)
196
+ : raw;
197
+ if (suppliedWithoutOneTrailingSlash !== canonical) {
198
+ throw new VistaFormTransportConfigError(
199
+ 'The trusted elicitation API base URL must be canonical and contain no traversal or ambiguous path.',
200
+ );
201
+ }
202
+
203
+ if (url.protocol === 'http:' && !isLoopbackHostname(url.hostname)) {
204
+ let page: URL | null = null;
205
+ if (activeBrowserOrigin !== undefined) {
206
+ try {
207
+ page = new URL(activeBrowserOrigin);
208
+ } catch {
209
+ page = null;
210
+ }
211
+ }
212
+ if (
213
+ page === null ||
214
+ page.protocol !== 'http:' ||
215
+ page.hostname !== url.hostname
216
+ ) {
217
+ throw new VistaFormTransportConfigError(
218
+ 'Insecure elicitation HTTP is allowed only on loopback or the same host as an HTTP development page.',
219
+ );
220
+ }
221
+ }
222
+
223
+ return new URL(canonical);
224
+ }
225
+
226
+ /**
227
+ * Derive the only two authenticated endpoints from trusted host configuration
228
+ * plus one opaque path segment. Calling this directly remains safe even if a
229
+ * caller bypasses the surface-payload validator.
230
+ */
231
+ export function resolveTrustedElicitationEndpoints(
232
+ apiBaseUrl: string,
233
+ ref: string,
234
+ activeBrowserOrigin: string | undefined = browserOrigin(),
235
+ ): TrustedElicitationEndpoints {
236
+ if (!isValidVistaFormRef(ref)) {
237
+ throw new VistaFormTransportConfigError(
238
+ 'The elicitation reference is not a valid opaque base64url path segment.',
239
+ );
240
+ }
241
+ const base = trustedApiBaseUrl(apiBaseUrl, activeBrowserOrigin)
242
+ .toString()
243
+ .replace(/\/$/, '');
244
+ const statusUrl = `${base}/${ref}`;
245
+ return { statusUrl, submitUrl: `${statusUrl}/submit` };
246
+ }
247
+
248
+ /**
249
+ * Resolve an unauthenticated launch path under the SAME trusted service origin.
250
+ * Absolute/network-path URLs and dot-segment traversal fail closed.
251
+ */
252
+ export function resolveTrustedElicitationLaunchUrl(
253
+ apiBaseUrl: string,
254
+ path: string,
255
+ activeBrowserOrigin: string | undefined = browserOrigin(),
256
+ ): string {
257
+ const base = trustedApiBaseUrl(apiBaseUrl, activeBrowserOrigin);
258
+ if (
259
+ !path.startsWith('/') ||
260
+ path.startsWith('//') ||
261
+ path.includes('\\') ||
262
+ path.includes('#')
263
+ ) {
264
+ throw new VistaFormTransportConfigError(
265
+ 'The elicitation launch target must be a root-relative service path.',
266
+ );
267
+ }
268
+ const resolved = new URL(path, `${base.origin}/`);
269
+ if (
270
+ resolved.origin !== base.origin ||
271
+ `${resolved.pathname}${resolved.search}` !== path
272
+ ) {
273
+ throw new VistaFormTransportConfigError(
274
+ 'The elicitation launch path contains traversal or an ambiguous encoding.',
275
+ );
276
+ }
277
+ return resolved.toString();
278
+ }
279
+
120
280
  /**
121
281
  * POST the captured fields to the owning service.
122
282
  *
@@ -125,20 +285,30 @@ function authHeaders(accessToken: string, json: boolean): Record<string, string>
125
285
  * user reads.
126
286
  */
127
287
  export async function submitElicitationFields(args: {
128
- readonly submitUrl: string;
288
+ readonly apiBaseUrl: string;
129
289
  readonly ref: string;
130
290
  readonly accessToken: string;
131
291
  readonly fields: Readonly<Record<string, string>>;
132
292
  }): Promise<VistaFormOutcome> {
133
- const response = await fetch(args.submitUrl, {
293
+ const { submitUrl } = resolveTrustedElicitationEndpoints(
294
+ args.apiBaseUrl,
295
+ args.ref,
296
+ );
297
+ const response = await fetch(submitUrl, {
134
298
  method: 'POST',
135
299
  headers: authHeaders(args.accessToken, true),
136
300
  body: JSON.stringify({ fields: args.fields }),
137
301
  credentials: 'omit',
302
+ // A 307/308 could otherwise replay the credential body to a different
303
+ // origin. Authenticated elicitation endpoints must never redirect.
304
+ redirect: 'error',
138
305
  });
139
306
  const body = await readBody(response);
140
307
  if (!response.ok) {
141
- throw new VistaFormSubmitError(response.status, extractServiceMessage(body, response.status));
308
+ throw new VistaFormSubmitError(
309
+ response.status,
310
+ extractServiceMessage(body, response.status),
311
+ );
142
312
  }
143
313
  const outcome = toOutcome(body, args.ref);
144
314
  if (outcome === null) {
@@ -158,18 +328,26 @@ export async function submitElicitationFields(args: {
158
328
  * Returns `null` while still pending — an explicit "not yet", never a guess.
159
329
  */
160
330
  export async function readElicitationStatus(args: {
161
- readonly statusUrl: string;
331
+ readonly apiBaseUrl: string;
162
332
  readonly ref: string;
163
333
  readonly accessToken: string;
164
334
  }): Promise<VistaFormOutcome | null> {
165
- const response = await fetch(args.statusUrl, {
335
+ const { statusUrl } = resolveTrustedElicitationEndpoints(
336
+ args.apiBaseUrl,
337
+ args.ref,
338
+ );
339
+ const response = await fetch(statusUrl, {
166
340
  method: 'GET',
167
341
  headers: authHeaders(args.accessToken, false),
168
342
  credentials: 'omit',
343
+ redirect: 'error',
169
344
  });
170
345
  const body = await readBody(response);
171
346
  if (!response.ok) {
172
- throw new VistaFormSubmitError(response.status, extractServiceMessage(body, response.status));
347
+ throw new VistaFormSubmitError(
348
+ response.status,
349
+ extractServiceMessage(body, response.status),
350
+ );
173
351
  }
174
352
  return toOutcome(body, args.ref);
175
353
  }
@@ -0,0 +1,62 @@
1
+ import type { VistaFileEntry } from '../vista-payloads';
2
+
3
+ interface FileExtMeta {
4
+ readonly color: string;
5
+ readonly tag: string;
6
+ }
7
+
8
+ const EXT_META: Readonly<Record<string, FileExtMeta>> = {
9
+ ts: { color: 'hsl(var(--info))', tag: 'TS' },
10
+ tsx: { color: 'hsl(var(--info))', tag: 'TSX' },
11
+ js: { color: 'hsl(var(--warning))', tag: 'JS' },
12
+ jsx: { color: 'hsl(var(--warning))', tag: 'JSX' },
13
+ json: { color: 'hsl(var(--ink-3))', tag: 'JSON' },
14
+ md: { color: 'hsl(var(--p-people))', tag: 'MD' },
15
+ css: { color: 'hsl(var(--p-sales))', tag: 'CSS' },
16
+ html: { color: 'hsl(var(--p-sales))', tag: 'HTML' },
17
+ yaml: { color: 'hsl(var(--ink-3))', tag: 'YAML' },
18
+ yml: { color: 'hsl(var(--ink-3))', tag: 'YAML' },
19
+ };
20
+
21
+ export function fileExtMeta(name: string): FileExtMeta {
22
+ if (/\.test\.[tj]sx?$/.test(name) || /\.spec\.[tj]sx?$/.test(name)) {
23
+ return { color: 'hsl(var(--success))', tag: 'TEST' };
24
+ }
25
+ const ext = name.split('.').pop() ?? '';
26
+ return (
27
+ EXT_META[ext] ?? {
28
+ color: 'hsl(var(--ink-4))',
29
+ tag: (ext || 'FILE').toUpperCase().slice(0, 4),
30
+ }
31
+ );
32
+ }
33
+
34
+ export interface VistaFileGroup {
35
+ readonly dir: string;
36
+ readonly files: readonly VistaFileEntry[];
37
+ }
38
+
39
+ function compareDirs(a: string, b: string): number {
40
+ if (a === '') return -1;
41
+ if (b === '') return 1;
42
+ return a.localeCompare(b);
43
+ }
44
+
45
+ export function groupFilesByFolder(
46
+ files: readonly VistaFileEntry[],
47
+ ): VistaFileGroup[] {
48
+ const groups = new Map<string, VistaFileEntry[]>();
49
+ for (const file of files) {
50
+ const slash = file.path.lastIndexOf('/');
51
+ const dir = slash < 0 ? '' : file.path.slice(0, slash);
52
+ const bucket = groups.get(dir);
53
+ if (bucket) bucket.push(file);
54
+ else groups.set(dir, [file]);
55
+ }
56
+ return [...groups.keys()].sort(compareDirs).map((dir) => ({
57
+ dir,
58
+ files: [...(groups.get(dir) ?? [])].sort((a, b) =>
59
+ a.path.localeCompare(b.path),
60
+ ),
61
+ }));
62
+ }
@@ -0,0 +1,24 @@
1
+ import './register-surfaces';
2
+
3
+ export { ChartSurface } from './ChartSurface';
4
+ export { FilesSurface } from './FilesSurface';
5
+ export {
6
+ fileExtMeta,
7
+ groupFilesByFolder,
8
+ type VistaFileGroup,
9
+ } from './file-surface-utils';
10
+ export { FormSurface } from './FormSurface';
11
+ export {
12
+ extractServiceMessage,
13
+ readElicitationStatus,
14
+ resolveTrustedElicitationEndpoints,
15
+ resolveTrustedElicitationLaunchUrl,
16
+ submitElicitationFields,
17
+ VistaFormSubmitError,
18
+ VistaFormTransportConfigError,
19
+ } from './elicitation-transport';
20
+ export { HtmlSurface } from './HtmlSurface';
21
+ export { TableSurface } from './TableSurface';
22
+ export { UrlSurface } from './UrlSurface';
23
+ export { CodeSurface, DiffSurface, JsonSurface } from './text-surfaces';
24
+ export { ImageSurface, MediaSurface, PdfSurface } from './blob-surfaces';
@@ -29,11 +29,7 @@
29
29
  */
30
30
  import { SurfaceKind } from '@xemahq/kernel-contracts/surface';
31
31
 
32
- import {
33
- ImageSurface,
34
- MediaSurface,
35
- PdfSurface,
36
- } from './blob-surfaces';
32
+ import { ImageSurface, MediaSurface, PdfSurface } from './blob-surfaces';
37
33
  import { ChartSurface } from './ChartSurface';
38
34
  import { FilesSurface } from './FilesSurface';
39
35
  import { FormSurface } from './FormSurface';
@@ -139,19 +135,3 @@ vistaSurfaceRegistry.registerFirstParty(SurfaceKind.Form, {
139
135
  validate: validateFormPayload,
140
136
  render: (payload) => <FormSurface {...payload} />,
141
137
  });
142
-
143
- export { ChartSurface } from './ChartSurface';
144
- export { FilesSurface, fileExtMeta, groupFilesByFolder } from './FilesSurface';
145
- export { FormSurface } from './FormSurface';
146
- export {
147
- submitElicitationFields,
148
- readElicitationStatus,
149
- extractServiceMessage,
150
- VistaFormSubmitError,
151
- } from './elicitation-transport';
152
- export { HtmlSurface } from './HtmlSurface';
153
- export { TableSurface } from './TableSurface';
154
- export { UrlSurface } from './UrlSurface';
155
- export { CodeSurface, DiffSurface, JsonSurface } from './text-surfaces';
156
- export { ImageSurface, MediaSurface, PdfSurface } from './blob-surfaces';
157
- export type { VistaFileGroup } from './FilesSurface';
@@ -9,7 +9,7 @@
9
9
  * `plain` tab kind was collapsed into it; one kind, one renderer, no alias).
10
10
  */
11
11
 
12
- import { useVistaCodeRenderer } from '../vista-code-renderer';
12
+ import { useVistaCodeRenderer } from '../vista-code-renderer-context';
13
13
  import { VistaFileText } from '../vista-file-source';
14
14
 
15
15
  import type { VistaTextPayload } from '../vista-payloads';
@@ -0,0 +1,16 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ import type { ReactNode } from 'react';
4
+
5
+ export type VistaCodeRenderer = (args: {
6
+ readonly content: string;
7
+ readonly path?: string;
8
+ readonly language?: string;
9
+ }) => ReactNode;
10
+
11
+ export const VistaCodeRendererContext =
12
+ createContext<VistaCodeRenderer | null>(null);
13
+
14
+ export function useVistaCodeRenderer(): VistaCodeRenderer | null {
15
+ return useContext(VistaCodeRendererContext);
16
+ }
@@ -14,20 +14,13 @@
14
14
  * the intended rendering. Neither is an error path.
15
15
  */
16
16
  import {
17
- createContext,
18
- useContext,
19
17
  type ReactElement,
20
18
  type ReactNode,
21
19
  } from 'react';
22
-
23
- /** Draw a code body. `path` is present for a file-backed surface. */
24
- export type VistaCodeRenderer = (args: {
25
- readonly content: string;
26
- readonly path?: string;
27
- readonly language?: string;
28
- }) => ReactNode;
29
-
30
- const VistaCodeRendererContext = createContext<VistaCodeRenderer | null>(null);
20
+ import {
21
+ VistaCodeRendererContext,
22
+ type VistaCodeRenderer,
23
+ } from './vista-code-renderer-context';
31
24
 
32
25
  export function VistaCodeRendererProvider({
33
26
  renderer,
@@ -39,8 +32,3 @@ export function VistaCodeRendererProvider({
39
32
  </VistaCodeRendererContext.Provider>
40
33
  );
41
34
  }
42
-
43
- /** The host's code renderer, or `null` for the built-in mono pane. */
44
- export function useVistaCodeRenderer(): VistaCodeRenderer | null {
45
- return useContext(VistaCodeRendererContext);
46
- }
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export interface VistaFileSource {
4
+ readText(path: string): Promise<string>;
5
+ resolveUrl(path: string): Promise<string>;
6
+ readonly revision?: string | number;
7
+ }
8
+
9
+ export const VistaFileSourceContext =
10
+ createContext<VistaFileSource | null>(null);
11
+
12
+ export function useVistaFileSource(): VistaFileSource | null {
13
+ return useContext(VistaFileSourceContext);
14
+ }
@@ -13,34 +13,16 @@
13
13
  * "content unavailable".
14
14
  */
15
15
  import {
16
- createContext,
17
- useContext,
18
16
  useEffect,
19
17
  useState,
20
18
  type ReactElement,
21
19
  type ReactNode,
22
20
  } from 'react';
23
-
24
- /** Host-supplied byte access for path-backed surfaces. */
25
- export interface VistaFileSource {
26
- /** Read a workspace-relative path as text. Rejects on failure. */
27
- readText(path: string): Promise<string>;
28
- /**
29
- * Resolve a workspace-relative path to a fetchable URL (`blob:`, `data:`, or
30
- * a durable `https:` URL) for binary surfaces. Rejects on failure.
31
- *
32
- * The host owns the lifetime of anything it creates (e.g. revoking an object
33
- * URL); the surface only reads.
34
- */
35
- resolveUrl(path: string): Promise<string>;
36
- /**
37
- * Opaque revision token. Change it to make every mounted path-backed surface
38
- * re-read (the host bumps it on a workspace file-change event).
39
- */
40
- readonly revision?: string | number;
41
- }
42
-
43
- const VistaFileSourceContext = createContext<VistaFileSource | null>(null);
21
+ import {
22
+ VistaFileSourceContext,
23
+ useVistaFileSource,
24
+ type VistaFileSource,
25
+ } from './vista-file-context';
44
26
 
45
27
  export function VistaFileSourceProvider({
46
28
  source,
@@ -53,11 +35,6 @@ export function VistaFileSourceProvider({
53
35
  );
54
36
  }
55
37
 
56
- /** The active file source, or `null` when no provider is mounted. */
57
- export function useVistaFileSource(): VistaFileSource | null {
58
- return useContext(VistaFileSourceContext);
59
- }
60
-
61
38
  /** Async load state for a path-backed surface. */
62
39
  type LoadState =
63
40
  | { readonly status: 'loading' }
@@ -0,0 +1,20 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export interface VistaFormOutcome {
4
+ readonly submitted: true;
5
+ readonly ref: string;
6
+ readonly connectionId: string | null;
7
+ }
8
+
9
+ export interface VistaFormHost {
10
+ readonly elicitationApiBaseUrl: string;
11
+ getAccessToken(): Promise<string>;
12
+ onOutcome(outcome: VistaFormOutcome): void;
13
+ }
14
+
15
+ export const VistaFormHostContext =
16
+ createContext<VistaFormHost | null>(null);
17
+
18
+ export function useVistaFormHost(): VistaFormHost | null {
19
+ return useContext(VistaFormHostContext);
20
+ }
@@ -18,16 +18,22 @@
18
18
  * 2. `getAccessToken` returns the SUBJECT's token. The host binds it to the
19
19
  * logged-in user's session; it is never a service token and never the
20
20
  * agent's token, so the service authorizes the human, not the model.
21
- * 3. The surface's transport posts to an ABSOLUTE service URL taken from the
22
- * validated payload (see `validateFormPayload`, which rejects a relative
23
- * URL). There is no host-relative path, so it cannot be pointed at the
24
- * app's own BFF/chat transport.
21
+ * 3. `elicitationApiBaseUrl` is supplied by trusted host configuration, never
22
+ * by the agent-controlled payload. The kernel validates its scheme and
23
+ * canonical authority/path before deriving the status/submit endpoints.
25
24
  *
26
25
  * There is deliberately NO default host: a `form` surface mounted without a
27
26
  * provider renders a visible refusal and cannot submit. Fail-closed — a form
28
27
  * that silently posts nowhere would be worse than one that says it cannot.
29
28
  */
30
- import { createContext, useContext, type ReactElement, type ReactNode } from 'react';
29
+ import {
30
+ type ReactElement,
31
+ type ReactNode,
32
+ } from 'react';
33
+ import {
34
+ VistaFormHostContext,
35
+ type VistaFormHost,
36
+ } from './vista-form-context';
31
37
 
32
38
  /**
33
39
  * Everything a settled elicitation reports. Closed, scalar-only, and
@@ -35,32 +41,6 @@ import { createContext, useContext, type ReactElement, type ReactNode } from 're
35
41
  * persistence layer behind it has no column able to hold a field value, and
36
42
  * neither does this type.
37
43
  */
38
- export interface VistaFormOutcome {
39
- /** Always `true`: an unsettled elicitation produces no outcome at all. */
40
- readonly submitted: true;
41
- /** The elicitation reference the agent polls. Opaque, non-secret. */
42
- readonly ref: string;
43
- /** The connection the submission provisioned, when the service reported one. */
44
- readonly connectionId: string | null;
45
- }
46
-
47
- /** Host-supplied capabilities for the `form` surface. */
48
- export interface VistaFormHost {
49
- /**
50
- * The SUBJECT's own access token. Called once per request so a refreshed
51
- * token is picked up without remounting the surface.
52
- */
53
- getAccessToken(): Promise<string>;
54
- /**
55
- * Called exactly once when the elicitation settles. The host relays this to
56
- * whatever needs to know (cache invalidation, the agent session). By type it
57
- * can carry nothing but the outcome.
58
- */
59
- onOutcome(outcome: VistaFormOutcome): void;
60
- }
61
-
62
- const VistaFormHostContext = createContext<VistaFormHost | null>(null);
63
-
64
44
  export function VistaFormHostProvider({
65
45
  host,
66
46
  children,
@@ -71,8 +51,3 @@ export function VistaFormHostProvider({
71
51
  </VistaFormHostContext.Provider>
72
52
  );
73
53
  }
74
-
75
- /** The active form host, or `null` when no provider is mounted. */
76
- export function useVistaFormHost(): VistaFormHost | null {
77
- return useContext(VistaFormHostContext);
78
- }
@@ -12,18 +12,13 @@
12
12
  * silently does nothing.
13
13
  */
14
14
  import {
15
- createContext,
16
- useContext,
17
15
  type ReactElement,
18
16
  type ReactNode,
19
17
  } from 'react';
20
-
21
- import type { VistaTab } from './vista-model';
22
-
23
- /** Open (or replace) a tab and focus it. Mirrors `VistaTabsController.open`. */
24
- export type VistaOpenFn = (surface: VistaTab) => string;
25
-
26
- const VistaOpenContext = createContext<VistaOpenFn | null>(null);
18
+ import {
19
+ VistaOpenContext,
20
+ type VistaOpenFn,
21
+ } from './vista-open-state';
27
22
 
28
23
  export function VistaOpenProvider({
29
24
  open,
@@ -35,8 +30,3 @@ export function VistaOpenProvider({
35
30
  </VistaOpenContext.Provider>
36
31
  );
37
32
  }
38
-
39
- /** The panel's `open`, or `null` when no panel is mounted above. */
40
- export function useVistaOpen(): VistaOpenFn | null {
41
- return useContext(VistaOpenContext);
42
- }
@@ -0,0 +1,11 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ import type { VistaTab } from './vista-model';
4
+
5
+ export type VistaOpenFn = (surface: VistaTab) => string;
6
+
7
+ export const VistaOpenContext = createContext<VistaOpenFn | null>(null);
8
+
9
+ export function useVistaOpen(): VistaOpenFn | null {
10
+ return useContext(VistaOpenContext);
11
+ }