@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
@@ -13,25 +13,45 @@
13
13
  * • the values live in this component's `useState`; the single reader is
14
14
  * `submitElicitationFields`, which takes them as a parameter and returns an
15
15
  * outcome — the values are not reachable from any return or any throw;
16
- * • the POST target is an ABSOLUTE service URL (`validateFormPayload` rejects
17
- * a relative one), so the request cannot be aimed at the host's own BFF or
18
- * the chat transport;
16
+ * • the payload has NO URL authority. The exact endpoints are derived from
17
+ * the trusted elicitation API base URL supplied by `VistaFormHost`;
19
18
  * • secret fields are DECLARED (`spec.isSecret`) and render write-only; the
20
19
  * surface holds no cache and no read path repopulates them.
21
20
  *
22
21
  * Nothing here is provider-aware. Whether the flow is a field form or an OAuth
23
22
  * handoff is decided by the payload the owning service produced.
24
23
  */
25
- import { AlertTriangle, CheckCircle2, ExternalLink, Loader2, ShieldCheck } from 'lucide-react';
26
- import { useCallback, useEffect, useState, type ReactElement, type ReactNode } from 'react';
24
+ import {
25
+ AlertTriangle,
26
+ CheckCircle2,
27
+ ExternalLink,
28
+ Loader2,
29
+ ShieldCheck,
30
+ } from 'lucide-react';
31
+ import {
32
+ useCallback,
33
+ useEffect,
34
+ useState,
35
+ type ReactElement,
36
+ type ReactNode,
37
+ } from 'react';
27
38
 
28
- import { readElicitationStatus, submitElicitationFields, VistaFormSubmitError } from './elicitation-transport';
39
+ import {
40
+ readElicitationStatus,
41
+ resolveTrustedElicitationEndpoints,
42
+ resolveTrustedElicitationLaunchUrl,
43
+ submitElicitationFields,
44
+ VistaFormSubmitError,
45
+ } from './elicitation-transport';
29
46
  import { SchemaForm } from '../../ui/capture/schema-form';
30
47
  import { captureValuesToStringMap } from '../../ui/capture/submit-values';
31
48
  import { useSchemaValidation } from '../../ui/capture/use-schema-validation';
32
49
  import { cn } from '../../ui/cn';
33
50
  import { Button } from '../../ui/primitives/button';
34
- import { useVistaFormHost, type VistaFormOutcome } from '../vista-form-host';
51
+ import {
52
+ useVistaFormHost,
53
+ type VistaFormOutcome,
54
+ } from '../vista-form-context';
35
55
 
36
56
  import type { CaptureValues } from '../../ui/capture/field-spec';
37
57
  import type { VistaFormPayload } from '../vista-payloads';
@@ -51,6 +71,29 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
51
71
  const [showFieldErrors, setShowFieldErrors] = useState(false);
52
72
  const [outcome, setOutcome] = useState<VistaFormOutcome | null>(null);
53
73
  const { errors, isValid } = useSchemaValidation(payload.fields, values);
74
+ let transportConfigurationError: string | null = null;
75
+ let launchUrl: string | undefined;
76
+ if (host !== null) {
77
+ try {
78
+ // Validate the trust anchor before retrieving a bearer token or accepting
79
+ // a submit action. The transport repeats this check at the fetch boundary.
80
+ resolveTrustedElicitationEndpoints(
81
+ host.elicitationApiBaseUrl,
82
+ payload.ref,
83
+ );
84
+ if (payload.launch !== undefined) {
85
+ launchUrl = resolveTrustedElicitationLaunchUrl(
86
+ host.elicitationApiBaseUrl,
87
+ payload.launch.path,
88
+ );
89
+ }
90
+ } catch (error) {
91
+ transportConfigurationError =
92
+ error instanceof Error
93
+ ? error.message
94
+ : 'The trusted elicitation service is not configured safely.';
95
+ }
96
+ }
54
97
 
