@xemahq/ui-kernel 0.11.1 → 0.12.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 (257) hide show
  1. package/dist/errors/http-error-status.d.ts +17 -0
  2. package/dist/errors/http-error-status.d.ts.map +1 -0
  3. package/dist/errors/http-error-status.js +59 -0
  4. package/dist/errors/http-error-status.js.map +1 -0
  5. package/dist/errors/index.d.ts +2 -0
  6. package/dist/errors/index.d.ts.map +1 -0
  7. package/dist/errors/index.js +2 -0
  8. package/dist/errors/index.js.map +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/biome-host/errors.d.ts.map +1 -1
  14. package/dist/lib/biome-host/errors.js +1 -19
  15. package/dist/lib/biome-host/errors.js.map +1 -1
  16. package/dist/offline/index.d.ts.map +1 -1
  17. package/dist/offline/index.js.map +1 -1
  18. package/dist/offline/online-controller.d.ts.map +1 -1
  19. package/dist/offline/online-controller.js +6 -1
  20. package/dist/offline/online-controller.js.map +1 -1
  21. package/dist/offline/outbox.d.ts.map +1 -1
  22. package/dist/offline/outbox.js +2 -24
  23. package/dist/offline/outbox.js.map +1 -1
  24. package/package.json +1 -2
  25. package/src/index.ts +0 -17
  26. package/src/lib/biome-host/agent-validation.ts +0 -239
  27. package/src/lib/biome-host/biome-builders.ts +0 -112
  28. package/src/lib/biome-host/biome-mode.ts +0 -6
  29. package/src/lib/biome-host/biome-navigation.ts +0 -37
  30. package/src/lib/biome-host/biome-registry.ts +0 -299
  31. package/src/lib/biome-host/biome-scope-hooks.ts +0 -41
  32. package/src/lib/biome-host/biome-scope.tsx +0 -52
  33. package/src/lib/biome-host/biome-scoped-query.ts +0 -134
  34. package/src/lib/biome-host/create-biome-orval-config.ts +0 -82
  35. package/src/lib/biome-host/define-web-biome.spec.ts +0 -221
  36. package/src/lib/biome-host/define-web-biome.ts +0 -222
  37. package/src/lib/biome-host/errors.ts +0 -220
  38. package/src/lib/biome-host/frontend-biome.ts +0 -256
  39. package/src/lib/biome-host/host-bridge.ts +0 -281
  40. package/src/lib/biome-host/host-sources.ts +0 -54
  41. package/src/lib/biome-host/i18n-hooks.ts +0 -51
  42. package/src/lib/biome-host/i18n-port.test.ts +0 -50
  43. package/src/lib/biome-host/i18n-port.ts +0 -96
  44. package/src/lib/biome-host/index.ts +0 -35
  45. package/src/lib/biome-host/realtime-hooks.ts +0 -75
  46. package/src/lib/biome-host/realtime-port.ts +0 -115
  47. package/src/lib/biome-host/session-contributions.ts +0 -296
  48. package/src/lib/biome-host/use-mutation-with-error-toast.ts +0 -90
  49. package/src/lib/biome-host/use-page-state.ts +0 -248
  50. package/src/lib/capabilities/capability-context.ts +0 -25
  51. package/src/lib/capabilities/capability-provider.tsx +0 -73
  52. package/src/lib/capabilities/index.ts +0 -17
  53. package/src/lib/capabilities/types.ts +0 -69
  54. package/src/lib/capabilities/use-capability.ts +0 -79
  55. package/src/lib/contribution-registry/contribution-registry.spec.ts +0 -141
  56. package/src/lib/contribution-registry/contribution-registry.ts +0 -204
  57. package/src/lib/contribution-registry/index.ts +0 -9
  58. package/src/lib/contribution-registry/renderable-registry.ts +0 -142
  59. package/src/lib/system-bus/capability-invoke.ts +0 -92
  60. package/src/lib/system-bus/deeplink.ts +0 -210
  61. package/src/lib/system-bus/enums.ts +0 -86
  62. package/src/lib/system-bus/host-ports.ts +0 -93
  63. package/src/lib/system-bus/index.ts +0 -16
  64. package/src/lib/system-bus/intent-registry.ts +0 -109
  65. package/src/lib/system-bus/intents.ts +0 -113
  66. package/src/lib/system-bus/palette.ts +0 -78
  67. package/src/lib/system-bus/system-bus-builder.ts +0 -159
  68. package/src/lib/system-bus/system-bus.ts +0 -37
  69. package/src/lib/system-bus/windows.ts +0 -51
  70. package/src/offline/id.ts +0 -16
  71. package/src/offline/index.ts +0 -54
  72. package/src/offline/mutation.ts +0 -75
  73. package/src/offline/offline-status-badge.tsx +0 -51
  74. package/src/offline/online-controller.ts +0 -105
  75. package/src/offline/outbox.test.ts +0 -172
  76. package/src/offline/outbox.ts +0 -348
  77. package/src/offline/react-hooks.ts +0 -40
  78. package/src/offline/react.tsx +0 -32
  79. package/src/offline/storage.ts +0 -112
  80. package/src/offline/types.ts +0 -77
  81. package/src/org-db/components/DbResultTable.tsx +0 -176
  82. package/src/org-db/index.ts +0 -1
  83. package/src/pdf/index.ts +0 -11
  84. package/src/registry/index.ts +0 -9
  85. package/src/registry/lib/agent-validation-host.ts +0 -36
  86. package/src/registry/lib/biome-slot.tsx +0 -50
  87. package/src/registry/lib/biomes-enabled-context.ts +0 -20
  88. package/src/registry/lib/extension-points.ts +0 -134
  89. package/src/registry/lib/primitives/SessionEventCard.tsx +0 -164
  90. package/src/registry/lib/primitives/SessionEventTimeline.tsx +0 -95
  91. package/src/registry/lib/primitives/SessionMutationBar.tsx +0 -76
  92. package/src/registry/lib/primitives/SessionTabbedDrawer.tsx +0 -165
  93. package/src/registry/lib/primitives/index.ts +0 -18
  94. package/src/registry/lib/session-context-builder.ts +0 -84
  95. package/src/registry/lib/session-context-provider.tsx +0 -33
  96. package/src/registry/lib/session-context.ts +0 -21
  97. package/src/registry/lib/session-selectors.ts +0 -272
  98. package/src/session/comments/CommentRail.tsx +0 -164
  99. package/src/session/index.ts +0 -3
  100. package/src/session/lib/cn.ts +0 -11
  101. package/src/session/shell/SessionWorkspaceShell.tsx +0 -147
  102. package/src/session-kit/approvals/ApprovalButton.tsx +0 -89
  103. package/src/session-kit/approvals/ApprovalCard.tsx +0 -341
  104. package/src/session-kit/approvals/ApprovalsCenter.tsx +0 -336
  105. package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +0 -118
  106. package/src/session-kit/approvals/Obligation.tsx +0 -111
  107. package/src/session-kit/approvals/ScopedBody.tsx +0 -392
  108. package/src/session-kit/approvals/approval-icons.ts +0 -31
  109. package/src/session-kit/approvals/approval-model.ts +0 -204
  110. package/src/session-kit/approvals/index.ts +0 -22
  111. package/src/session-kit/approvals/obligation-display.ts +0 -100
  112. package/src/session-kit/approvals/risk-accent.ts +0 -47
  113. package/src/session-kit/approvals/scope-icons.ts +0 -19
  114. package/src/session-kit/combobox/Combobox.tsx +0 -358
  115. package/src/session-kit/combobox/use-click-outside.ts +0 -21
  116. package/src/session-kit/display/ContextHeader.tsx +0 -148
  117. package/src/session-kit/display/FileDiffCard.tsx +0 -140
  118. package/src/session-kit/display/MD.tsx +0 -162
  119. package/src/session-kit/display/MessageTurn.tsx +0 -157
  120. package/src/session-kit/display/ThinkingPanel.tsx +0 -81
  121. package/src/session-kit/display/TodoChecklist.tsx +0 -120
  122. package/src/session-kit/display/TokenMeter.tsx +0 -89
  123. package/src/session-kit/display/ToolStrip.tsx +0 -278
  124. package/src/session-kit/display/TypingDots.tsx +0 -24
  125. package/src/session-kit/index.ts +0 -44
  126. package/src/session-kit/lib/enums.ts +0 -66
  127. package/src/session-kit/lib/portal-accent.ts +0 -30
  128. package/src/session-kit/lib/status-dot.ts +0 -68
  129. package/src/session-kit/primitives/Avatar.tsx +0 -44
  130. package/src/session-kit/primitives/PortalGlyph.tsx +0 -51
  131. package/src/session-kit/primitives/RiskPill.tsx +0 -95
  132. package/src/session-kit/primitives/ScopeDot.tsx +0 -47
  133. package/src/session-kit/primitives/Segmented.tsx +0 -71
  134. package/src/ui/branding/app-branding-context.ts +0 -20
  135. package/src/ui/branding/app-branding-provider.tsx +0 -123
  136. package/src/ui/branding/sanitize.ts +0 -89
  137. package/src/ui/breakpoints.ts +0 -14
  138. package/src/ui/capture/document-preview.tsx +0 -166
  139. package/src/ui/capture/field-spec-to-zod.test.ts +0 -81
  140. package/src/ui/capture/field-spec-to-zod.ts +0 -174
  141. package/src/ui/capture/field-spec-validate.ts +0 -236
  142. package/src/ui/capture/field-spec.test.ts +0 -49
  143. package/src/ui/capture/field-spec.ts +0 -274
  144. package/src/ui/capture/index.ts +0 -51
  145. package/src/ui/capture/schema-field-input.tsx +0 -620
  146. package/src/ui/capture/schema-form.tsx +0 -149
  147. package/src/ui/capture/submit-values.test.ts +0 -70
  148. package/src/ui/capture/submit-values.ts +0 -63
  149. package/src/ui/capture/use-prefill.ts +0 -186
  150. package/src/ui/capture/use-schema-validation.ts +0 -145
  151. package/src/ui/capture/use-wizard.ts +0 -180
  152. package/src/ui/capture/value-format.ts +0 -32
  153. package/src/ui/capture/wizard.tsx +0 -105
  154. package/src/ui/charts/bar-chart.tsx +0 -139
  155. package/src/ui/charts/chart-frame.tsx +0 -125
  156. package/src/ui/charts/chart-types.ts +0 -52
  157. package/src/ui/charts/chart-utils.ts +0 -190
  158. package/src/ui/charts/charts.test.tsx +0 -204
  159. package/src/ui/charts/donut-chart.tsx +0 -129
  160. package/src/ui/charts/index.ts +0 -27
  161. package/src/ui/charts/line-chart.tsx +0 -202
  162. package/src/ui/charts/sparkline.tsx +0 -109
  163. package/src/ui/chrome/AsyncBoundary.tsx +0 -74
  164. package/src/ui/chrome/EmptyState.tsx +0 -257
  165. package/src/ui/chrome/ErrorCard.tsx +0 -63
  166. package/src/ui/chrome/LoadingState.tsx +0 -61
  167. package/src/ui/chrome/PageHeader.tsx +0 -137
  168. package/src/ui/chrome/RecordCardList.tsx +0 -98
  169. package/src/ui/chrome/StateCard.tsx +0 -150
  170. package/src/ui/chrome/confirm-delete-dialog.tsx +0 -59
  171. package/src/ui/chrome/page-shell.spec.tsx +0 -117
  172. package/src/ui/chrome/page-shell.tsx +0 -221
  173. package/src/ui/chrome/scope-badge.tsx +0 -40
  174. package/src/ui/chrome/status-badge.tsx +0 -79
  175. package/src/ui/cn.ts +0 -32
  176. package/src/ui/design-tokens.ts +0 -372
  177. package/src/ui/hooks/use-debounced-value.ts +0 -16
  178. package/src/ui/hooks/use-is-mobile.ts +0 -28
  179. package/src/ui/index.ts +0 -110
  180. package/src/ui/pdf/pdf-preview.tsx +0 -122
  181. package/src/ui/primitives/alert-dialog.tsx +0 -104
  182. package/src/ui/primitives/async-combobox.tsx +0 -182
  183. package/src/ui/primitives/badge-variants.ts +0 -21
  184. package/src/ui/primitives/badge.tsx +0 -25
  185. package/src/ui/primitives/button-variants.ts +0 -31
  186. package/src/ui/primitives/button.tsx +0 -29
  187. package/src/ui/primitives/card.tsx +0 -43
  188. package/src/ui/primitives/checkbox.tsx +0 -26
  189. package/src/ui/primitives/collapsible.tsx +0 -9
  190. package/src/ui/primitives/dialog.tsx +0 -103
  191. package/src/ui/primitives/dropdown-menu.tsx +0 -179
  192. package/src/ui/primitives/form-stepper.tsx +0 -94
  193. package/src/ui/primitives/form-stepper.types.ts +0 -15
  194. package/src/ui/primitives/input.tsx +0 -22
  195. package/src/ui/primitives/label.tsx +0 -17
  196. package/src/ui/primitives/overflow-tabs.tsx +0 -331
  197. package/src/ui/primitives/popover.tsx +0 -33
  198. package/src/ui/primitives/preview-device-frame.tsx +0 -213
  199. package/src/ui/primitives/radio-group.tsx +0 -36
  200. package/src/ui/primitives/resizable.tsx +0 -67
  201. package/src/ui/primitives/scroll-area.tsx +0 -38
  202. package/src/ui/primitives/select.tsx +0 -143
  203. package/src/ui/primitives/separator.tsx +0 -20
  204. package/src/ui/primitives/sheet.tsx +0 -107
  205. package/src/ui/primitives/skeleton.tsx +0 -99
  206. package/src/ui/primitives/switch.tsx +0 -27
  207. package/src/ui/primitives/table.tsx +0 -72
  208. package/src/ui/primitives/tabs.tsx +0 -53
  209. package/src/ui/primitives/tag-multi-select.tsx +0 -261
  210. package/src/ui/primitives/textarea.tsx +0 -21
  211. package/src/ui/primitives/tooltip.tsx +0 -30
  212. package/src/ui/primitives/url-preview-chrome.tsx +0 -103
  213. package/src/vista/GenericCatalogueSurface.tsx +0 -179
  214. package/src/vista/SurfaceFallbackCard.tsx +0 -109
  215. package/src/vista/VistaPanel.tsx +0 -163
  216. package/src/vista/form-payload.spec.ts +0 -139
  217. package/src/vista/html/CustomUiFrame.spec.tsx +0 -127
  218. package/src/vista/html/CustomUiFrame.tsx +0 -206
  219. package/src/vista/html/custom-ui-frame-bridge.spec.ts +0 -191
  220. package/src/vista/html/custom-ui-frame-bridge.ts +0 -98
  221. package/src/vista/html/custom-ui-frame-contract.ts +0 -88
  222. package/src/vista/html/custom-ui-intents.spec.ts +0 -135
  223. package/src/vista/html/custom-ui-intents.ts +0 -155
  224. package/src/vista/html/custom-ui-srcdoc.spec.ts +0 -151
  225. package/src/vista/html/custom-ui-srcdoc.ts +0 -193
  226. package/src/vista/html/index.ts +0 -13
  227. package/src/vista/index.ts +0 -52
  228. package/src/vista/surface-catalogue.spec.ts +0 -140
  229. package/src/vista/surface-catalogue.ts +0 -90
  230. package/src/vista/surface-icons.ts +0 -57
  231. package/src/vista/surface-registry.ts +0 -56
  232. package/src/vista/surfaces/ChartSurface.tsx +0 -34
  233. package/src/vista/surfaces/FilesSurface.tsx +0 -202
  234. package/src/vista/surfaces/FormSurface.tsx +0 -380
  235. package/src/vista/surfaces/HtmlSurface.tsx +0 -43
  236. package/src/vista/surfaces/TableSurface.tsx +0 -100
  237. package/src/vista/surfaces/UrlSurface.tsx +0 -40
  238. package/src/vista/surfaces/blob-surfaces.tsx +0 -139
  239. package/src/vista/surfaces/elicitation-transport.spec.ts +0 -369
  240. package/src/vista/surfaces/elicitation-transport.ts +0 -367
  241. package/src/vista/surfaces/file-surface-utils.ts +0 -69
  242. package/src/vista/surfaces/index.ts +0 -24
  243. package/src/vista/surfaces/register-surfaces.tsx +0 -139
  244. package/src/vista/surfaces/text-surfaces.tsx +0 -132
  245. package/src/vista/use-vista-tabs.ts +0 -196
  246. package/src/vista/vista-code-renderer-context.ts +0 -16
  247. package/src/vista/vista-code-renderer.tsx +0 -35
  248. package/src/vista/vista-file-context.ts +0 -14
  249. package/src/vista/vista-file-source.tsx +0 -141
  250. package/src/vista/vista-form-context.ts +0 -20
  251. package/src/vista/vista-form-host.tsx +0 -48
  252. package/src/vista/vista-model.ts +0 -137
  253. package/src/vista/vista-open-context.tsx +0 -27
  254. package/src/vista/vista-open-state.ts +0 -11
  255. package/src/vista/vista-payloads.ts +0 -635
  256. package/src/vista/vista-tabs-manifest.ts +0 -138
  257. package/src/vista/vista-tools.ts +0 -232
