@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,127 +0,0 @@
1
- /**
2
- * CustomUiFrame — the executes-author-code security surface.
3
- *
4
- * Asserts the opaque-origin posture on the rendered iframe: sandbox is
5
- * `allow-scripts` with NO `allow-same-origin`, the srcdoc pins the server SRI
6
- * hash as the CSP script-src, and the chunk bytes are the inline module script.
7
- * The static-document form is asserted too: same containment, `script-src
8
- * 'none'`, so an authored `<script>` can never run.
9
- *
10
- * The final block MUTATION-tests the sandbox invariant IN ISOLATION: it proves
11
- * the suite goes RED if `allow-same-origin` is ever added to the sandbox token
12
- * (which would dissolve the origin boundary), independent of the CSP-hash
13
- * mutation proof in `custom-ui-srcdoc.spec.ts`.
14
- *
15
- * Migrated from `app-forge-web` when the frame was promoted into the UI kernel;
16
- * runs under the host-web vitest mirror (this package carries no vitest dep).
17
- */
18
- import { renderToStaticMarkup } from 'react-dom/server';
19
- import { describe, expect, it, vi } from 'vitest';
20
-
21
- import {
22
- createNavigateIntentSet,
23
- CustomUiIntentKind,
24
- NO_CUSTOM_UI_INTENTS,
25
- } from './custom-ui-intents';
26
- import { CustomUiFrame } from './CustomUiFrame';
27
-
28
- const INTEGRITY = 'sha384-AbC123def456Ghi789jklMnoPqr';
29
- const COMPILED = 'export default function C(){return null}';
30
- // The style import is a HOST concern (the kernel defaults to its own reset) —
31
- // this test is about the security surface, so it pins a trivial stylesheet.
32
- const STYLE_CSS = ':root{--x:1}';
33
-
34
- function parseIframe(markup: string): Element {
35
- const doc = new DOMParser().parseFromString(markup, 'text/html');
36
- const iframe = doc.querySelector('iframe');
37
- if (!iframe) {
38
- throw new Error('no iframe rendered');
39
- }
40
- return iframe;
41
- }
42
-
43
- function renderFrame() {
44
- return parseIframe(
45
- renderToStaticMarkup(
46
- <CustomUiFrame
47
- content={{ compiledOutput: COMPILED, integrity: INTEGRITY }}
48
- componentProps={{ records: [] }}
49
- intents={createNavigateIntentSet({
50
- [CustomUiIntentKind.Navigate]: vi.fn(),
51
- })}
52
- styleCss={STYLE_CSS}
53
- title="Test custom view"
54
- />,
55
- ),
56
- );
57
- }
58
-
59
- function renderDocumentFrame(document: string) {
60
- return parseIframe(
61
- renderToStaticMarkup(
62
- <CustomUiFrame
63
- content={{ document }}
64
- intents={NO_CUSTOM_UI_INTENTS}
65
- styleCss={STYLE_CSS}
66
- title="Test static document"
67
- />,
68
- ),
69
- );
70
- }
71
-
72
- describe('CustomUiFrame security posture', () => {
73
- it('renders an opaque-origin sandbox: allow-scripts, NO allow-same-origin', () => {
74
- const iframe = renderFrame();
75
- expect(iframe.getAttribute('sandbox')).toBe('allow-scripts');
76
- expect(iframe.getAttribute('sandbox')).not.toContain('allow-same-origin');
77
- });
78
-
79
- it('pins the server integrity as the CSP script-src in the srcdoc', () => {
80
- const srcdoc = renderFrame().getAttribute('srcdoc') ?? '';
81
- expect(srcdoc).toContain(`script-src '${INTEGRITY}'`);
82
- expect(srcdoc).toContain(`default-src 'none'`);
83
- });
84
-
85
- it('inlines the chunk bytes as the one module script', () => {
86
- const srcdoc = renderFrame().getAttribute('srcdoc') ?? '';
87
- expect(srcdoc).toContain(`<script type="module">${COMPILED}</script>`);
88
- expect(srcdoc).toContain('<div id="root"></div>');
89
- });
90
-
91
- // ── MUTATION TEST (sandbox invariant, isolated) ────────────────────────────
92
- // Prove the assertion is load-bearing: if the component were changed to
93
- // `sandbox="allow-scripts allow-same-origin"`, THIS exact assertion fails.
94
- it('MUTATION: adding allow-same-origin to the sandbox token is caught', () => {
95
- const sandbox = renderFrame().getAttribute('sandbox') ?? '';
96
- // Exact-match pin: any extra token (e.g. allow-same-origin) breaks equality.
97
- expect(sandbox).toBe('allow-scripts');
98
- // And the explicit negative guard the mutation would trip.
99
- const mutated = 'allow-scripts allow-same-origin';
100
- expect(sandbox).not.toBe(mutated);
101
- });
102
- });
103
-
104
- describe('CustomUiFrame static-document form', () => {
105
- it("renders an untrusted document with script-src 'none' in the same sandbox", () => {
106
- const iframe = renderDocumentFrame('<h1>Report</h1>');
107
- expect(iframe.getAttribute('sandbox')).toBe('allow-scripts');
108
- const srcdoc = iframe.getAttribute('srcdoc') ?? '';
109
- expect(srcdoc).toContain(`script-src 'none'`);
110
- expect(srcdoc).toContain(`default-src 'none'`);
111
- expect(srcdoc).toContain('<h1>Report</h1>');
112
- });
113
-
114
- it('renders a visible notice (no iframe) when the document cannot be built safely', () => {
115
- const markup = renderToStaticMarkup(
116
- <CustomUiFrame
117
- content={{ document: '<p>x</p></script><script>alert(1)</script>' }}
118
- intents={NO_CUSTOM_UI_INTENTS}
119
- styleCss={STYLE_CSS}
120
- title="Unsafe document"
121
- />,
122
- );
123
- expect(markup).toContain('role="alert"');
124
- expect(markup).toContain('could not be prepared safely');
125
- expect(markup).not.toContain('<iframe');
126
- });
127
- });
@@ -1,206 +0,0 @@
1
- /**
2
- * `CustomUiFrame` — the reusable, security-hardened renderer for untrusted UI.
3
- * It runs author-/agent-authored content inside an OPAQUE-ORIGIN sandbox iframe
4
- * and bridges it to the host over the frozen `postMessage` contract.
5
- *
6
- * Promoted from `app-forge-web` into the UI kernel (Canopy §8.3) so App Forge's
7
- * runtime views and the Vista `html` surface share ONE hardened frame. The
8
- * intent set is now supplied by the mounting surface
9
- * ({@link CustomUiIntentSet}); everything below is carried over verbatim.
10
- *
11
- * Security posture (NON-NEGOTIABLE — this renders untrusted content):
12
- * • sandbox="allow-scripts" and NO allow-same-origin → the frame runs in a
13
- * unique opaque origin and cannot reach host DOM, cookies, localStorage or
14
- * tokens.
15
- * • srcdoc with a `<meta>` CSP that pins the chunk's server SRI hash as the
16
- * sole script-src (or `'none'` for a static document) and sets default-src
17
- * 'none' → no network, only the exact pinned bytes execute. See
18
- * {@link buildCustomUiSrcDoc} / {@link buildUntrustedHtmlSrcDoc}.
19
- * • Every inbound message is validated by {@link classifyFrameMessage}
20
- * (source-window identity + protocol + closed type set) before the host
21
- * acts. Intents are validated against a closed set and performed by the
22
- * host — the frame never acts with the user's authority.
23
- *
24
- * The host pushes read-only props on `frameReady` (and re-pushes when they
25
- * change), performs validated intents, and surfaces a render error the frame
26
- * reports — never a blank frame.
27
- */
28
- import { AlertTriangle } from 'lucide-react';
29
- import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
30
-
31
- import { classifyFrameMessage } from './custom-ui-frame-bridge';
32
- import {
33
- hostPropsMessage,
34
- type CustomUiComponentProps,
35
- } from './custom-ui-frame-contract';
36
- import { type CustomUiIntentSet } from './custom-ui-intents';
37
- import {
38
- CUSTOM_UI_FRAME_RESET_CSS,
39
- buildCustomUiSrcDoc,
40
- buildUntrustedHtmlSrcDoc,
41
- } from './custom-ui-srcdoc';
42
- import { cn } from '../../ui/cn';
43
-
44
- /** What the frame renders. Exactly one of the two forms. */
45
- export type CustomUiFrameContent =
46
- | {
47
- /** A server-compiled, SRI-pinned, self-mounting ESM chunk. */
48
- readonly compiledOutput: string;
49
- /** Server SRI/CSP token (`sha384-<base64>`) pinning the chunk bytes. */
50
- readonly integrity: string;
51
- }
52
- | {
53
- /** An untrusted HTML document. Rendered with `script-src 'none'`. */
54
- readonly document: string;
55
- };
56
-
57
- export interface CustomUiFrameProps<TIntent> {
58
- /** The content to render. */
59
- readonly content: CustomUiFrameContent;
60
- /** Read-only, serializable props handed to a compiled component. */
61
- readonly componentProps?: CustomUiComponentProps;
62
- /** The CLOSED intent set this frame may exercise against the host. */
63
- readonly intents: CustomUiIntentSet<TIntent>;
64
- /**
65
- * CSS inlined into the frame (the frame cannot fetch a stylesheet — its CSP
66
- * is `default-src 'none'`). Defaults to {@link CUSTOM_UI_FRAME_RESET_CSS};
67
- * hosts prepend it to their own design-token CSS.
68
- */
69
- readonly styleCss?: string;
70
- /** Accessible title for the iframe. */
71
- readonly title: string;
72
- readonly className?: string;
73
- }
74
-
75
- const EMPTY_PROPS: CustomUiComponentProps = {};
76
-
77
- export function CustomUiFrame<TIntent>({
78
- content,
79
- componentProps = EMPTY_PROPS,
80
- intents,
81
- styleCss = CUSTOM_UI_FRAME_RESET_CSS,
82
- title,
83
- className,
84
- }: CustomUiFrameProps<TIntent>): ReactElement {
85
- const iframeRef = useRef<HTMLIFrameElement | null>(null);
86
- const [frameError, setFrameError] = useState<string | null>(null);
87
-
88
- // Latest props/intent set, read by the message listener without
89
- // re-subscribing (the listener is attached once; a props re-render is handled
90
- // by a dedicated effect below).
91
- const propsRef = useRef(componentProps);
92
- propsRef.current = componentProps;
93
- const intentsRef = useRef(intents);
94
- intentsRef.current = intents;
95
- const readyRef = useRef(false);
96
-
97
- // The srcdoc is a pure function of the content identity + inlined styles — it
98
- // does NOT depend on props (props flow over postMessage, not by re-mounting
99
- // the frame). Recomputing only on content change avoids reloading the frame
100
- // on every prop update.
101
- // The builders throw on content that would truncate the inline module (a
102
- // `</script`-bearing body) — surface it as a visible error rather than
103
- // white-screening. `null` srcDoc ⇒ render the notice, no iframe.
104
- const isCompiled = 'compiledOutput' in content;
105
- const compiledOutput = isCompiled ? content.compiledOutput : null;
106
- const integrity = isCompiled ? content.integrity : null;
107
- const document = isCompiled ? null : content.document;
108
- const srcDoc = useMemo<string | null>(() => {
109
- try {
110
- return compiledOutput !== null && integrity !== null
111
- ? buildCustomUiSrcDoc({ compiledOutput, integrity, styleCss })
112
- : buildUntrustedHtmlSrcDoc({ document: document ?? '', styleCss });
113
- } catch {
114
- return null;
115
- }
116
- }, [compiledOutput, integrity, document, styleCss]);
117
-
118
- // Fresh content is a fresh frame lifecycle — reset ready + error state.
119
- useEffect(() => {
120
- readyRef.current = false;
121
- setFrameError(null);
122
- }, [srcDoc]);
123
-
124
- useEffect(() => {
125
- function onMessage(event: MessageEvent): void {
126
- const frameWindow = iframeRef.current?.contentWindow ?? null;
127
- const classified = classifyFrameMessage(
128
- event,
129
- frameWindow,
130
- intentsRef.current.parse,
131
- );
132
- switch (classified.kind) {
133
- case 'ready':
134
- readyRef.current = true;
135
- setFrameError(null);
136
- frameWindow?.postMessage(hostPropsMessage(propsRef.current), '*');
137
- return;
138
- case 'intent':
139
- intentsRef.current.perform(classified.intent);
140
- return;
141
- case 'error':
142
- setFrameError(classified.message);
143
- return;
144
- case 'ignored':
145
- return;
146
- }
147
- }
148
- window.addEventListener('message', onMessage);
149
- return () => window.removeEventListener('message', onMessage);
150
- }, []);
151
-
152
- // Re-push props to an already-ready frame when they change (repeatable
153
- // re-render, per the HostProps contract). Before ready, the frameReady
154
- // handler above pushes the current props — so no update is lost.
155
- useEffect(() => {
156
- if (!readyRef.current) {
157
- return;
158
- }
159
- iframeRef.current?.contentWindow?.postMessage(
160
- hostPropsMessage(componentProps),
161
- '*',
162
- );
163
- }, [componentProps]);
164
-
165
- if (srcDoc === null) {
166
- return (
167
- <div className={className}>
168
- <div
169
- role="alert"
170
- className="flex items-start gap-2 rounded border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive"
171
- >
172
- <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
173
- <span>
174
- This custom view could not be prepared safely and was not rendered.
175
- </span>
176
- </div>
177
- </div>
178
- );
179
- }
180
-
181
- return (
182
- <div className={cn('flex h-full min-h-0 flex-col', className)}>
183
- {frameError !== null ? (
184
- <div
185
- role="alert"
186
- className="mb-2 flex items-start gap-2 rounded border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive"
187
- >
188
- <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
189
- <span>This custom view failed to render: {frameError}</span>
190
- </div>
191
- ) : null}
192
- <iframe
193
- ref={iframeRef}
194
- title={title}
195
- // OPAQUE ORIGIN: allow-scripts ONLY. Adding allow-same-origin would
196
- // dissolve the origin boundary and give author code access to host
197
- // cookies/DOM/tokens — never add it.
198
- sandbox="allow-scripts"
199
- srcDoc={srcDoc}
200
- className="min-h-[24rem] w-full flex-1 rounded border border-rule bg-paper"
201
- />
202
- </div>
203
- );
204
- }
205
-
206
- export default CustomUiFrame;
@@ -1,191 +0,0 @@
1
- /**
2
- * The host-side inbound-message classifier — the security gate for every
3
- * message the host acts on. Proves it REJECTS a wrong-source message, a
4
- * wrong-protocol message, an unknown type, and a well-formed envelope carrying
5
- * an unknown intent; and that it correctly classifies frameReady / a valid
6
- * intent / a render error. Also proves frameReady is answered with a hostProps
7
- * message (via the paired builder), and that a frame mounted with the EMPTY
8
- * intent set can never get an intent performed.
9
- *
10
- * Migrated from `app-forge-web` when the frame was promoted into the UI kernel
11
- * (the intent parser is now an argument instead of a hard-wired import — every
12
- * gate is otherwise unchanged); runs under the host-web vitest mirror.
13
- */
14
- import { describe, expect, it } from 'vitest';
15
-
16
- import { classifyFrameMessage } from './custom-ui-frame-bridge';
17
- import {
18
- CUSTOM_UI_FRAME_PROTOCOL,
19
- CustomUiFrameMessageType,
20
- hostPropsMessage,
21
- } from './custom-ui-frame-contract';
22
- import {
23
- CustomUiIntentKind,
24
- NO_CUSTOM_UI_INTENTS,
25
- parseCustomUiIntent,
26
- } from './custom-ui-intents';
27
-
28
- // A stand-in for the iframe's contentWindow — identity is all the gate checks.
29
- const FRAME_WINDOW = { name: 'frame' } as unknown as Window;
30
- const OTHER_WINDOW = { name: 'other' } as unknown as Window;
31
-
32
- function evt(
33
- data: unknown,
34
- source: Window | null,
35
- ): Pick<MessageEvent, 'source' | 'data'> {
36
- return { data, source };
37
- }
38
-
39
- describe('classifyFrameMessage', () => {
40
- it('classifies a valid frameReady', () => {
41
- const result = classifyFrameMessage(
42
- evt(
43
- {
44
- type: CustomUiFrameMessageType.FrameReady,
45
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
46
- },
47
- FRAME_WINDOW,
48
- ),
49
- FRAME_WINDOW,
50
- parseCustomUiIntent,
51
- );
52
- expect(result).toEqual({ kind: 'ready' });
53
- });
54
-
55
- it('classifies a valid frameIntent into a typed intent', () => {
56
- const result = classifyFrameMessage(
57
- evt(
58
- {
59
- type: CustomUiFrameMessageType.FrameIntent,
60
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
61
- intent: { kind: 'navigate', href: '/apps/x/page' },
62
- },
63
- FRAME_WINDOW,
64
- ),
65
- FRAME_WINDOW,
66
- parseCustomUiIntent,
67
- );
68
- expect(result).toEqual({
69
- kind: 'intent',
70
- intent: { kind: CustomUiIntentKind.Navigate, href: '/apps/x/page' },
71
- });
72
- });
73
-
74
- it('classifies a frameError, surfacing the message', () => {
75
- const result = classifyFrameMessage(
76
- evt(
77
- {
78
- type: CustomUiFrameMessageType.FrameError,
79
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
80
- message: 'boom',
81
- },
82
- FRAME_WINDOW,
83
- ),
84
- FRAME_WINDOW,
85
- parseCustomUiIntent,
86
- );
87
- expect(result).toEqual({ kind: 'error', message: 'boom' });
88
- });
89
-
90
- it('REJECTS a message from the wrong source window', () => {
91
- const result = classifyFrameMessage(
92
- evt(
93
- {
94
- type: CustomUiFrameMessageType.FrameReady,
95
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
96
- },
97
- OTHER_WINDOW,
98
- ),
99
- FRAME_WINDOW,
100
- parseCustomUiIntent,
101
- );
102
- expect(result).toEqual({ kind: 'ignored' });
103
- });
104
-
105
- it('REJECTS a message with the wrong protocol', () => {
106
- const result = classifyFrameMessage(
107
- evt(
108
- { type: CustomUiFrameMessageType.FrameReady, protocol: 999 },
109
- FRAME_WINDOW,
110
- ),
111
- FRAME_WINDOW,
112
- parseCustomUiIntent,
113
- );
114
- expect(result).toEqual({ kind: 'ignored' });
115
- });
116
-
117
- it('REJECTS an unknown message type', () => {
118
- const result = classifyFrameMessage(
119
- evt(
120
- {
121
- type: 'xema.customui.somethingElse',
122
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
123
- },
124
- FRAME_WINDOW,
125
- ),
126
- FRAME_WINDOW,
127
- parseCustomUiIntent,
128
- );
129
- expect(result).toEqual({ kind: 'ignored' });
130
- });
131
-
132
- it('REJECTS a well-formed envelope carrying an unknown intent', () => {
133
- const result = classifyFrameMessage(
134
- evt(
135
- {
136
- type: CustomUiFrameMessageType.FrameIntent,
137
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
138
- intent: { kind: 'wipe-db' },
139
- },
140
- FRAME_WINDOW,
141
- ),
142
- FRAME_WINDOW,
143
- parseCustomUiIntent,
144
- );
145
- expect(result).toEqual({ kind: 'ignored' });
146
- });
147
-
148
- it('REJECTS when no frame window is mounted yet', () => {
149
- const result = classifyFrameMessage(
150
- evt(
151
- {
152
- type: CustomUiFrameMessageType.FrameReady,
153
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
154
- },
155
- FRAME_WINDOW,
156
- ),
157
- null,
158
- parseCustomUiIntent,
159
- );
160
- expect(result).toEqual({ kind: 'ignored' });
161
- });
162
-
163
- // The intent set is now supplied by the mounting surface — a frame mounted
164
- // with the EMPTY set has no authority, so even a perfectly-formed navigate
165
- // envelope is ignored.
166
- it('REJECTS every intent when mounted with the EMPTY intent set', () => {
167
- const result = classifyFrameMessage(
168
- evt(
169
- {
170
- type: CustomUiFrameMessageType.FrameIntent,
171
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
172
- intent: { kind: 'navigate', href: '/apps/x/page' },
173
- },
174
- FRAME_WINDOW,
175
- ),
176
- FRAME_WINDOW,
177
- NO_CUSTOM_UI_INTENTS.parse,
178
- );
179
- expect(result).toEqual({ kind: 'ignored' });
180
- });
181
-
182
- it('answers frameReady with a hostProps message (paired builder)', () => {
183
- const props = { records: [{ id: 'r1' }] };
184
- const message = hostPropsMessage(props);
185
- expect(message).toEqual({
186
- type: CustomUiFrameMessageType.HostProps,
187
- protocol: CUSTOM_UI_FRAME_PROTOCOL,
188
- props,
189
- });
190
- });
191
- });
@@ -1,98 +0,0 @@
1
- /**
2
- * The host-side inbound-message classifier for the sandbox-frame bridge.
3
- *
4
- * Every `message` event the host window receives while a frame is mounted
5
- * passes through {@link classifyFrameMessage}. It enforces the security gate
6
- * BEFORE the host acts on anything:
7
- *
8
- * 1. SOURCE IDENTITY — `event.source` must be the frame's own
9
- * `contentWindow`. We compare the window identity, NOT `event.origin`: an
10
- * opaque-origin sandbox frame (no `allow-same-origin`) reports its origin
11
- * as the string `"null"`, so origin is useless as an allow-check. Window
12
- * identity is unforgeable across the boundary.
13
- * 2. PROTOCOL — `event.data.protocol` must equal the pinned protocol version.
14
- * 3. TYPE — `event.data.type` must be a member of the closed
15
- * {@link CustomUiFrameMessageType} set.
16
- *
17
- * Anything failing any gate is classified `ignored` and the host does nothing.
18
- * A valid message is narrowed to a typed, discriminated result the caller acts
19
- * on. The intent payload is parsed against the mounting surface's CLOSED intent
20
- * set here too, so a frame that posts a well-formed envelope but an
21
- * unknown/invalid intent is still `ignored` — never performed.
22
- *
23
- * Promoted from `app-forge-web` into the UI kernel (Canopy §8.3). The only
24
- * change is that the intent set is a parameter instead of a hard-wired import;
25
- * every gate is carried over verbatim.
26
- */
27
- import {
28
- CUSTOM_UI_FRAME_PROTOCOL,
29
- CustomUiFrameMessageType,
30
- } from './custom-ui-frame-contract';
31
-
32
- /** The typed outcome of classifying one inbound frame message. */
33
- export type ClassifiedFrameMessage<TIntent> =
34
- | { readonly kind: 'ready' }
35
- | { readonly kind: 'intent'; readonly intent: TIntent }
36
- | { readonly kind: 'error'; readonly message: string }
37
- | { readonly kind: 'ignored' };
38
-
39
- const IGNORED: ClassifiedFrameMessage<never> = { kind: 'ignored' };
40
-
41
- /**
42
- * Classify an inbound `message` event from a sandbox frame. `frameWindow` is the
43
- * mounted iframe's `contentWindow` (may be `null` before mount); `parseIntent`
44
- * is the mounting surface's closed-set parser (returns `null` to reject).
45
- * Returns a typed result; every rejection path returns `ignored` (fail-safe —
46
- * the host performs nothing on a message it cannot fully validate).
47
- */
48
- export function classifyFrameMessage<TIntent>(
49
- event: Pick<MessageEvent, 'source' | 'data'>,
50
- frameWindow: Window | null,
51
- parseIntent: (raw: unknown) => TIntent | null,
52
- ): ClassifiedFrameMessage<TIntent> {
53
- // Gate 1: source identity. Reject if we have no frame yet or the message did
54
- // not come from THIS frame's window.
55
- if (frameWindow === null || event.source !== frameWindow) {
56
- return IGNORED;
57
- }
58
-
59
- const data = event.data;
60
- if (data === null || typeof data !== 'object') {
61
- return IGNORED;
62
- }
63
- const envelope = data as {
64
- protocol?: unknown;
65
- type?: unknown;
66
- intent?: unknown;
67
- message?: unknown;
68
- };
69
-
70
- // Gate 2: protocol version.
71
- if (envelope.protocol !== CUSTOM_UI_FRAME_PROTOCOL) {
72
- return IGNORED;
73
- }
74
-
75
- // Gate 3: closed message-type set.
76
- switch (envelope.type) {
77
- case CustomUiFrameMessageType.FrameReady:
78
- return { kind: 'ready' };
79
- case CustomUiFrameMessageType.FrameIntent: {
80
- const intent = parseIntent(envelope.intent);
81
- // A valid envelope carrying an unknown/invalid intent is rejected, not
82
- // performed.
83
- return intent === null ? IGNORED : { kind: 'intent', intent };
84
- }
85
- case CustomUiFrameMessageType.FrameError:
86
- return {
87
- kind: 'error',
88
- message:
89
- typeof envelope.message === 'string'
90
- ? envelope.message
91
- : 'The custom view failed to render.',
92
- };
93
- default:
94
- // Includes HostProps (host→frame only, never inbound) and any unknown
95
- // string — ignored.
96
- return IGNORED;
97
- }
98
- }