55
98
  const settle = useCallback(
56
99
  (next: VistaFormOutcome): void => {
@@ -85,14 +128,14 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
85
128
  .getAccessToken()
86
129
  .then((accessToken) =>
87
130
  submitElicitationFields({
88
- submitUrl: payload.submitUrl,
131
+ apiBaseUrl: host.elicitationApiBaseUrl,
89
132
  ref: payload.ref,
90
133
  accessToken,
91
134
  fields: captureValuesToStringMap(payload.fields, values),
92
135
  }),
93
136
  )
94
137
  .then(settle, failed);
95
- }, [host, isValid, payload.fields, payload.ref, payload.submitUrl, settle, failed, values]);
138
+ }, [host, isValid, payload.fields, payload.ref, settle, failed, values]);
96
139
 
97
140
  const handleCheckStatus = useCallback((): void => {
98
141
  if (host === null) return;
@@ -101,17 +144,23 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
101
144
  void host
102
145
  .getAccessToken()
103
146
  .then((accessToken) =>
104
- readElicitationStatus({ statusUrl: payload.statusUrl, ref: payload.ref, accessToken }),
147
+ readElicitationStatus({
148
+ apiBaseUrl: host.elicitationApiBaseUrl,
149
+ ref: payload.ref,
150
+ accessToken,
151
+ }),
105
152
  )
106
153
  .then((next) => {
107
154
  if (next === null) {
108
- setServiceError('Not completed yet — finish the steps in the other window, then check again.');
155
+ setServiceError(
156
+ 'Not completed yet — finish the steps in the other window, then check again.',
157
+ );
109
158
  setPhase(FormPhase.Editing);
110
159
  return;
111
160
  }
112
161
  settle(next);
113
162
  }, failed);
114
- }, [host, payload.ref, payload.statusUrl, settle, failed]);
163
+ }, [host, payload.ref, settle, failed]);
115
164
 
116
165
  // The consent/install window is a separate browsing context, so the only
117
166
  // signal this tab gets is the user returning to it. Re-checking on focus is
@@ -137,6 +186,19 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
137
186
  );
138
187
  }
139
188
 