@@ -1,88 +0,0 @@
1
- /**
2
- * The frozen custom-UI frame wire contract — the HOST side of the ONLY channel
3
- * across the sandbox-frame boundary.
4
- *
5
- * Promoted from `app-forge-web` into the UI kernel (Canopy §8.3) so every plane
6
- * that renders untrusted, author-authored UI — App Forge's runtime views AND
7
- * the Vista `html` surface — speaks ONE protocol through ONE hardened frame.
8
- *
9
- * A compiled custom-UI chunk (self-mounting ESM, running inside a strict-CSP,
10
- * opaque-origin iframe) speaks exactly these four message `type`s at exactly
11
- * this protocol version. The chunk's self-mount wrapper is generated by the
12
- * app-function-broker from its OWN copy of these values
13
- * (`repos/xema-cultivars/biomes/platform/app-function-broker/api/src/compile/ui/custom-ui-frame-contract.ts`),
14
- * so the enum members and protocol number here MUST stay byte-for-byte
15
- * identical to that source of truth. Changing a value is a coordinated,
16
- * breaking wire change: bump the protocol on BOTH sides in lockstep.
17
- *
18
- * The chunk never touches host state directly — it announces readiness, the
19
- * host pushes props, the component asks the host to perform intents, and a
20
- * render throw is reported back. The host validates every inbound message
21
- * (source identity + protocol + closed `type` set) before acting.
22
- */
23
-
24
- /** The frozen protocol version stamped on every frame message. */
25
- export const CUSTOM_UI_FRAME_PROTOCOL = 1 as const;
26
-
27
- /**
28
- * The closed set of message `type`s exchanged across the frame boundary. The
29
- * string values are the wire contract and must never diverge from the broker's.
30
- *
31
- * - {@link FrameReady} frame → host: mounted, ready for props (no payload).
32
- * - {@link HostProps} host → frame: `{ props }`; repeatable to re-render.
33
- * - {@link FrameIntent} frame → host: `{ intent }` — the host validates+performs.
34
- * - {@link FrameError} frame → host: `{ message }` — the render threw.
35
- */
36
- export enum CustomUiFrameMessageType {
37
- FrameReady = 'xema.customui.frameReady',
38
- HostProps = 'xema.customui.hostProps',
39
- FrameIntent = 'xema.customui.frameIntent',
40
- FrameError = 'xema.customui.frameError',
41
- }
42
-
43
- /** frame → host: emitted once on load. */
44
- export interface FrameReadyMessage {
45
- readonly type: CustomUiFrameMessageType.FrameReady;
46
- readonly protocol: typeof CUSTOM_UI_FRAME_PROTOCOL;
47
- }
48
-
49
- /** host → frame: the props to render the component with. */
50
- export interface HostPropsMessage {
51
- readonly type: CustomUiFrameMessageType.HostProps;
52
- readonly protocol: typeof CUSTOM_UI_FRAME_PROTOCOL;
53
- readonly props: CustomUiComponentProps;
54
- }
55
-
56
- /** frame → host: the component asked the host to perform an action. */
57
- export interface FrameIntentMessage {
58
- readonly type: CustomUiFrameMessageType.FrameIntent;
59
- readonly protocol: typeof CUSTOM_UI_FRAME_PROTOCOL;
60
- readonly intent: unknown;
61
- }
62
-
63
- /** frame → host: the component's render threw. */
64
- export interface FrameErrorMessage {
65
- readonly type: CustomUiFrameMessageType.FrameError;
66
- readonly protocol: typeof CUSTOM_UI_FRAME_PROTOCOL;
67
- readonly message: string;
68
- }
69
-
70
- /**
71
- * The read-only props payload handed to a custom-UI component. It MUST be a
72
- * plain, structured-clone-serializable object — it crosses the frame boundary
73
- * via `postMessage`. The component also receives an injected `xemaEmit(intent)`
74
- * function (added inside the frame by the self-mount wrapper, NOT sent over the
75
- * wire), so this shape carries data only.
76
- */
77
- export type CustomUiComponentProps = Record<string, unknown>;
78
-
79
- /** Build the host→frame props message (single seam so the shape can't drift). */
80
- export function hostPropsMessage(
81
- props: CustomUiComponentProps,
82
- ): HostPropsMessage {
83
- return {
84
- type: CustomUiFrameMessageType.HostProps,
85
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
86
- props,
87
- };
88
- }
@@ -1,135 +0,0 @@
1
- /**
2
- * Custom-UI intent parsing/dispatch — the frame asks, the host validates+
3
- * performs. Proves the closed set rejects unknown kinds and out-of-policy
4
- * (external) navigation targets, that a valid intent dispatches to its handler,
5
- * and that the two shipped intent SETS behave as their names promise: the
6
- * built-in navigate set parses+performs exactly one kind, and
7
- * `NO_CUSTOM_UI_INTENTS` grants a frame no authority at all.
8
- *
9
- * Migrated from `app-forge-web` when the frame was promoted into the UI kernel;
10
- * runs under the host-web vitest mirror (this package carries no vitest dep).
11
- */
12
- import { describe, expect, it, vi } from 'vitest';
13
-
14
- import {
15
- createNavigateIntentSet,
16
- CustomUiIntentKind,
17
- NO_CUSTOM_UI_INTENTS,
18
- parseCustomUiIntent,
19
- performCustomUiIntent,
20
- type CustomUiIntentHandlers,
21
- } from './custom-ui-intents';
22
-
23
- describe('parseCustomUiIntent', () => {
24
- it('parses a valid host-relative navigate intent', () => {
25
- expect(
26
- parseCustomUiIntent({ kind: 'navigate', href: '/apps/x/page' }),
27
- ).toEqual({ kind: CustomUiIntentKind.Navigate, href: '/apps/x/page' });
28
- });
29
-
30
- it('rejects an unknown intent kind', () => {
31
- expect(parseCustomUiIntent({ kind: 'delete-everything' })).toBeNull();
32
- });
33
-
34
- it('rejects a navigate to an external (protocol-relative) URL', () => {
35
- expect(
36
- parseCustomUiIntent({ kind: 'navigate', href: '//evil.example.com' }),
37
- ).toBeNull();
38
- });
39
-
40
- it('rejects a navigate to an absolute external URL', () => {
41
- expect(
42
- parseCustomUiIntent({ kind: 'navigate', href: 'https://evil.example' }),
43
- ).toBeNull();
44
- });
45
-
46
- // Regression: a backslash normalises to a forward slash (WHATWG), so `/\host`
47
- // is protocol-relative and resolves to a FOREIGN origin — the exact bypass a
48
- // prefix-only check (`startsWith('/') && !startsWith('//')`) missed. The
49
- // origin-resolution validator MUST reject every backslash form.
50
- it('rejects a navigate whose backslash form resolves to a foreign origin', () => {
51
- for (const href of [
52
- '/\\evil.example.com',
53
- '/\\\\evil.example.com',
54
- '/\\/evil.example.com',
55
- '\\/evil.example.com',
56
- ]) {
57
- expect(parseCustomUiIntent({ kind: 'navigate', href })).toBeNull();
58
- }
59
- });
60
-
61
- it('rejects a navigate href containing whitespace or control chars', () => {
62
- expect(
63
- parseCustomUiIntent({ kind: 'navigate', href: '/ /evil' }),
64
- ).toBeNull();
65
- expect(parseCustomUiIntent({ kind: 'navigate', href: '/\tx' })).toBeNull();
66
- });
67
-
68
- it('rejects a navigate with a non-string href', () => {
69
- expect(parseCustomUiIntent({ kind: 'navigate', href: 42 })).toBeNull();
70
- });
71
-
72
- it('rejects non-object payloads', () => {
73
- expect(parseCustomUiIntent(null)).toBeNull();
74
- expect(parseCustomUiIntent('navigate')).toBeNull();
75
- });
76
- });
77
-
78
- describe('performCustomUiIntent', () => {
79
- it('dispatches a navigate intent to the navigate handler', () => {
80
- const navigate = vi.fn();
81
- const handlers: CustomUiIntentHandlers = {
82
- [CustomUiIntentKind.Navigate]: navigate,
83
- };
84
- performCustomUiIntent(
85
- { kind: CustomUiIntentKind.Navigate, href: '/apps/x' },
86
- handlers,
87
- );
88
- expect(navigate).toHaveBeenCalledWith({
89
- kind: CustomUiIntentKind.Navigate,
90
- href: '/apps/x',
91
- });
92
- });
93
- });
94
-
95
- describe('createNavigateIntentSet', () => {
96
- it('parses and performs a host-relative navigate, and only that', () => {
97
- const navigate = vi.fn();
98
- const set = createNavigateIntentSet({
99
- [CustomUiIntentKind.Navigate]: navigate,
100
- });
101
-
102
- const intent = set.parse({ kind: 'navigate', href: '/apps/x' });
103
- expect(intent).toEqual({
104
- kind: CustomUiIntentKind.Navigate,
105
- href: '/apps/x',
106
- });
107
- if (intent === null) {
108
- throw new Error('expected a parsed navigate intent');
109
- }
110
- set.perform(intent);
111
- expect(navigate).toHaveBeenCalledWith({
112
- kind: CustomUiIntentKind.Navigate,
113
- href: '/apps/x',
114
- });
115
-
116
- // Anything outside the closed set is rejected by `parse` — never performed.
117
- expect(set.parse({ kind: 'wipe-db' })).toBeNull();
118
- expect(
119
- set.parse({ kind: 'navigate', href: 'https://evil.example' }),
120
- ).toBeNull();
121
- });
122
- });
123
-
124
- describe('NO_CUSTOM_UI_INTENTS', () => {
125
- it('grants the frame NO authority: every payload is rejected', () => {
126
- for (const raw of [
127
- { kind: 'navigate', href: '/apps/x' },
128
- { kind: 'wipe-db' },
129
- null,
130
- 'navigate',
131
- ]) {
132
- expect(NO_CUSTOM_UI_INTENTS.parse(raw)).toBeNull();
133
- }
134
- });
135
- });
@@ -1,155 +0,0 @@
1
- /**
2
- * The intent plane of the custom-UI frame bridge — now PLUGGABLE.
3
- *
4
- * A component running inside the sandbox frame NEVER acts with the user's
5
- * authority. It posts an intent across the boundary; the host validates it
6
- * against a CLOSED set and performs it. An unrecognised or malformed intent is
7
- * rejected — never coerced, never silently dropped into a default action.
8
- *
9
- * Promoting this out of App Forge required one change and one only: WHICH
10
- * closed set applies is now supplied by the mounting surface
11
- * ({@link CustomUiIntentSet}) instead of being hard-wired. The security shape is
12
- * unchanged — a set is still a `parse` that returns `null` for anything outside
13
- * it, plus a `perform` that is exhaustive over it. There is no "accept any
14
- * intent" set and no free-form action strings.
15
- *
16
- * {@link createNavigateIntentSet} is the built-in set every host gets.
17
- */
18
-
19
- /**
20
- * A closed intent set: parse-or-reject, then perform. `parse` MUST be total and
21
- * MUST NOT throw — returning `null` is the ONLY way to reject, and the bridge
22
- * turns that into `ignored`.
23
- */
24
- export interface CustomUiIntentSet<TIntent> {
25
- parse(raw: unknown): TIntent | null;
26
- perform(intent: TIntent): void;
27
- }
28
-
29
- /** Closed set of intent kinds the built-in set understands. */
30
- export enum CustomUiIntentKind {
31
- /** Navigate the host to an in-app path. */
32
- Navigate = 'navigate',
33
- }
34
-
35
- /**
36
- * Navigate intent — the host routes to `href`. Constrained to a host-relative
37
- * path (leading `/`, no scheme, no protocol-relative `//`) so a frame can never
38
- * drive the host to an arbitrary external origin.
39
- */
40
- export interface NavigateIntent {
41
- readonly kind: CustomUiIntentKind.Navigate;
42
- readonly href: string;
43
- }
44
-
45
- /** The discriminated union of every intent the BUILT-IN set performs. */
46
- export type CustomUiIntent = NavigateIntent;
47
-
48
- /**
49
- * Typed handler set for the built-in intents. Exhaustive over
50
- * {@link CustomUiIntentKind} by construction — adding a kind forces adding a
51
- * handler here (a compile error otherwise), so no intent kind can ship without
52
- * a host-side performer.
53
- */
54
- export interface CustomUiIntentHandlers {
55
- readonly [CustomUiIntentKind.Navigate]: (intent: NavigateIntent) => void;
56
- }
57
-
58
- /**
59
- * True for a same-origin, host-relative path — the only href a frame may request.
60
- *
61
- * A prefix check (`startsWith('/') && !startsWith('//')`) is NOT sufficient:
62
- * browsers (WHATWG URL) normalize a backslash to a forward slash for special
63
- * schemes, so `"/\\evil.com"` is protocol-relative and resolves to a FOREIGN
64
- * origin — a `router.push` of it hard-navigates the victim's top window off to
65
- * `evil.com` (open redirect / phishing). Author code runs inside the frame and
66
- * an authored app is viewed by OTHER users, so this is a real cross-user
67
- * surface. We therefore RESOLVE the href against the current origin and require
68
- * the resolved origin to be identical — the browser's own parser decides, so no
69
- * textual-prefix trick (`\`, control chars, whitespace, embedded scheme) can
70
- * slip a foreign origin past us. A relative href that cannot be parsed, or that
71
- * escapes the origin, is rejected.
72
- */
73
- function isHostRelativePath(href: string): boolean {
74
- // Must be rooted at '/'.
75
- if (!href.startsWith('/')) {
76
- return false;
77
- }
78
- // A backslash is a protocol-relative/scheme trick after WHATWG normalisation
79
- // ("/\\evil.com" -> //evil.com -> foreign origin); whitespace / control
80
- // chars can also confuse the parser. Reject any before resolving.
81
- if (href.includes('\\')) {
82
- return false;
83
- }
84
- // eslint-disable-next-line no-control-regex
85
- if (/[\u0000-\u0020\u007f]/.test(href)) {
86
- return false;
87
- }
88
- let resolved: URL;
89
- try {
90
- resolved = new URL(href, window.location.origin);
91
- } catch {
92
- return false;
93
- }
94
- // The browser's parser is authoritative: the resolved origin MUST equal ours.
95
- return resolved.origin === window.location.origin;
96
- }
97
-
98
- /**
99
- * Parse an untrusted intent payload (arrived from the sandbox frame) into a
100
- * typed {@link CustomUiIntent}, or `null` if it is not a recognized, valid
101
- * member of the closed set. Fail-safe: unknown `kind`, wrong payload shape, or
102
- * an out-of-policy value all return `null` — the caller rejects it.
103
- */
104
- export function parseCustomUiIntent(raw: unknown): CustomUiIntent | null {
105
- if (raw === null || typeof raw !== 'object') {
106
- return null;
107
- }
108
- const candidate = raw as { kind?: unknown; href?: unknown };
109
- switch (candidate.kind) {
110
- case CustomUiIntentKind.Navigate:
111
- if (typeof candidate.href !== 'string') {
112
- return null;
113
- }
114
- if (!isHostRelativePath(candidate.href)) {
115
- return null;
116
- }
117
- return { kind: CustomUiIntentKind.Navigate, href: candidate.href };
118
- default:
119
- return null;
120
- }
121
- }
122
-
123
- /**
124
- * Dispatch a parsed intent to its handler. Exhaustive `switch` over the closed
125
- * set — a new kind added to {@link CustomUiIntentKind} without a `case` here is
126
- * a compile error (the `never` guard), so the set can never be silently
127
- * under-served.
128
- */
129
- export function performCustomUiIntent(
130
- intent: CustomUiIntent,
131
- handlers: CustomUiIntentHandlers,
132
- ): void {
133
- handlers[CustomUiIntentKind.Navigate](intent);
134
- }
135
-
136
- /** The built-in intent set: host-relative navigation and nothing else. */
137
- export function createNavigateIntentSet(
138
- handlers: CustomUiIntentHandlers,
139
- ): CustomUiIntentSet<CustomUiIntent> {
140
- return {
141
- parse: parseCustomUiIntent,
142
- perform: (intent) => performCustomUiIntent(intent, handlers),
143
- };
144
- }
145
-
146
- /**
147
- * The EMPTY intent set — parses nothing, performs nothing. Mount a frame with
148
- * this when the rendered content must be strictly presentational (the Vista
149
- * `html` surface's static-document form). It is an explicit "this frame has no
150
- * authority", not an absent configuration.
151
- */
152
- export const NO_CUSTOM_UI_INTENTS: CustomUiIntentSet<never> = {
153
- parse: () => null,
154
- perform: () => undefined,
155
- };
@@ -1,151 +0,0 @@
1
- /**
2
- * Custom-UI srcdoc / CSP builder — the security invariant lives HERE.
3
- *
4
- * These assertions pin the strict-CSP posture that makes running author- and
5
- * agent-authored content safe, on BOTH forms the frame renders: the compiled,
6
- * SRI-pinned chunk (`script-src '<hash>'`) and the untrusted static document
7
- * (`script-src 'none'`). The mutation blocks prove the invariants IN ISOLATION:
8
- * the suite goes RED if the CSP `script-src` hash is dropped or loosened (e.g.
9
- * to `'unsafe-inline'` / a wildcard), or if the static-document policy ever
10
- * grew a script source.
11
- *
12
- * Migrated verbatim from `app-forge-web` when the frame was promoted into the
13
- * UI kernel; runs under the host-web vitest mirror (this package carries no
14
- * vitest dep).
15
- */
16
- import { describe, expect, it } from 'vitest';
17
-
18
- import {
19
- buildCustomUiFrameCsp,
20
- buildCustomUiSrcDoc,
21
- buildUntrustedHtmlFrameCsp,
22
- buildUntrustedHtmlSrcDoc,
23
- CustomUiSrcDocError,
24
- } from './custom-ui-srcdoc';
25
-
26
- const INTEGRITY = 'sha384-AbC123+/def456Ghi789jklMnoPqr==';
27
- const COMPILED = 'export default function C(){return null}';
28
-
29
- describe('buildCustomUiFrameCsp', () => {
30
- it('emits the exact airtight directive string, pinning the server hash', () => {
31
- expect(buildCustomUiFrameCsp(INTEGRITY)).toBe(
32
- `default-src 'none'; script-src '${INTEGRITY}'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'`,
33
- );
34
- });
35
-
36
- it('script-src carries the server integrity token verbatim', () => {
37
- expect(buildCustomUiFrameCsp(INTEGRITY)).toContain(
38
- `script-src '${INTEGRITY}'`,
39
- );
40
- });
41
-
42
- it('never grants scripts unsafe-inline or a wildcard', () => {
43
- const csp = buildCustomUiFrameCsp(INTEGRITY);
44
- expect(csp).not.toContain("script-src 'unsafe-inline'");
45
- expect(csp).not.toContain('script-src *');
46
- expect(csp).toContain(`default-src 'none'`);
47
- });
48
- });
49
-
50
- describe('buildCustomUiSrcDoc', () => {
51
- const srcDoc = buildCustomUiSrcDoc({
52
- compiledOutput: COMPILED,
53
- integrity: INTEGRITY,
54
- styleCss: ':root{--x:1}',
55
- });
56
-
57
- it('embeds the pinned CSP as a meta http-equiv', () => {
58
- expect(srcDoc).toContain(
59
- `<meta http-equiv="Content-Security-Policy" content="${buildCustomUiFrameCsp(
60
- INTEGRITY,
61
- )}" />`,
62
- );
63
- });
64
-
65
- it('inlines the chunk verbatim as the one module script (hash must match)', () => {
66
- expect(srcDoc).toContain(`<script type="module">${COMPILED}</script>`);
67
- });
68
-
69
- it('carries the #root mount point the self-mount wrapper renders into', () => {
70
- expect(srcDoc).toContain('<div id="root"></div>');
71
- });
72
-
73
- it('inlines the design-system style (style-src unsafe-inline)', () => {
74
- expect(srcDoc).toContain('<style>:root{--x:1}</style>');
75
- });
76
-
77
- it('refuses a chunk containing a literal </script (would truncate the module)', () => {
78
- expect(() =>
79
- buildCustomUiSrcDoc({
80
- compiledOutput: 'const x = "a</script><script>alert(1)</script>";',
81
- integrity: INTEGRITY,
82
- styleCss: '',
83
- }),
84
- ).toThrow(CustomUiSrcDocError);
85
- });
86
-
87
- // ── MUTATION TEST (security invariant, isolated) ───────────────────────────
88
- // A loosened policy that drops the hash pin must FAIL these assertions. This
89
- // proves the guard is load-bearing: remove/loosen the script-src hash and the
90
- // suite goes red.
91
- it('MUTATION: dropping the script-src hash makes the CSP invalid — caught', () => {
92
- const loosened = `default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'`;
93
- // The real builder must NOT equal the loosened (hash-dropped) policy…
94
- expect(buildCustomUiFrameCsp(INTEGRITY)).not.toBe(loosened);
95
- // …and the real policy MUST pin the exact hash the loosened one dropped.
96
- expect(buildCustomUiFrameCsp(INTEGRITY)).toContain(
97
- `script-src '${INTEGRITY}'`,
98
- );
99
- });
100
- });
101
-
102
- describe('buildUntrustedHtmlFrameCsp', () => {
103
- it('emits the same airtight policy with script-src none', () => {
104
- expect(buildUntrustedHtmlFrameCsp()).toBe(
105
- `default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'`,
106
- );
107
- });
108
- });
109
-
110
- describe('buildUntrustedHtmlSrcDoc', () => {
111
- const DOCUMENT = '<h1>Report</h1><p>Static, agent-authored markup.</p>';
112
- const srcDoc = buildUntrustedHtmlSrcDoc({
113
- document: DOCUMENT,
114
- styleCss: ':root{--x:1}',
115
- });
116
-
117
- it("pins script-src 'none' — an authored <script> can NEVER execute", () => {
118
- expect(srcDoc).toContain(
119
- `<meta http-equiv="Content-Security-Policy" content="${buildUntrustedHtmlFrameCsp()}" />`,
120
- );
121
- expect(srcDoc).toContain(`script-src 'none'`);
122
- // No script source of any kind may be granted on the static path.
123
- expect(srcDoc).not.toContain("script-src 'unsafe-inline'");
124
- expect(srcDoc).not.toContain('script-src *');
125
- expect(srcDoc).not.toContain('script-src sha384-');
126
- expect(srcDoc).toContain(`default-src 'none'`);
127
- expect(srcDoc).toContain(`base-uri 'none'`);
128
- expect(srcDoc).toContain(`form-action 'none'`);
129
- });
130
-
131
- it('emits the document verbatim with the inlined styles', () => {
132
- expect(srcDoc).toContain('<style>:root{--x:1}</style>');
133
- expect(srcDoc).toContain(DOCUMENT);
134
- });
135
-
136
- it('refuses a document containing a literal </script (fail-closed)', () => {
137
- expect(() =>
138
- buildUntrustedHtmlSrcDoc({
139
- document: '<p>ok</p></script><script>alert(1)</script>',
140
- styleCss: '',
141
- }),
142
- ).toThrow(CustomUiSrcDocError);
143
- });
144
-
145
- // ── MUTATION TEST (static-document invariant, isolated) ────────────────────
146
- it('MUTATION: granting the static document any script source is caught', () => {
147
- const loosened = `default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'`;
148
- expect(buildUntrustedHtmlFrameCsp()).not.toBe(loosened);
149
- expect(buildUntrustedHtmlFrameCsp()).toContain(`script-src 'none'`);
150
- });
151
- });