@assistant-ui/react 0.15.24 → 0.15.26
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.
- package/dist/hooks/useToolCallElapsed.d.ts +3 -1
- package/dist/hooks/useToolCallElapsed.d.ts.map +1 -1
- package/dist/hooks/useToolCallElapsed.js +8 -8
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +39 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +3 -2
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +56 -46
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/csp.d.ts +4 -0
- package/dist/mcp-apps/csp.d.ts.map +1 -0
- package/dist/mcp-apps/csp.js +93 -0
- package/dist/mcp-apps/csp.js.map +1 -0
- package/dist/mcp-apps/types.d.ts +1 -0
- package/dist/mcp-apps/types.d.ts.map +1 -1
- package/dist/mcp-apps/types.js.map +1 -1
- package/dist/model-context/makeAssistantVisible.d.ts +1 -0
- package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
- package/dist/model-context/makeAssistantVisible.js +31 -12
- package/dist/model-context/makeAssistantVisible.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts +1 -0
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/attachment.d.ts +3 -1
- package/dist/primitives/attachment.d.ts.map +1 -1
- package/dist/primitives/attachment.js.map +1 -1
- package/dist/primitives/composer/ComposerCancel.d.ts +5 -1
- package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerCancel.js +14 -1
- package/dist/primitives/composer/ComposerCancel.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts +15 -1
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +5 -4
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerRoot.js +24 -19
- package/dist/primitives/composer/ComposerRoot.js.map +1 -1
- package/dist/primitives/composer/composerInputSelector.d.ts +3 -0
- package/dist/primitives/composer/composerInputSelector.d.ts.map +1 -0
- package/dist/primitives/composer/composerInputSelector.js +7 -0
- package/dist/primitives/composer/composerInputSelector.js.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts +11 -11
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.js +62 -60
- package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts +8 -8
- package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +6 -6
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +3 -3
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts +9 -9
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +7 -7
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +5 -5
- package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +8 -8
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +8 -8
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
- package/dist/primitives/composer/trigger/index.d.ts +1 -1
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts +2 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts.map +1 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js +42 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js.map +1 -0
- package/dist/primitives/composer.d.ts +55 -15
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/composer.js +8 -1
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts +8 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js +58 -278
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -2
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadLoadEarlier.d.ts +19 -0
- package/dist/primitives/thread/ThreadLoadEarlier.d.ts.map +1 -0
- package/dist/primitives/thread/ThreadLoadEarlier.js +36 -0
- package/dist/primitives/thread/ThreadLoadEarlier.js.map +1 -0
- package/dist/primitives/thread/ThreadMessages.d.ts +1 -1
- package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadMessages.js +2 -2
- package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +4 -3
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +2 -2
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +195 -17
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/primitives/thread.d.ts +2 -0
- package/dist/primitives/thread.d.ts.map +1 -1
- package/dist/primitives/thread.js +5 -2
- package/dist/primitives/thread.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +9 -3
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useComposerInput.d.ts +7 -4
- package/dist/unstable/useComposerInput.d.ts.map +1 -1
- package/dist/unstable/useComposerInput.js +4 -4
- package/dist/unstable/useComposerInput.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.d.ts +3 -2
- package/dist/unstable/useComposerInputHistory.d.ts.map +1 -1
- package/dist/unstable/useComposerInputHistory.js +2 -2
- package/dist/unstable/useComposerInputHistory.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.d.ts +3 -2
- package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +8 -7
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +12 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +2 -2
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts +4 -2
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +12 -8
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.d.ts +5 -2
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +5 -4
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +4 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.js +2 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
- package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
- package/dist/utils/getSelectionMessageId.js +31 -8
- package/dist/utils/getSelectionMessageId.js.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +2 -22
- package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
- package/dist/utils/smooth/useSmooth.js +6 -6
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/dist/utils/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/utils/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/utils/useIsomorphicLayoutEffect.js +6 -0
- package/dist/utils/useIsomorphicLayoutEffect.js.map +1 -0
- package/package.json +12 -11
- package/src/hooks/useToolCallElapsed.test.tsx +37 -0
- package/src/hooks/useToolCallElapsed.ts +10 -7
- package/src/index.ts +47 -5
- package/src/mcp-apps/McpAppRenderer.tsx +3 -9
- package/src/mcp-apps/app-frame.test.tsx +38 -0
- package/src/mcp-apps/app-frame.tsx +5 -8
- package/src/mcp-apps/csp.test.ts +112 -0
- package/src/mcp-apps/csp.ts +124 -0
- package/src/mcp-apps/types.ts +1 -0
- package/src/model-context/frame/AssistantFrame.test.ts +45 -0
- package/src/model-context/frame/SPEC_AssistantFrame.md +1 -0
- package/src/model-context/makeAssistantVisible.test.tsx +315 -5
- package/src/model-context/makeAssistantVisible.tsx +43 -7
- package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -0
- package/src/primitives/attachment.ts +4 -1
- package/src/primitives/composer/ComposerCancel.focus.test.tsx +307 -0
- package/src/primitives/composer/ComposerCancel.ts +8 -1
- package/src/primitives/composer/ComposerInput.tsx +20 -2
- package/src/primitives/composer/ComposerRoot.tsx +6 -5
- package/src/primitives/composer/composerInputSelector.ts +4 -0
- package/src/primitives/composer/trigger/TriggerPopover.tsx +11 -11
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +10 -13
- package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +2 -2
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +11 -14
- package/src/primitives/composer/trigger/TriggerPopoverExports.test.ts +61 -0
- package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +3 -3
- package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +8 -8
- package/src/primitives/composer/useComposerCancelWithFocus.ts +57 -0
- package/src/primitives/composer.ts +70 -15
- package/src/primitives/message/MessagePartsGrouped.test.tsx +106 -2
- package/src/primitives/message/MessagePartsGrouped.tsx +36 -134
- package/src/primitives/message/MessageRoot.test.tsx +125 -2
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +20 -12
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +6 -2
- package/src/primitives/thread/ThreadLoadEarlier.tsx +35 -0
- package/src/primitives/thread/ThreadMessages.tsx +1 -0
- package/src/primitives/thread/ThreadViewport.tsx +3 -3
- package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +2 -2
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +922 -68
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +330 -56
- package/src/primitives/thread.ts +2 -0
- package/src/sandbox-host/SandboxHost.test.tsx +34 -0
- package/src/sandbox-host/SandboxHost.tsx +11 -12
- package/src/tests/toolCallTiming.test.tsx +19 -2
- package/src/unstable/useComposerInput.ts +7 -4
- package/src/unstable/useComposerInputHistory.ts +3 -2
- package/src/unstable/useLiveCompletionAdapter.ts +9 -14
- package/src/unstable/useMentionAdapter.ts +12 -3
- package/src/unstable/useMessageStallDetection.test.tsx +40 -0
- package/src/unstable/useMessageStallDetection.ts +14 -8
- package/src/unstable/useSlashCommandAdapter.ts +8 -4
- package/src/unstable/webmcp/useWebMcpProvider.ts +4 -0
- package/src/utils/getSelectionMessageId.test.ts +231 -0
- package/src/utils/getSelectionMessageId.ts +45 -7
- package/src/utils/hooks/useOnResizeContent.ts +2 -29
- package/src/utils/smooth/useSmooth.test.tsx +16 -5
- package/src/utils/smooth/useSmooth.ts +7 -7
- package/src/utils/useIsomorphicLayoutEffect.test.tsx +21 -0
- package/src/utils/useIsomorphicLayoutEffect.ts +9 -0
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useLayoutEffect,
|
|
6
|
-
useMemo,
|
|
7
|
-
useRef,
|
|
8
|
-
useState,
|
|
9
|
-
type ReactNode,
|
|
10
|
-
} from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect";
|
|
11
5
|
import type {
|
|
12
6
|
McpAppMetadata,
|
|
13
7
|
TextMessagePart,
|
|
@@ -204,7 +198,7 @@ function InlineRenderer({
|
|
|
204
198
|
const opts = resolvePartOptions(rendererOptions, part);
|
|
205
199
|
const app = getMcpAppFromToolPart(part);
|
|
206
200
|
const cachedAppRef = useRef<McpAppMetadata | undefined>(undefined);
|
|
207
|
-
|
|
201
|
+
useIsomorphicLayoutEffect(() => {
|
|
208
202
|
if (app != null) cachedAppRef.current = app;
|
|
209
203
|
}, [app]);
|
|
210
204
|
const appForRender = app ?? cachedAppRef.current;
|
|
@@ -31,6 +31,44 @@ describe("McpAppFrame", () => {
|
|
|
31
31
|
cleanup();
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
it("enforces the resource CSP before rendering widget HTML", () => {
|
|
35
|
+
let sandboxProps: SandboxHostProps | undefined;
|
|
36
|
+
sandboxHostMock.mockImplementation((props: SandboxHostProps) => {
|
|
37
|
+
sandboxProps = props;
|
|
38
|
+
return null;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
render(
|
|
42
|
+
<McpAppFrame
|
|
43
|
+
app={{ resourceUri: "ui://example/widget" }}
|
|
44
|
+
resource={{
|
|
45
|
+
uri: "ui://example/widget",
|
|
46
|
+
mimeType: MCP_APP_MIME_TYPE,
|
|
47
|
+
html: '<!doctype html><script src="https://cdn.example/app.js"></script>',
|
|
48
|
+
meta: {
|
|
49
|
+
csp: {
|
|
50
|
+
connectDomains: ["https://api.example"],
|
|
51
|
+
resourceDomains: ["https://cdn.example"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(sandboxProps?.content.html).toContain(
|
|
59
|
+
'http-equiv="Content-Security-Policy"',
|
|
60
|
+
);
|
|
61
|
+
expect(sandboxProps?.content.html).toContain(
|
|
62
|
+
"connect-src 'self' https://api.example",
|
|
63
|
+
);
|
|
64
|
+
expect(sandboxProps?.content.html).toContain(
|
|
65
|
+
"script-src 'self' 'unsafe-inline' https://cdn.example",
|
|
66
|
+
);
|
|
67
|
+
expect(
|
|
68
|
+
sandboxProps?.content.html.indexOf("Content-Security-Policy"),
|
|
69
|
+
).toBeLessThan(sandboxProps?.content.html.indexOf("<script") ?? 0);
|
|
70
|
+
});
|
|
71
|
+
|
|
34
72
|
it("keeps bridge options scoped to committed renders", async () => {
|
|
35
73
|
let committedCreateBridge: SandboxHostProps["createBridge"] | null = null;
|
|
36
74
|
sandboxHostMock.mockImplementation((props: SandboxHostProps) => {
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useEffect,
|
|
6
|
-
useLayoutEffect,
|
|
7
|
-
useRef,
|
|
8
|
-
} from "react";
|
|
3
|
+
import { type MutableRefObject, useEffect, useRef } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect";
|
|
9
5
|
import { type McpAppBridge, createMcpAppBridge } from "./bridge";
|
|
10
6
|
import {
|
|
11
7
|
SandboxHost,
|
|
@@ -19,6 +15,7 @@ import type {
|
|
|
19
15
|
McpAppHostContext,
|
|
20
16
|
} from "./types";
|
|
21
17
|
import { isRecord } from "@assistant-ui/core/internal";
|
|
18
|
+
import { applyMcpAppCsp } from "./csp";
|
|
22
19
|
|
|
23
20
|
const DEFAULT_PRODUCT = "assistant-ui-mcp-app";
|
|
24
21
|
const INIT_TIMEOUT_MS = 5000;
|
|
@@ -190,7 +187,7 @@ export function McpAppFrame({
|
|
|
190
187
|
input,
|
|
191
188
|
output,
|
|
192
189
|
});
|
|
193
|
-
|
|
190
|
+
useIsomorphicLayoutEffect(() => {
|
|
194
191
|
liveRef.current = {
|
|
195
192
|
handlers,
|
|
196
193
|
hostInfo,
|
|
@@ -313,7 +310,7 @@ export function McpAppFrame({
|
|
|
313
310
|
|
|
314
311
|
return (
|
|
315
312
|
<SandboxHost
|
|
316
|
-
content={{ html: resource.html }}
|
|
313
|
+
content={{ html: applyMcpAppCsp(resource.html, resource.meta?.csp) }}
|
|
317
314
|
contentKey={
|
|
318
315
|
app.serverId ? `${app.serverId} ${resource.uri}` : resource.uri
|
|
319
316
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { applyMcpAppCsp, buildMcpAppCsp } from "./csp";
|
|
5
|
+
|
|
6
|
+
const getPolicyMeta = (html: string) =>
|
|
7
|
+
new DOMParser()
|
|
8
|
+
.parseFromString(applyMcpAppCsp(html), "text/html")
|
|
9
|
+
.head.querySelector('meta[http-equiv="Content-Security-Policy"]');
|
|
10
|
+
|
|
11
|
+
const expectPolicyBeforeScript = (html: string) => {
|
|
12
|
+
const policyIndex = html.indexOf("Content-Security-Policy");
|
|
13
|
+
const scriptIndex = html.indexOf("<script");
|
|
14
|
+
expect(policyIndex).toBeGreaterThanOrEqual(0);
|
|
15
|
+
expect(scriptIndex).toBeGreaterThanOrEqual(0);
|
|
16
|
+
expect(policyIndex).toBeLessThan(scriptIndex);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("MCP App CSP", () => {
|
|
20
|
+
it("uses restrictive defaults when the resource omits CSP metadata", () => {
|
|
21
|
+
expect(buildMcpAppCsp()).toBe(
|
|
22
|
+
"default-src 'none'; " +
|
|
23
|
+
"script-src 'self' 'unsafe-inline'; " +
|
|
24
|
+
"style-src 'self' 'unsafe-inline'; " +
|
|
25
|
+
"connect-src 'none'; " +
|
|
26
|
+
"img-src 'self' data:; " +
|
|
27
|
+
"font-src 'self'; " +
|
|
28
|
+
"media-src 'self' data:; " +
|
|
29
|
+
"frame-src 'none'; " +
|
|
30
|
+
"base-uri 'self'; " +
|
|
31
|
+
"object-src 'none'; " +
|
|
32
|
+
"form-action 'none'",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("allows only valid declared origins", () => {
|
|
37
|
+
const policy = buildMcpAppCsp({
|
|
38
|
+
connectDomains: [
|
|
39
|
+
"https://api.example",
|
|
40
|
+
"wss://events.example:8443",
|
|
41
|
+
"https://api.example; default-src *",
|
|
42
|
+
],
|
|
43
|
+
resourceDomains: ["https://*.cdn.example", "wss://assets.example"],
|
|
44
|
+
frameDomains: ["https://video.example/path"],
|
|
45
|
+
baseUriDomains: ["https://base.example"],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(policy.split("; ")).toContain(
|
|
49
|
+
"connect-src 'self' https://api.example wss://events.example:8443",
|
|
50
|
+
);
|
|
51
|
+
expect(
|
|
52
|
+
buildMcpAppCsp({
|
|
53
|
+
resourceDomains: ["https://*.cdn.example", "wss://assets.example"],
|
|
54
|
+
}),
|
|
55
|
+
).toContain("script-src 'self' 'unsafe-inline' https://*.cdn.example");
|
|
56
|
+
expect(
|
|
57
|
+
buildMcpAppCsp({ resourceDomains: ["wss://assets.example"] }),
|
|
58
|
+
).toContain("script-src 'self' 'unsafe-inline';");
|
|
59
|
+
expect(
|
|
60
|
+
buildMcpAppCsp({ frameDomains: ["https://video.example/path"] }),
|
|
61
|
+
).toContain("frame-src 'none'");
|
|
62
|
+
expect(
|
|
63
|
+
buildMcpAppCsp({ baseUriDomains: ["https://base.example"] }),
|
|
64
|
+
).toContain("base-uri https://base.example");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("falls back to the restrictive policy for malformed metadata", () => {
|
|
68
|
+
expect(
|
|
69
|
+
buildMcpAppCsp({
|
|
70
|
+
connectDomains: "https://api.example" as unknown as string[],
|
|
71
|
+
}),
|
|
72
|
+
).toContain("connect-src 'none'");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("places the policy after a doctype and before executable content", () => {
|
|
76
|
+
const html =
|
|
77
|
+
'\uFEFF<!-- license --><!doctype html><script src="https://cdn.example/app.js"></script>';
|
|
78
|
+
const secured = applyMcpAppCsp(html);
|
|
79
|
+
|
|
80
|
+
expect(secured.startsWith("<!-- license --><!doctype html><meta ")).toBe(
|
|
81
|
+
true,
|
|
82
|
+
);
|
|
83
|
+
expectPolicyBeforeScript(secured);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it.each([
|
|
87
|
+
"<!--><script>run()</script>-->",
|
|
88
|
+
"<!---><script>run()</script>",
|
|
89
|
+
"<!-- license --!><script>run()</script>",
|
|
90
|
+
])("places the policy before scripts after HTML comment closers", (html) => {
|
|
91
|
+
expectPolicyBeforeScript(applyMcpAppCsp(html));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it.each([
|
|
95
|
+
'<?xml version="1.0"?><!doctype html><script>run()</script>',
|
|
96
|
+
"<!foo><!doctype html><script>run()</script>",
|
|
97
|
+
])("preserves standards mode after leading bogus comments", (html) => {
|
|
98
|
+
const secured = applyMcpAppCsp(html);
|
|
99
|
+
const document = new DOMParser().parseFromString(secured, "text/html");
|
|
100
|
+
|
|
101
|
+
expect(secured.indexOf("<!doctype html><meta ")).toBeGreaterThan(0);
|
|
102
|
+
expect(document.compatMode).toBe("CSS1Compat");
|
|
103
|
+
expectPolicyBeforeScript(secured);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it.each([
|
|
107
|
+
"\u00a0<script>run()</script>",
|
|
108
|
+
"\uFEFF\uFEFF<script>run()</script>",
|
|
109
|
+
])("keeps the policy in head before non-HTML whitespace", (html) => {
|
|
110
|
+
expect(getPolicyMeta(html)).not.toBeNull();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { McpAppResourceCSP } from "./types";
|
|
2
|
+
|
|
3
|
+
const isAllowedOrigin = (value: string, allowWebSocket: boolean): boolean => {
|
|
4
|
+
const scheme = allowWebSocket ? /^(?:https?|wss?):\/\//i : /^https?:\/\//i;
|
|
5
|
+
if (!scheme.test(value) || /[\s;'"`,]/.test(value)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(value.replace("://*.", "://wildcard."));
|
|
11
|
+
return (
|
|
12
|
+
url.username === "" &&
|
|
13
|
+
url.password === "" &&
|
|
14
|
+
url.pathname === "/" &&
|
|
15
|
+
url.search === "" &&
|
|
16
|
+
url.hash === ""
|
|
17
|
+
);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getAllowedOrigins = (
|
|
24
|
+
values: unknown,
|
|
25
|
+
allowWebSocket = false,
|
|
26
|
+
): string[] =>
|
|
27
|
+
Array.isArray(values)
|
|
28
|
+
? [
|
|
29
|
+
...new Set(
|
|
30
|
+
values.filter(
|
|
31
|
+
(value): value is string =>
|
|
32
|
+
typeof value === "string" &&
|
|
33
|
+
isAllowedOrigin(value, allowWebSocket),
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
]
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
const directive = (name: string, values: readonly string[]) =>
|
|
40
|
+
`${name} ${values.join(" ")}`;
|
|
41
|
+
|
|
42
|
+
export const buildMcpAppCsp = (csp?: McpAppResourceCSP): string => {
|
|
43
|
+
const connectDomains = getAllowedOrigins(csp?.connectDomains, true);
|
|
44
|
+
const resourceDomains = getAllowedOrigins(csp?.resourceDomains);
|
|
45
|
+
const frameDomains = getAllowedOrigins(csp?.frameDomains);
|
|
46
|
+
const baseUriDomains = getAllowedOrigins(csp?.baseUriDomains);
|
|
47
|
+
|
|
48
|
+
return [
|
|
49
|
+
"default-src 'none'",
|
|
50
|
+
directive("script-src", ["'self'", "'unsafe-inline'", ...resourceDomains]),
|
|
51
|
+
directive("style-src", ["'self'", "'unsafe-inline'", ...resourceDomains]),
|
|
52
|
+
directive(
|
|
53
|
+
"connect-src",
|
|
54
|
+
connectDomains.length > 0 ? ["'self'", ...connectDomains] : ["'none'"],
|
|
55
|
+
),
|
|
56
|
+
directive("img-src", ["'self'", "data:", ...resourceDomains]),
|
|
57
|
+
directive("font-src", ["'self'", ...resourceDomains]),
|
|
58
|
+
directive("media-src", ["'self'", "data:", ...resourceDomains]),
|
|
59
|
+
directive("frame-src", frameDomains.length > 0 ? frameDomains : ["'none'"]),
|
|
60
|
+
directive(
|
|
61
|
+
"base-uri",
|
|
62
|
+
baseUriDomains.length > 0 ? baseUriDomains : ["'self'"],
|
|
63
|
+
),
|
|
64
|
+
"object-src 'none'",
|
|
65
|
+
"form-action 'none'",
|
|
66
|
+
].join("; ");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const findDoctypeEnd = (html: string): number => {
|
|
70
|
+
let offset = 0;
|
|
71
|
+
|
|
72
|
+
while (offset < html.length) {
|
|
73
|
+
const whitespace = html.slice(offset).match(/^[\t\n\f\r ]+/)?.[0];
|
|
74
|
+
if (whitespace) {
|
|
75
|
+
offset += whitespace.length;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (html.startsWith("<!--", offset)) {
|
|
79
|
+
if (html.startsWith("<!-->", offset)) {
|
|
80
|
+
offset += 5;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (html.startsWith("<!--->", offset)) {
|
|
84
|
+
offset += 6;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const commentEnd = html.indexOf("-->", offset + 4);
|
|
88
|
+
const bangCommentEnd = html.indexOf("--!>", offset + 4);
|
|
89
|
+
if (commentEnd === -1 && bangCommentEnd === -1) return 0;
|
|
90
|
+
offset =
|
|
91
|
+
bangCommentEnd !== -1 &&
|
|
92
|
+
(commentEnd === -1 || bangCommentEnd < commentEnd)
|
|
93
|
+
? bangCommentEnd + 4
|
|
94
|
+
: commentEnd + 3;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (html.slice(offset, offset + 9).toLowerCase() === "<!doctype") {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (html.startsWith("<?", offset) || html.startsWith("<!", offset)) {
|
|
101
|
+
const bogusCommentEnd = html.indexOf(">", offset + 2);
|
|
102
|
+
if (bogusCommentEnd === -1) return 0;
|
|
103
|
+
offset = bogusCommentEnd + 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (html.slice(offset, offset + 9).toLowerCase() !== "<!doctype") {
|
|
110
|
+
return offset;
|
|
111
|
+
}
|
|
112
|
+
const end = html.indexOf(">", offset + 9);
|
|
113
|
+
return end === -1 ? 0 : end + 1;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const applyMcpAppCsp = (
|
|
117
|
+
html: string,
|
|
118
|
+
csp?: McpAppResourceCSP,
|
|
119
|
+
): string => {
|
|
120
|
+
const normalizedHtml = html.charCodeAt(0) === 0xfeff ? html.slice(1) : html;
|
|
121
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${buildMcpAppCsp(csp)}">`;
|
|
122
|
+
const insertionPoint = findDoctypeEnd(normalizedHtml);
|
|
123
|
+
return `${normalizedHtml.slice(0, insertionPoint)}${meta}${normalizedHtml.slice(insertionPoint)}`;
|
|
124
|
+
};
|
package/src/mcp-apps/types.ts
CHANGED
|
@@ -237,6 +237,51 @@ describe("AssistantFrame Integration", () => {
|
|
|
237
237
|
unsubscribe();
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
it("rejects a tool call the frame drops while it is disposed", async () => {
|
|
241
|
+
vi.mocked(iframeWindow.postMessage).mockImplementation((data: any) => {
|
|
242
|
+
Promise.resolve().then(() => {
|
|
243
|
+
messageHandlers.get("iframe")?.({
|
|
244
|
+
data,
|
|
245
|
+
source: parentWindow,
|
|
246
|
+
origin: window.location.origin,
|
|
247
|
+
} as MessageEvent);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
vi.mocked(window.removeEventListener).mockImplementation(
|
|
251
|
+
(event: string, handler: any) => {
|
|
252
|
+
if (event === "message" && messageHandlers.get("iframe") === handler) {
|
|
253
|
+
messageHandlers.delete("iframe");
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
const registry = new ModelContextRegistry();
|
|
258
|
+
const toolExecute = vi.fn().mockResolvedValue("result");
|
|
259
|
+
registry.addTool({
|
|
260
|
+
toolName: "search",
|
|
261
|
+
parameters: z.object({}),
|
|
262
|
+
execute: toolExecute,
|
|
263
|
+
});
|
|
264
|
+
AssistantFrameProvider.addModelContextProvider(registry);
|
|
265
|
+
const host = new AssistantFrameHost(iframeWindow);
|
|
266
|
+
await vi.waitFor(() => {
|
|
267
|
+
expect(host.getModelContext().tools?.search).toBeDefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const result = host.getModelContext().tools!.search!.execute!(
|
|
271
|
+
{},
|
|
272
|
+
{} as any,
|
|
273
|
+
);
|
|
274
|
+
AssistantFrameProvider.dispose();
|
|
275
|
+
|
|
276
|
+
await expect(result).rejects.toThrow(
|
|
277
|
+
"AssistantFrameProvider has been disposed",
|
|
278
|
+
);
|
|
279
|
+
expect(toolExecute).not.toHaveBeenCalled();
|
|
280
|
+
expect(host.getModelContext().tools).toBeUndefined();
|
|
281
|
+
|
|
282
|
+
host.dispose();
|
|
283
|
+
});
|
|
284
|
+
|
|
240
285
|
it("should handle multiple providers", async () => {
|
|
241
286
|
// Setup multiple providers
|
|
242
287
|
const registry1 = new ModelContextRegistry();
|
|
@@ -78,6 +78,7 @@ All messages are wrapped with a channel identifier to avoid conflicts with other
|
|
|
78
78
|
2. **Tool Execution**
|
|
79
79
|
- `tool-call`: Parent requests tool execution in iframe (where tools are defined)
|
|
80
80
|
- `tool-result`: Iframe returns execution result or error to parent
|
|
81
|
+
- `provider-disposed`: Iframe tells the parent it was disposed, so the parent rejects tool calls the iframe will not answer
|
|
81
82
|
|
|
82
83
|
#### Serialization
|
|
83
84
|
|