189
+ if (transportConfigurationError !== null) {
190
+ return (
191
+ <Shell heading={payload.heading} purpose={payload.purpose}>
192
+ <Notice
193
+ tone="error"
194
+ title="The secure service is not configured safely"
195
+ >
196
+ {transportConfigurationError}
197
+ </Notice>
198
+ </Shell>
199
+ );
200
+ }
201
+
140
202
  if (phase === FormPhase.Settled && outcome !== null) {
141
203
  return (
142
204
  <Shell heading={payload.heading} purpose={payload.purpose}>
@@ -146,7 +208,10 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
146
208
  <p className="text-body-1 font-medium text-ink">Submitted.</p>
147
209
  <p className="text-caption text-ink-3">
148
210
  The value went straight to the service and was never shown to the
149
- agent{outcome.connectionId === null ? '.' : `. Connection ${outcome.connectionId} is ready.`}
211
+ agent
212
+ {outcome.connectionId === null
213
+ ? '.'
214
+ : `. Connection ${outcome.connectionId} is ready.`}
150
215
  </p>
151
216
  </div>
152
217
  </div>
@@ -166,7 +231,9 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
166
231
  <div className="flex flex-wrap items-center gap-2">
167
232
  <Button
168
233
  onClick={() => {
169
- globalThis.open(payload.launch?.url, '_blank', 'noopener,noreferrer');
234
+ // `launchUrl` was derived from the host trust anchor above; the
235
+ // payload contributes only a validated same-origin path.
236
+ globalThis.open(launchUrl, '_blank', 'noopener,noreferrer');
170
237
  }}
171
238
  className="gap-1.5"
172
239
  disabled={working}
@@ -174,7 +241,11 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
174
241
  <ExternalLink className="h-4 w-4" />
175
242
  {payload.launch.label}
176
243
  </Button>
177
- <Button variant="outline" onClick={handleCheckStatus} disabled={working}>
244
+ <Button
245
+ variant="outline"
246
+ onClick={handleCheckStatus}
247
+ disabled={working}
248
+ >
178
249
  {working && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
179
250
  I&apos;ve finished — check
180
251
  </Button>
@@ -184,8 +255,8 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
184
255
  <>
185
256
  {payload.fields.length === 0 && (
186
257
  <Notice tone="warn" title="No form fields were declared">
187
- This connector declares no onboarding form. Submitting will ask the
188
- service, which is the authority on whether that is allowed.
258
+ This connector declares no onboarding form. Submitting will ask
259
+ the service, which is the authority on whether that is allowed.
189
260
  </Notice>
190
261
  )}
191
262
  <SchemaForm
@@ -217,16 +288,25 @@ function Shell({
217
288
  heading,
218
289
  purpose,
219
290
  children,
220
- }: Readonly<{ heading?: string; purpose: string; children: ReactNode }>): ReactElement {
291
+ }: Readonly<{
292
+ heading?: string;
293
+ purpose: string;
294
+ children: ReactNode;
295
+ }>): ReactElement {
221
296
  return (
222
297
  <div className="h-full overflow-auto">
223
298
  <div className="mx-auto max-w-2xl space-y-4 p-6">
224
299
  <div className="space-y-1">
225
- <h2 className="text-subtitle font-semibold text-ink">{heading ?? 'Secure request'}</h2>
226
- {purpose.length > 0 && <p className="text-body-1 text-ink-2">{purpose}</p>}
300
+ <h2 className="text-subtitle font-semibold text-ink">
301
+ {heading ?? 'Secure request'}
302
+ </h2>
303
+ {purpose.length > 0 && (
304
+ <p className="text-body-1 text-ink-2">{purpose}</p>
305
+ )}
227
306
  <p className="flex items-center gap-1.5 text-caption text-ink-3">
228
307
  <ShieldCheck className="h-3.5 w-3.5" />
229
- Sent straight to the service with your own sign-in. The agent never sees what you type.
308
+ Sent straight to the service with your own sign-in. The agent never
309
+ sees what you type.
230
310
  </p>
231
311
  </div>
232
312
  {children}
@@ -239,23 +319,42 @@ function Notice({
239
319
  tone,
240
320
  title,
241
321
  children,
242
- }: Readonly<{ tone: 'error' | 'warn'; title: string; children: ReactNode }>): ReactElement {
322
+ }: Readonly<{
323
+ tone: 'error' | 'warn';
324
+ title: string;
325
+ children: ReactNode;
326
+ }>): ReactElement {
243
327
  return (
244
328
  <div
245
329
  role="alert"
246
330
  className={cn(
247
331
  'flex items-start gap-2 rounded-md border p-3',
248
- tone === 'error' ? 'border-destructive/40 bg-destructive/10' : 'border-rule bg-paper-elev',
332
+ tone === 'error'
333
+ ? 'border-destructive/40 bg-destructive/10'
334
+ : 'border-rule bg-paper-elev',
249
335
  )}
250
336
  >
251
337
  <AlertTriangle
252
- className={cn('mt-0.5 h-4 w-4 shrink-0', tone === 'error' ? 'text-destructive' : 'text-ink-3')}
338
+ className={cn(
339
+ 'mt-0.5 h-4 w-4 shrink-0',
340
+ tone === 'error' ? 'text-destructive' : 'text-ink-3',
341
+ )}
253
342
  />
254
343
  <div className="space-y-0.5">
255
- <p className={cn('text-body-1 font-medium', tone === 'error' ? 'text-destructive' : 'text-ink')}>
344
+ <p
345
+ className={cn(
346
+ 'text-body-1 font-medium',
347
+ tone === 'error' ? 'text-destructive' : 'text-ink',
348
+ )}
349
+ >
256
350
  {title}
257
351
  </p>
258
- <p className={cn('text-body-1', tone === 'error' ? 'text-destructive/90' : 'text-ink-3')}>
352
+ <p
353
+ className={cn(
354
+ 'text-body-1',
355
+ tone === 'error' ? 'text-destructive/90' : 'text-ink-3',
356
+ )}
357
+ >
259
358
  {children}
260
359
  </p>
261
360
  </div>
@@ -12,11 +12,15 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
12
12
  import {
13
13
  extractServiceMessage,
14
14
  readElicitationStatus,
15
+ resolveTrustedElicitationEndpoints,
16
+ resolveTrustedElicitationLaunchUrl,
15
17
  submitElicitationFields,
16
18
  VistaFormSubmitError,
19
+ VistaFormTransportConfigError,
17
20
  } from './elicitation-transport';
18
21
 
19
22
  const SERVICE_ORIGIN = 'https://connector-gateway.example.test';
23
+ const API_BASE_URL = `${SERVICE_ORIGIN}/credential-elicitations`;
20
24
  const SUBMIT_URL = `${SERVICE_ORIGIN}/credential-elicitations/ref-1/submit`;
21
25
  const STATUS_URL = `${SERVICE_ORIGIN}/credential-elicitations/ref-1`;
22
26
  const SECRET = 'sk-live-DO-NOT-LEAK';
@@ -39,14 +43,128 @@ afterEach(() => {
39
43
  vi.restoreAllMocks();
40
44
  });
41
45
 
46
+ describe('trusted elicitation endpoint resolution', () => {
47
+ it('derives exact status and submit paths from the host trust anchor', () => {
48
+ expect(resolveTrustedElicitationEndpoints(API_BASE_URL, 'ref-1')).toEqual({
49
+ statusUrl: STATUS_URL,
50
+ submitUrl: SUBMIT_URL,
51
+ });
52
+ });
53
+
54
+ it.each([
55
+ ['relative', '/credential-elicitations'],
56
+ ['network-path', '//evil.example/credential-elicitations'],
57
+ ['scheme confusion', 'javascript:https://trusted.example'],
58
+ [
59
+ 'authority confusion',
60
+ 'https://trusted.example@evil.example/credential-elicitations',
61
+ ],
62
+ [
63
+ 'query',
64
+ 'https://trusted.example/credential-elicitations?next=https://evil.example',
65
+ ],
66
+ [
67
+ 'fragment',
68
+ 'https://trusted.example/credential-elicitations#https://evil.example',
69
+ ],
70
+ [
71
+ 'path traversal',
72
+ 'https://trusted.example/credential-elicitations/../evil',
73
+ ],
74
+ [
75
+ 'encoded path traversal',
76
+ 'https://trusted.example/credential-elicitations/%2e%2e/evil',
77
+ ],
78
+ ])('rejects %s base URL input', (_label, apiBaseUrl) => {
79
+ expect(() =>
80
+ resolveTrustedElicitationEndpoints(apiBaseUrl, 'ref-1'),
81
+ ).toThrow(VistaFormTransportConfigError);
82
+ });
83
+
84
+ it('requires HTTPS for a non-local service', () => {
85
+ expect(() =>
86
+ resolveTrustedElicitationEndpoints(
87
+ 'http://api.example.test/credential-elicitations',
88
+ 'ref-1',
89
+ 'http://host.example.test:8080',
90
+ ),
91
+ ).toThrow(/Insecure elicitation HTTP/);
92
+ });
93
+
94
+ it('permits explicit loopback and same-host HTTP development', () => {
95
+ expect(
96
+ resolveTrustedElicitationEndpoints(
97
+ 'http://localhost:3010/credential-elicitations',
98
+ 'ref-1',
99
+ 'http://localhost:8080',
100
+ ).statusUrl,
101
+ ).toBe('http://localhost:3010/credential-elicitations/ref-1');
102
+ expect(
103
+ resolveTrustedElicitationEndpoints(
104
+ 'http://192.168.1.20:3010/credential-elicitations',
105
+ 'ref-1',
106
+ 'http://192.168.1.20:8080',
107
+ ).statusUrl,
108
+ ).toBe('http://192.168.1.20:3010/credential-elicitations/ref-1');
109
+ });
110
+
111
+ it.each(['.', '..', '../steal', 'a/b', '%2e%2e', 'réf'])(
112
+ 'rejects unsafe ref %s before URL construction',
113
+ (ref) => {
114
+ expect(() =>
115
+ resolveTrustedElicitationEndpoints(API_BASE_URL, ref),
116
+ ).toThrow(VistaFormTransportConfigError);
117
+ },
118
+ );
119
+
120
+ it('keeps launch paths on the trusted origin and rejects authority/traversal', () => {
121
+ expect(
122
+ resolveTrustedElicitationLaunchUrl(
123
+ API_BASE_URL,
124
+ '/oauth/acme/authorize?flow=1',
125
+ ),
126
+ ).toBe(`${SERVICE_ORIGIN}/oauth/acme/authorize?flow=1`);
127
+
128
+ for (const path of [
129
+ 'https://evil.example/steal',
130
+ '//evil.example/steal',
131
+ '/oauth/../steal',
132
+ '/oauth/%2e%2e/steal',
133
+ '/oauth\\..\\steal',
134
+ '/oauth/start#https://evil.example',
135
+ ]) {
136
+ expect(() =>
137
+ resolveTrustedElicitationLaunchUrl(API_BASE_URL, path),
138
+ ).toThrow(VistaFormTransportConfigError);
139
+ }
140
+ });
141
+
142
+ it('does not call fetch when the trusted base or ref is invalid', async () => {
143
+ const fetchMock = vi.fn();
144
+ vi.stubGlobal('fetch', fetchMock);
145
+ await expect(
146
+ submitElicitationFields({
147
+ apiBaseUrl:
148
+ 'https://trusted.example@evil.example/credential-elicitations',
149
+ ref: '../steal',
150
+ accessToken: 'user-jwt',
151
+ fields: { apiKey: SECRET },
152
+ }),
153
+ ).rejects.toBeInstanceOf(VistaFormTransportConfigError);
154
+ expect(fetchMock).not.toHaveBeenCalled();
155
+ });
156
+ });
157
+
42
158
  describe('submitElicitationFields', () => {
43
159
  it('POSTs to the SERVICE origin directly — not a host-relative path', async () => {
44
160
  const fetchMock = stubFetch(
45
- jsonResponse(201, { data: { submitted: true, ref: 'ref-1', connectionId: 'conn-9' } }),
161
+ jsonResponse(201, {
162
+ data: { submitted: true, ref: 'ref-1', connectionId: 'conn-9' },
163
+ }),
46
164
  );
47
165
 
48
166
  await submitElicitationFields({
49
- submitUrl: SUBMIT_URL,
167
+ apiBaseUrl: API_BASE_URL,
50
168
  ref: 'ref-1',
51
169
  accessToken: 'user-jwt',
52
170
  fields: { apiKey: SECRET },
@@ -57,8 +175,11 @@ describe('submitElicitationFields', () => {
57
175
  expect(url).toBe(SUBMIT_URL);
58
176
  expect(new URL(url).origin).toBe(SERVICE_ORIGIN);
59
177
  expect(init.method).toBe('POST');
178
+ expect(init.redirect).toBe('error');
60
179
  // The SUBJECT's own bearer — not a service token, not the agent's.
61
- expect((init.headers as Record<string, string>).Authorization).toBe('Bearer user-jwt');
180
+ expect((init.headers as Record<string, string>).Authorization).toBe(
181
+ 'Bearer user-jwt',
182
+ );
62
183
  });
63
184
 
64
185
  it('never hands a secret back to the caller, even when the service echoes it', async () => {
@@ -76,26 +197,36 @@ describe('submitElicitationFields', () => {
76
197
  );
77
198
 
78
199
  const outcome = await submitElicitationFields({
79
- submitUrl: SUBMIT_URL,
200
+ apiBaseUrl: API_BASE_URL,
80
201
  ref: 'ref-1',
81
202
  accessToken: 'user-jwt',
82
203
  fields: { apiKey: SECRET },
83
204
  });
84
205
 
85
206
  // The outcome is a FRESH object with exactly three keys.
86
- expect(Object.keys(outcome).sort()).toEqual(['connectionId', 'ref', 'submitted']);
87
- expect(outcome).toEqual({ submitted: true, ref: 'ref-1', connectionId: 'conn-9' });
207
+ expect(Object.keys(outcome).sort()).toEqual([
208
+ 'connectionId',
209
+ 'ref',
210
+ 'submitted',
211
+ ]);
212
+ expect(outcome).toEqual({
213
+ submitted: true,
214
+ ref: 'ref-1',
215
+ connectionId: 'conn-9',
216
+ });
88
217
  expect(JSON.stringify(outcome)).not.toContain(SECRET);
89
218
  expect(JSON.stringify(outcome)).not.toContain('apiKey');
90
219
  });
91
220
 
92
221
  it('round-trips a NON-secret field to the service body', async () => {
93
222
  const fetchMock = stubFetch(
94
- jsonResponse(201, { data: { submitted: true, ref: 'ref-1', connectionId: 'conn-9' } }),
223
+ jsonResponse(201, {
224
+ data: { submitted: true, ref: 'ref-1', connectionId: 'conn-9' },
225
+ }),
95
226
  );
96
227
 
97
228
  await submitElicitationFields({
98
- submitUrl: SUBMIT_URL,
229
+ apiBaseUrl: API_BASE_URL,
99
230
  ref: 'ref-1',
100
231
  accessToken: 'user-jwt',
101
232
  fields: { baseUrl: 'https://acme.example.com', apiKey: SECRET },
@@ -108,10 +239,25 @@ describe('submitElicitationFields', () => {
108
239
  });
109
240
 
110
241
  it('accepts a flat (un-enveloped) response body', async () => {
111
- stubFetch(jsonResponse(200, { submitted: true, ref: 'ref-1', connectionId: 'conn-2' }));
242
+ stubFetch(
243
+ jsonResponse(200, {
244
+ submitted: true,
245
+ ref: 'ref-1',
246
+ connectionId: 'conn-2',
247
+ }),
248
+ );
112
249
  await expect(
113
- submitElicitationFields({ submitUrl: SUBMIT_URL, ref: 'ref-1', accessToken: 't', fields: {} }),
114
- ).resolves.toEqual({ submitted: true, ref: 'ref-1', connectionId: 'conn-2' });
250
+ submitElicitationFields({
251
+ apiBaseUrl: API_BASE_URL,
252
+ ref: 'ref-1',
253
+ accessToken: 't',
254
+ fields: {},
255
+ }),
256
+ ).resolves.toEqual({
257
+ submitted: true,
258
+ ref: 'ref-1',
259
+ connectionId: 'conn-2',
260
+ });
115
261
  });
116
262
 
117
263
  it("surfaces the backend's empty-schema 400 as a usable message", async () => {
@@ -126,7 +272,7 @@ describe('submitElicitationFields', () => {
126
272
 
127
273
  // An empty field schema submits an empty map; the SERVICE is the authority.
128
274
  const promise = submitElicitationFields({
129
- submitUrl: SUBMIT_URL,
275
+ apiBaseUrl: API_BASE_URL,
130
276
  ref: 'ref-1',
131
277
  accessToken: 'user-jwt',
132
278
  fields: {},
@@ -139,35 +285,67 @@ describe('submitElicitationFields', () => {
139
285
  it('refuses a 2xx that does not report the elicitation as submitted', async () => {
140
286
  stubFetch(jsonResponse(200, { data: { ref: 'ref-1' } }));
141
287
  await expect(
142
- submitElicitationFields({ submitUrl: SUBMIT_URL, ref: 'ref-1', accessToken: 't', fields: {} }),
288
+ submitElicitationFields({
289
+ apiBaseUrl: API_BASE_URL,
290
+ ref: 'ref-1',
291
+ accessToken: 't',
292
+ fields: {},
293
+ }),
143
294
  ).rejects.toThrow(/did not report the elicitation as submitted/);
144
295
  });
145
296
  });
146
297
 
147
298
  describe('readElicitationStatus', () => {
148
299
  it('returns null while pending — an explicit "not yet", never a guess', async () => {
149
- stubFetch(jsonResponse(200, { data: { ref: 'ref-1', status: 'pending', connectionId: null } }));
300
+ stubFetch(
301
+ jsonResponse(200, {
302
+ data: { ref: 'ref-1', status: 'pending', connectionId: null },
303
+ }),
304
+ );
150
305
  await expect(
151
- readElicitationStatus({ statusUrl: STATUS_URL, ref: 'ref-1', accessToken: 't' }),
306
+ readElicitationStatus({
307
+ apiBaseUrl: API_BASE_URL,
308
+ ref: 'ref-1',
309
+ accessToken: 't',
310
+ }),
152
311
  ).resolves.toBeNull();
153
312
  });
154
313
 
155
314
  it('settles on the service-reported submitted status', async () => {
156
315
  stubFetch(
157
- jsonResponse(200, { data: { ref: 'ref-1', status: 'submitted', connectionId: 'conn-3' } }),
316
+ jsonResponse(200, {
317
+ data: { ref: 'ref-1', status: 'submitted', connectionId: 'conn-3' },
318
+ }),
158
319
  );
159
320
  await expect(
160
- readElicitationStatus({ statusUrl: STATUS_URL, ref: 'ref-1', accessToken: 't' }),
161
- ).resolves.toEqual({ submitted: true, ref: 'ref-1', connectionId: 'conn-3' });
321
+ readElicitationStatus({
322
+ apiBaseUrl: API_BASE_URL,
323
+ ref: 'ref-1',
324
+ accessToken: 't',
325
+ }),
326
+ ).resolves.toEqual({
327
+ submitted: true,
328
+ ref: 'ref-1',
329
+ connectionId: 'conn-3',
330
+ });
162
331
  });
163
332
 
164
333
  it('GETs the service origin with the subject bearer', async () => {
165
- const fetchMock = stubFetch(jsonResponse(200, { data: { ref: 'ref-1', status: 'pending' } }));
166
- await readElicitationStatus({ statusUrl: STATUS_URL, ref: 'ref-1', accessToken: 'user-jwt' });
334
+ const fetchMock = stubFetch(
335
+ jsonResponse(200, { data: { ref: 'ref-1', status: 'pending' } }),
336
+ );
337
+ await readElicitationStatus({
338
+ apiBaseUrl: API_BASE_URL,
339
+ ref: 'ref-1',
340
+ accessToken: 'user-jwt',
341
+ });
167
342
  const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
168
343
  expect(new URL(url).origin).toBe(SERVICE_ORIGIN);
169
344
  expect(init.method).toBe('GET');
170
- expect((init.headers as Record<string, string>).Authorization).toBe('Bearer user-jwt');
345
+ expect(init.redirect).toBe('error');
346
+ expect((init.headers as Record<string, string>).Authorization).toBe(
347
+ 'Bearer user-jwt',
348
+ );
171
349
  });
172
350
  });
173
351
 
@@ -175,11 +353,15 @@ describe('extractServiceMessage', () => {
175
353
  it('reads a string message, a message array, and a nested error message', () => {
176
354
  expect(extractServiceMessage({ message: 'nope' }, 400)).toBe('nope');
177
355
  expect(extractServiceMessage({ message: ['a', 'b'] }, 400)).toBe('a b');
178
- expect(extractServiceMessage({ error: { message: 'deep' } }, 400)).toBe('deep');
356
+ expect(extractServiceMessage({ error: { message: 'deep' } }, 400)).toBe(
357
+ 'deep',
358
+ );
179
359
  });
180
360
 
181
361
  it('never yields an empty string', () => {
182
- expect(extractServiceMessage(null, 503)).toBe('The service rejected the submission (HTTP 503).');
362
+ expect(extractServiceMessage(null, 503)).toBe(
363
+ 'The service rejected the submission (HTTP 503).',
364
+ );
183
365
  expect(extractServiceMessage({ message: '' }, 500)).toBe(
184
366
  'The service rejected the submission (HTTP 500).',
185
367
  );