@botiverse/porthole 0.0.1 → 0.1.1
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/LICENSE +217 -0
- package/dist/bridge/bridge.d.ts +39 -0
- package/dist/bridge/bridge.js +198 -0
- package/dist/bridge/types.d.ts +83 -0
- package/dist/bridge/types.js +1 -0
- package/dist/compile/compile.d.ts +35 -0
- package/dist/compile/compile.js +30 -0
- package/dist/compile/policy.d.ts +55 -0
- package/dist/compile/policy.js +103 -0
- package/dist/config/capabilities.d.ts +154 -0
- package/dist/config/capabilities.js +63 -0
- package/dist/config/schema.d.ts +152 -0
- package/dist/config/schema.js +11 -0
- package/dist/config/validate.d.ts +22 -0
- package/dist/config/validate.js +261 -0
- package/dist/conformance/behavior.d.ts +2 -0
- package/dist/conformance/behavior.js +426 -0
- package/dist/conformance/index.d.ts +101 -0
- package/dist/conformance/index.js +4 -0
- package/dist/conformance/kit.d.ts +42 -0
- package/dist/conformance/kit.js +24 -0
- package/dist/conformance/oracles.d.ts +35 -0
- package/dist/conformance/oracles.js +77 -0
- package/dist/conformance/probes.d.ts +33 -0
- package/dist/conformance/probes.js +876 -0
- package/dist/conformance/served.d.ts +75 -0
- package/dist/conformance/served.js +335 -0
- package/dist/guide/examples.d.ts +70 -0
- package/dist/guide/examples.js +69 -0
- package/dist/guide/guide.d.ts +16 -0
- package/dist/guide/guide.js +265 -0
- package/dist/host/create-host.d.ts +8 -0
- package/dist/host/create-host.js +292 -0
- package/dist/host/locate.d.ts +8 -0
- package/dist/host/locate.js +16 -0
- package/dist/host/outcome.d.ts +24 -0
- package/dist/host/outcome.js +134 -0
- package/dist/host/types.d.ts +127 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +21 -0
- package/dist/isolation/served.d.ts +35 -0
- package/dist/isolation/served.js +117 -0
- package/dist/isolation/types.d.ts +96 -0
- package/dist/isolation/types.js +2 -0
- package/dist/mcp/adapter.d.ts +43 -0
- package/dist/mcp/adapter.js +327 -0
- package/dist/mcp/index.d.ts +27 -0
- package/dist/mcp/index.js +25 -0
- package/dist/mcp/proxy.d.ts +25 -0
- package/dist/mcp/proxy.js +106 -0
- package/dist/mcp/view.d.ts +31 -0
- package/dist/mcp/view.js +72 -0
- package/dist/platform/types.d.ts +51 -0
- package/dist/platform/types.js +1 -0
- package/dist/platform/web.d.ts +8 -0
- package/dist/platform/web.js +85 -0
- package/dist/presets/presets.d.ts +71 -0
- package/dist/presets/presets.js +140 -0
- package/dist/protocol/adapter.d.ts +73 -0
- package/dist/protocol/adapter.js +1 -0
- package/dist/protocol/jsonrpc.d.ts +43 -0
- package/dist/protocol/jsonrpc.js +50 -0
- package/dist/protocol/mcp-apps.d.ts +79 -0
- package/dist/protocol/mcp-apps.js +99 -0
- package/dist/protocol/shell-adapter.d.ts +5 -0
- package/dist/protocol/shell-adapter.js +228 -0
- package/dist/react/dialog.d.ts +20 -0
- package/dist/react/dialog.js +98 -0
- package/dist/react/frame.d.ts +91 -0
- package/dist/react/frame.js +255 -0
- package/dist/react/index.d.ts +13 -0
- package/dist/react/index.js +13 -0
- package/dist/react/live.d.ts +24 -0
- package/dist/react/live.js +57 -0
- package/dist/server/index.d.ts +27 -0
- package/dist/server/index.js +26 -0
- package/dist/server/keys.d.ts +19 -0
- package/dist/server/keys.js +78 -0
- package/dist/server/pages.d.ts +11 -0
- package/dist/server/pages.js +86 -0
- package/dist/server/proxy.d.ts +21 -0
- package/dist/server/proxy.js +107 -0
- package/dist/server/server.d.ts +7 -0
- package/dist/server/server.js +542 -0
- package/dist/server/tickets.d.ts +8 -0
- package/dist/server/tickets.js +28 -0
- package/dist/server/types.d.ts +97 -0
- package/dist/server/types.js +1 -0
- package/dist/shell/document.d.ts +29 -0
- package/dist/shell/document.js +85 -0
- package/dist/shell/plan.d.ts +37 -0
- package/dist/shell/plan.js +35 -0
- package/dist/shell/runtime.d.ts +28 -0
- package/dist/shell/runtime.js +848 -0
- package/dist/theme/variables.d.ts +73 -0
- package/dist/theme/variables.js +171 -0
- package/dist/transport/post-message.d.ts +9 -0
- package/dist/transport/post-message.js +38 -0
- package/dist/transport/types.d.ts +15 -0
- package/dist/transport/types.js +1 -0
- package/package.json +63 -1
- package/src/bridge/bridge.ts +278 -0
- package/src/bridge/types.ts +95 -0
- package/src/compile/compile.ts +68 -0
- package/src/compile/policy.ts +130 -0
- package/src/config/capabilities.ts +138 -0
- package/src/config/schema.ts +168 -0
- package/src/config/validate.ts +318 -0
- package/src/conformance/behavior.ts +485 -0
- package/src/conformance/index.ts +116 -0
- package/src/conformance/kit.ts +63 -0
- package/src/conformance/oracles.ts +126 -0
- package/src/conformance/probes.ts +1129 -0
- package/src/conformance/served.ts +501 -0
- package/src/guide/examples.ts +74 -0
- package/src/guide/guide.ts +430 -0
- package/src/host/create-host.ts +335 -0
- package/src/host/locate.ts +25 -0
- package/src/host/outcome.ts +159 -0
- package/src/host/types.ts +126 -0
- package/src/index.ts +128 -0
- package/src/isolation/served.ts +151 -0
- package/src/isolation/types.ts +91 -0
- package/src/mcp/adapter.ts +441 -0
- package/src/mcp/index.ts +50 -0
- package/src/mcp/proxy.ts +117 -0
- package/src/mcp/view.ts +97 -0
- package/src/platform/types.ts +55 -0
- package/src/platform/web.ts +91 -0
- package/src/presets/presets.ts +217 -0
- package/src/protocol/adapter.ts +67 -0
- package/src/protocol/jsonrpc.ts +83 -0
- package/src/protocol/mcp-apps.ts +138 -0
- package/src/protocol/shell-adapter.ts +261 -0
- package/src/react/dialog.tsx +158 -0
- package/src/react/frame.tsx +486 -0
- package/src/react/index.ts +20 -0
- package/src/react/live.ts +76 -0
- package/src/server/index.ts +37 -0
- package/src/server/keys.ts +100 -0
- package/src/server/pages.ts +117 -0
- package/src/server/proxy.ts +131 -0
- package/src/server/server.ts +657 -0
- package/src/server/tickets.ts +30 -0
- package/src/server/types.ts +104 -0
- package/src/shell/document.ts +121 -0
- package/src/shell/plan.ts +69 -0
- package/src/shell/runtime.ts +848 -0
- package/src/theme/variables.ts +190 -0
- package/src/transport/post-message.ts +45 -0
- package/src/transport/types.ts +16 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { egressOrigins } from "../compile/policy.js";
|
|
2
|
+
import { JSON_RPC_ERRORS, parseJsonRpc, } from "../protocol/jsonrpc.js";
|
|
3
|
+
import { MCP_APPS_METHODS, MCP_APPS_PROTOCOL_VERSION, } from "../protocol/mcp-apps.js";
|
|
4
|
+
import { MCP_APPS_STYLE_VARIABLES } from "../protocol/mcp-apps.js";
|
|
5
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
/** Levels of `notifications/message` that are errors: they go to the host as `reportError`. */
|
|
7
|
+
const ERROR_LEVELS = new Set(["error", "critical", "alert", "emergency"]);
|
|
8
|
+
const DISPLAY_MODES = new Set(["inline", "fullscreen", "pip"]);
|
|
9
|
+
const STYLE_VARIABLES = new Set(MCP_APPS_STYLE_VARIABLES);
|
|
10
|
+
/**
|
|
11
|
+
* The host context as a view may receive it: Porthole's fields as they are, its theme variables
|
|
12
|
+
* cut to the names the standard defines (the official SDK refuses any other).
|
|
13
|
+
*/
|
|
14
|
+
export function viewHostContext(context, toolInfo) {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [key, value] of Object.entries(context)) {
|
|
17
|
+
if (value !== undefined && key !== "styles") {
|
|
18
|
+
out[key] = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const variables = {};
|
|
22
|
+
for (const [name, value] of Object.entries(context.styles?.variables ?? {})) {
|
|
23
|
+
if (STYLE_VARIABLES.has(name) && typeof value === "string") {
|
|
24
|
+
variables[name] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
out.styles = { variables };
|
|
28
|
+
if (toolInfo !== undefined) {
|
|
29
|
+
out.toolInfo = toolInfo;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/** The text of a `ui/message`'s content: its text blocks, in order (the spec's examples send one). */
|
|
34
|
+
function messageText(content) {
|
|
35
|
+
const blocks = Array.isArray(content) ? content : [content];
|
|
36
|
+
const texts = blocks.flatMap((block) => isRecord(block) && block.type === "text" && typeof block.text === "string" ? [block.text] : []);
|
|
37
|
+
return texts.length === 0 ? null : texts.join("\n\n");
|
|
38
|
+
}
|
|
39
|
+
/** Whether the host page has a live user gesture (a gesture in a frame activates its ancestors). */
|
|
40
|
+
function hostActivation() {
|
|
41
|
+
const activation = globalThis
|
|
42
|
+
.navigator?.userActivation;
|
|
43
|
+
return activation === undefined ? undefined : activation.isActive;
|
|
44
|
+
}
|
|
45
|
+
function connect(session, options) {
|
|
46
|
+
const { transport, bridge } = session;
|
|
47
|
+
let closed = false;
|
|
48
|
+
let initialized = false;
|
|
49
|
+
let inputSent = false;
|
|
50
|
+
let resourceSent = false;
|
|
51
|
+
/** What the host said before the view was initialized: sent, in order, once it is. */
|
|
52
|
+
let queue = [];
|
|
53
|
+
const pending = new Map();
|
|
54
|
+
let nextId = 1;
|
|
55
|
+
const send = (message) => {
|
|
56
|
+
if (!closed) {
|
|
57
|
+
transport.send(message);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const notify = (method, params) => {
|
|
61
|
+
const message = { jsonrpc: "2.0", method, params };
|
|
62
|
+
if (initialized) {
|
|
63
|
+
send(message);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
queue.push(message);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const answer = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
70
|
+
const refuse = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
71
|
+
const hostCapabilities = () => {
|
|
72
|
+
const declared = (name) => bridge.declared(name);
|
|
73
|
+
const { config } = session.compiled;
|
|
74
|
+
const connectDomains = egressOrigins(config, "connect");
|
|
75
|
+
const resourceDomains = [
|
|
76
|
+
...new Set(["script", "style", "img", "font", "media"].flatMap((kind) => egressOrigins(config, kind))),
|
|
77
|
+
];
|
|
78
|
+
return {
|
|
79
|
+
logging: {},
|
|
80
|
+
...(declared("openLink") ? { openLinks: {} } : {}),
|
|
81
|
+
...(declared("downloadFile") ? { downloadFile: {} } : {}),
|
|
82
|
+
...(declared("callTool") ? { serverTools: {} } : {}),
|
|
83
|
+
...(declared("readData") ? { serverResources: {} } : {}),
|
|
84
|
+
...(declared("sendPrompt") ? { message: { text: {} } } : {}),
|
|
85
|
+
...(declared("setModelContext")
|
|
86
|
+
? { updateModelContext: { text: {}, structuredContent: {} } }
|
|
87
|
+
: {}),
|
|
88
|
+
sandbox: {
|
|
89
|
+
csp: {
|
|
90
|
+
...(connectDomains.length > 0 ? { connectDomains } : {}),
|
|
91
|
+
...(resourceDomains.length > 0 ? { resourceDomains } : {}),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
/** Runs a capability through the bridge and answers the request with what it gives. */
|
|
97
|
+
async function call(request, capability, args, result) {
|
|
98
|
+
const userActivated = hostActivation();
|
|
99
|
+
let outcome;
|
|
100
|
+
try {
|
|
101
|
+
outcome = await bridge.call(capability, args, userActivated === undefined ? undefined : { userActivated });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
refuse(request.id, JSON_RPC_ERRORS.internal, `${request.method} failed`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (outcome.ok) {
|
|
108
|
+
answer(request.id, result(outcome.value));
|
|
109
|
+
}
|
|
110
|
+
else if (outcome.refusal === "not-declared") {
|
|
111
|
+
refuse(request.id, JSON_RPC_ERRORS.methodNotFound, `${request.method} isn't offered here`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// The spec's "implementation-defined" error for a refused request.
|
|
115
|
+
refuse(request.id, -32_000, `${request.method} refused (${outcome.refusal})`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** `{}` when a host function did it, `{ isError: true }` when it said it couldn't. */
|
|
119
|
+
const done = (value) => value === false ? { isError: true } : {};
|
|
120
|
+
async function onRequest(request) {
|
|
121
|
+
const params = request.params ?? {};
|
|
122
|
+
const invalid = (what) => refuse(request.id, JSON_RPC_ERRORS.invalidParams, `${request.method}: ${what}`);
|
|
123
|
+
switch (request.method) {
|
|
124
|
+
case MCP_APPS_METHODS.initialize:
|
|
125
|
+
answer(request.id, {
|
|
126
|
+
protocolVersion: MCP_APPS_PROTOCOL_VERSION,
|
|
127
|
+
hostInfo: session.hostInfo,
|
|
128
|
+
hostCapabilities: hostCapabilities(),
|
|
129
|
+
hostContext: viewHostContext(session.hostContext(), options.toolInfo?.(session.display.id)),
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
case "ping":
|
|
133
|
+
answer(request.id, {});
|
|
134
|
+
return;
|
|
135
|
+
case MCP_APPS_METHODS.message: {
|
|
136
|
+
const text = params.role === "user" ? messageText(params.content) : null;
|
|
137
|
+
if (text === null) {
|
|
138
|
+
invalid('needs role "user" and text content');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
await call(request, "sendPrompt", { text }, done);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
case MCP_APPS_METHODS.openLink:
|
|
145
|
+
if (typeof params.url !== "string") {
|
|
146
|
+
invalid("needs a url");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
await call(request, "openLink", { url: params.url }, done);
|
|
150
|
+
return;
|
|
151
|
+
case MCP_APPS_METHODS.downloadFile:
|
|
152
|
+
if (!Array.isArray(params.contents)) {
|
|
153
|
+
invalid("needs contents");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await call(request, "downloadFile", { contents: params.contents }, done);
|
|
157
|
+
return;
|
|
158
|
+
case MCP_APPS_METHODS.callTool:
|
|
159
|
+
if (typeof params.name !== "string" ||
|
|
160
|
+
(params.arguments !== undefined && !isRecord(params.arguments))) {
|
|
161
|
+
invalid("needs a tool name and object arguments");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
await call(request, "callTool", { name: params.name, arguments: params.arguments ?? {} }, (value) => (isRecord(value) ? value : { content: [] }));
|
|
165
|
+
return;
|
|
166
|
+
case MCP_APPS_METHODS.readResource:
|
|
167
|
+
if (typeof params.uri !== "string") {
|
|
168
|
+
invalid("needs a uri");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await call(request, "readData", { uri: params.uri }, (value) => isRecord(value) ? value : { contents: [] });
|
|
172
|
+
return;
|
|
173
|
+
case MCP_APPS_METHODS.updateModelContext:
|
|
174
|
+
if ((params.content !== undefined && !Array.isArray(params.content)) ||
|
|
175
|
+
(params.structuredContent !== undefined && !isRecord(params.structuredContent))) {
|
|
176
|
+
invalid("content is a list, structuredContent an object");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await call(request, "setModelContext", {
|
|
180
|
+
...(params.content === undefined ? {} : { content: params.content }),
|
|
181
|
+
...(params.structuredContent === undefined
|
|
182
|
+
? {}
|
|
183
|
+
: { structuredContent: params.structuredContent }),
|
|
184
|
+
}, () => ({}));
|
|
185
|
+
return;
|
|
186
|
+
case MCP_APPS_METHODS.requestDisplayMode: {
|
|
187
|
+
if (typeof params.mode !== "string" || !DISPLAY_MODES.has(params.mode)) {
|
|
188
|
+
invalid("needs a display mode");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Changing how a display is shown is the host's own layout: the view keeps its mode, and
|
|
192
|
+
// the answer says so (spec: return the current mode when the request isn't granted).
|
|
193
|
+
answer(request.id, { mode: session.hostContext().displayMode });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
default:
|
|
197
|
+
refuse(request.id, JSON_RPC_ERRORS.methodNotFound, `${request.method} isn't offered here`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function onNotification(method, params) {
|
|
201
|
+
switch (method) {
|
|
202
|
+
case MCP_APPS_METHODS.sandboxProxyReady:
|
|
203
|
+
// Behind the sandbox proxy: it takes the view's HTML once, then becomes the view (P026).
|
|
204
|
+
if (!resourceSent && session.document !== undefined) {
|
|
205
|
+
resourceSent = true;
|
|
206
|
+
send({
|
|
207
|
+
jsonrpc: "2.0",
|
|
208
|
+
method: MCP_APPS_METHODS.sandboxResourceReady,
|
|
209
|
+
params: { html: session.document },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
case MCP_APPS_METHODS.initialized:
|
|
214
|
+
if (!initialized) {
|
|
215
|
+
initialized = true;
|
|
216
|
+
const waiting = queue;
|
|
217
|
+
queue = [];
|
|
218
|
+
for (const message of waiting) {
|
|
219
|
+
send(message);
|
|
220
|
+
}
|
|
221
|
+
session.emit({ type: "initialized" });
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
case MCP_APPS_METHODS.sizeChanged: {
|
|
225
|
+
const { height, width } = params;
|
|
226
|
+
const size = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
227
|
+
if (size(height)) {
|
|
228
|
+
void bridge.call("resize", { height, ...(size(width) ? { width } : {}) });
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
case MCP_APPS_METHODS.log:
|
|
233
|
+
if (typeof params.level === "string" && ERROR_LEVELS.has(params.level)) {
|
|
234
|
+
const data = params.data;
|
|
235
|
+
void bridge.call("reportError", {
|
|
236
|
+
message: (typeof data === "string" ? data : (JSON.stringify(data) ?? "")).slice(0, 2_000),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
case MCP_APPS_METHODS.requestTeardown:
|
|
241
|
+
session.emit({ type: "teardown-requested" });
|
|
242
|
+
return;
|
|
243
|
+
default:
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const unlisten = transport.listen((raw) => {
|
|
247
|
+
const message = parseJsonRpc(raw);
|
|
248
|
+
if (message === null || closed) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!("method" in message)) {
|
|
252
|
+
const waiting = pending.get(message.id);
|
|
253
|
+
pending.delete(message.id);
|
|
254
|
+
waiting?.();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if ("id" in message) {
|
|
258
|
+
void onRequest(message);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
onNotification(message.method, message.params ?? {});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
hostContextChanged(context) {
|
|
266
|
+
if (initialized) {
|
|
267
|
+
send({
|
|
268
|
+
jsonrpc: "2.0",
|
|
269
|
+
method: MCP_APPS_METHODS.hostContextChanged,
|
|
270
|
+
params: viewHostContext(context),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
teardown() {
|
|
275
|
+
if (closed || !initialized) {
|
|
276
|
+
return Promise.resolve();
|
|
277
|
+
}
|
|
278
|
+
return new Promise((resolve) => {
|
|
279
|
+
const id = nextId++;
|
|
280
|
+
const finish = () => {
|
|
281
|
+
clearTimeout(timer);
|
|
282
|
+
pending.delete(id);
|
|
283
|
+
resolve();
|
|
284
|
+
};
|
|
285
|
+
const timer = setTimeout(finish, options.teardownTimeoutMs ?? 3_000);
|
|
286
|
+
pending.set(id, finish);
|
|
287
|
+
send({ jsonrpc: "2.0", id, method: MCP_APPS_METHODS.resourceTeardown, params: {} });
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
close() {
|
|
291
|
+
closed = true;
|
|
292
|
+
unlisten();
|
|
293
|
+
queue = [];
|
|
294
|
+
for (const waiting of pending.values()) {
|
|
295
|
+
waiting();
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
sendToolInputPartial(args) {
|
|
299
|
+
if (inputSent) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!initialized) {
|
|
303
|
+
// Only the latest draft matters to a view that hasn't seen any.
|
|
304
|
+
queue = queue.filter((message) => !("method" in message) || message.method !== MCP_APPS_METHODS.toolInputPartial);
|
|
305
|
+
}
|
|
306
|
+
notify(MCP_APPS_METHODS.toolInputPartial, { arguments: args });
|
|
307
|
+
},
|
|
308
|
+
sendToolInput(args) {
|
|
309
|
+
if (inputSent) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
inputSent = true;
|
|
313
|
+
queue = queue.filter((message) => !("method" in message) || message.method !== MCP_APPS_METHODS.toolInputPartial);
|
|
314
|
+
notify(MCP_APPS_METHODS.toolInput, { arguments: args });
|
|
315
|
+
},
|
|
316
|
+
sendToolResult(result) {
|
|
317
|
+
notify(MCP_APPS_METHODS.toolResult, result);
|
|
318
|
+
},
|
|
319
|
+
sendToolCancelled(reason) {
|
|
320
|
+
notify(MCP_APPS_METHODS.toolCancelled, reason === undefined ? {} : { reason });
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/** The MCP Apps adapter: pass it as `HostOptions.adapter` with the `mcpApp` preset. */
|
|
325
|
+
export function mcpAppsAdapter(options = {}) {
|
|
326
|
+
return { name: "mcp-apps", connect: (session) => connect(session, options) };
|
|
327
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@botiverse/porthole/mcp`: the MCP Apps adapter for full MCP Apps views (P007): the host-side
|
|
3
|
+
* subset of the protocol (the handshake and host context, tool input and result, cancellation,
|
|
4
|
+
* teardown, sizes and errors; `ui/message`, `ui/open-link`, `ui/download-file`, `tools/call`,
|
|
5
|
+
* `resources/read` and `ui/update-model-context` through the bridge; the sandbox-proxy handshake),
|
|
6
|
+
* implemented as a `ProtocolAdapter` against the core's seams. It depends on the core only, never
|
|
7
|
+
* on the MCP SDK.
|
|
8
|
+
*
|
|
9
|
+
* const view = readViewResource(await client.readResource({ uri }));
|
|
10
|
+
* const host = createHost(presets.mcpApp({ egress: view.egress, capabilities: { callTool: {} } }), {
|
|
11
|
+
* adapter: mcpAppsAdapter(),
|
|
12
|
+
* handlers: { callTool: ({ name, arguments: args }) => client.callTool({ name, arguments: args }) },
|
|
13
|
+
* });
|
|
14
|
+
* const display = host.mount(element, { displayId, title, scheme, document: view.html });
|
|
15
|
+
* display.sendToolInput(args);
|
|
16
|
+
* display.sendToolResult(await client.callTool({ name, arguments: args }));
|
|
17
|
+
*
|
|
18
|
+
* Tool visibility (`_meta.ui.visibility`) is the host's policy: its `callTool` handler refuses a
|
|
19
|
+
* tool the view may not call.
|
|
20
|
+
*/
|
|
21
|
+
export { mcpAppsAdapter, viewHostContext, type McpAppConnection, type McpAppsAdapterOptions, type ToolInfo, type ToolResult, } from "./adapter.ts";
|
|
22
|
+
export { sandboxProxyDocument } from "./proxy.ts";
|
|
23
|
+
export { VIEW_MIME_TYPE, readViewResource, viewEgress, type ViewResource } from "./view.ts";
|
|
24
|
+
export type { AdapterConnection, AdapterEvent, AdapterSession, ProtocolAdapter, } from "../protocol/adapter.ts";
|
|
25
|
+
export type { HostContext } from "../protocol/mcp-apps.ts";
|
|
26
|
+
export { MCP_APPS_METHODS, MCP_APPS_PROTOCOL_VERSION, MCP_APPS_STYLE_VARIABLES, PORTHOLE_STYLES, } from "../protocol/mcp-apps.ts";
|
|
27
|
+
export { JSON_RPC_ERRORS, parseJsonRpc, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, } from "../protocol/jsonrpc.ts";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@botiverse/porthole/mcp`: the MCP Apps adapter for full MCP Apps views (P007): the host-side
|
|
3
|
+
* subset of the protocol (the handshake and host context, tool input and result, cancellation,
|
|
4
|
+
* teardown, sizes and errors; `ui/message`, `ui/open-link`, `ui/download-file`, `tools/call`,
|
|
5
|
+
* `resources/read` and `ui/update-model-context` through the bridge; the sandbox-proxy handshake),
|
|
6
|
+
* implemented as a `ProtocolAdapter` against the core's seams. It depends on the core only, never
|
|
7
|
+
* on the MCP SDK.
|
|
8
|
+
*
|
|
9
|
+
* const view = readViewResource(await client.readResource({ uri }));
|
|
10
|
+
* const host = createHost(presets.mcpApp({ egress: view.egress, capabilities: { callTool: {} } }), {
|
|
11
|
+
* adapter: mcpAppsAdapter(),
|
|
12
|
+
* handlers: { callTool: ({ name, arguments: args }) => client.callTool({ name, arguments: args }) },
|
|
13
|
+
* });
|
|
14
|
+
* const display = host.mount(element, { displayId, title, scheme, document: view.html });
|
|
15
|
+
* display.sendToolInput(args);
|
|
16
|
+
* display.sendToolResult(await client.callTool({ name, arguments: args }));
|
|
17
|
+
*
|
|
18
|
+
* Tool visibility (`_meta.ui.visibility`) is the host's policy: its `callTool` handler refuses a
|
|
19
|
+
* tool the view may not call.
|
|
20
|
+
*/
|
|
21
|
+
export { mcpAppsAdapter, viewHostContext, } from "./adapter.js";
|
|
22
|
+
export { sandboxProxyDocument } from "./proxy.js";
|
|
23
|
+
export { VIEW_MIME_TYPE, readViewResource, viewEgress } from "./view.js";
|
|
24
|
+
export { MCP_APPS_METHODS, MCP_APPS_PROTOCOL_VERSION, MCP_APPS_STYLE_VARIABLES, PORTHOLE_STYLES, } from "../protocol/mcp-apps.js";
|
|
25
|
+
export { JSON_RPC_ERRORS, parseJsonRpc, } from "../protocol/jsonrpc.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CompiledConfig } from "../compile/compile.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The MCP Apps sandbox proxy (spec § Sandbox proxy), served by `/server` on a scope's origin: a
|
|
4
|
+
* second origin, never the host's, where a view gets `allow-scripts allow-same-origin` (P016).
|
|
5
|
+
* It announces itself (`sandbox-proxy-ready`) until its host answers with the view's HTML
|
|
6
|
+
* (`sandbox-resource-ready`), then becomes the view: it writes the shell's document into its own,
|
|
7
|
+
* with the view's HTML as data in the shell's plan (never as markup for the page's parser, which
|
|
8
|
+
* could build what the shell's guards can't see), and the shell renders it inert, guarded, then
|
|
9
|
+
* runs it, under the CSP the proxy's headers already carry. Writing a document erases
|
|
10
|
+
* the window's listeners, so nothing of the proxy is left once the view runs, and the view talks
|
|
11
|
+
* to its host directly: the relay the spec describes is the identity here (P026).
|
|
12
|
+
*
|
|
13
|
+
* The proxy takes the HTML only from its parent, only from the host origins, and only once. It
|
|
14
|
+
* ignores the message's `csp`, `sandbox` and `permissions`: the policy is the one its headers set
|
|
15
|
+
* from the config and the view's declared egress when the host's backend issued it, and the
|
|
16
|
+
* frame's sandbox and `allow` are the host's, from the same config.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SANDBOX_PROXY_RUNTIME: string;
|
|
19
|
+
/**
|
|
20
|
+
* The proxy's document for a view under `compiled` (an `mcp-app` config with domain isolation),
|
|
21
|
+
* taking the view only from `hostOrigins`. The document it becomes is exactly
|
|
22
|
+
* `compiled.shellDocument({ origins: hostOrigins, document: html, cspMeta: false })`: the shell,
|
|
23
|
+
* carrying the view as data.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sandboxProxyDocument(compiled: CompiledConfig, hostOrigins: readonly string[]): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { MCP_APPS_METHODS } from "../protocol/mcp-apps.js";
|
|
2
|
+
/**
|
|
3
|
+
* The MCP Apps sandbox proxy (spec § Sandbox proxy), served by `/server` on a scope's origin: a
|
|
4
|
+
* second origin, never the host's, where a view gets `allow-scripts allow-same-origin` (P016).
|
|
5
|
+
* It announces itself (`sandbox-proxy-ready`) until its host answers with the view's HTML
|
|
6
|
+
* (`sandbox-resource-ready`), then becomes the view: it writes the shell's document into its own,
|
|
7
|
+
* with the view's HTML as data in the shell's plan (never as markup for the page's parser, which
|
|
8
|
+
* could build what the shell's guards can't see), and the shell renders it inert, guarded, then
|
|
9
|
+
* runs it, under the CSP the proxy's headers already carry. Writing a document erases
|
|
10
|
+
* the window's listeners, so nothing of the proxy is left once the view runs, and the view talks
|
|
11
|
+
* to its host directly: the relay the spec describes is the identity here (P026).
|
|
12
|
+
*
|
|
13
|
+
* The proxy takes the HTML only from its parent, only from the host origins, and only once. It
|
|
14
|
+
* ignores the message's `csp`, `sandbox` and `permissions`: the policy is the one its headers set
|
|
15
|
+
* from the config and the view's declared egress when the host's backend issued it, and the
|
|
16
|
+
* frame's sandbox and `allow` are the host's, from the same config.
|
|
17
|
+
*/
|
|
18
|
+
export const SANDBOX_PROXY_RUNTIME = String.raw `(() => {
|
|
19
|
+
"use strict";
|
|
20
|
+
const setup = __SETUP__;
|
|
21
|
+
if (window.parent === window) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let done = false;
|
|
25
|
+
window.addEventListener("message", (event) => {
|
|
26
|
+
if (done || event.source !== window.parent || setup.origins.indexOf(event.origin) === -1) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const data = event.data;
|
|
30
|
+
if (data === null || typeof data !== "object" || data.jsonrpc !== "2.0" || data.method !== setup.resource) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const params = data.params;
|
|
34
|
+
if (params === null || typeof params !== "object" || typeof params.html !== "string") {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
done = true;
|
|
38
|
+
// The view goes into the shell's plan as a JSON string, safe inside its <script>.
|
|
39
|
+
const literal = JSON.stringify(params.html)
|
|
40
|
+
.replace(/</g, "\\u003c")
|
|
41
|
+
.replace(/\u2028/g, "\\u2028")
|
|
42
|
+
.replace(/\u2029/g, "\\u2029");
|
|
43
|
+
const page = setup.shell.replace(setup.slot, () => literal);
|
|
44
|
+
document.open();
|
|
45
|
+
document.write(page);
|
|
46
|
+
document.close();
|
|
47
|
+
});
|
|
48
|
+
// Say so until answered: the host may start listening after this runs. Nothing secret is said.
|
|
49
|
+
let tries = 0;
|
|
50
|
+
const announce = () => {
|
|
51
|
+
if (done || tries >= 40) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
tries += 1;
|
|
55
|
+
window.parent.postMessage({ jsonrpc: "2.0", method: setup.ready, params: {} }, "*");
|
|
56
|
+
setTimeout(announce, 250);
|
|
57
|
+
};
|
|
58
|
+
announce();
|
|
59
|
+
})();`;
|
|
60
|
+
/** Where the view's HTML goes in the shell's plan: a string no view can produce as JSON. */
|
|
61
|
+
const VIEW_SLOT = "porthole-view-slot-7f3a9c";
|
|
62
|
+
/** JSON that is safe inside a `<script>`: no `</script>`, no `<!--`. */
|
|
63
|
+
const scriptJson = (value) => JSON.stringify(value)
|
|
64
|
+
.replace(/</gu, "\\u003c")
|
|
65
|
+
.replace(/\u2028/gu, "\\u2028")
|
|
66
|
+
.replace(/\u2029/gu, "\\u2029");
|
|
67
|
+
/**
|
|
68
|
+
* The proxy's document for a view under `compiled` (an `mcp-app` config with domain isolation),
|
|
69
|
+
* taking the view only from `hostOrigins`. The document it becomes is exactly
|
|
70
|
+
* `compiled.shellDocument({ origins: hostOrigins, document: html, cspMeta: false })`: the shell,
|
|
71
|
+
* carrying the view as data.
|
|
72
|
+
*/
|
|
73
|
+
export function sandboxProxyDocument(compiled, hostOrigins) {
|
|
74
|
+
if (compiled.config.mode !== "mcp-app") {
|
|
75
|
+
throw new TypeError(`The sandbox proxy serves MCP Apps views, not ${compiled.config.mode}`);
|
|
76
|
+
}
|
|
77
|
+
// The shell with a slot for the view: the slot's JSON literal, replaced by the view's.
|
|
78
|
+
const shell = compiled.shellDocument({
|
|
79
|
+
origins: hostOrigins,
|
|
80
|
+
document: VIEW_SLOT,
|
|
81
|
+
cspMeta: false,
|
|
82
|
+
});
|
|
83
|
+
const slot = JSON.stringify(VIEW_SLOT);
|
|
84
|
+
if (shell.split(slot).length !== 2) {
|
|
85
|
+
throw new Error("The shell document must hold the view's slot exactly once");
|
|
86
|
+
}
|
|
87
|
+
const runtime = SANDBOX_PROXY_RUNTIME.replace("__SETUP__", () => scriptJson({
|
|
88
|
+
origins: hostOrigins,
|
|
89
|
+
shell,
|
|
90
|
+
slot,
|
|
91
|
+
ready: MCP_APPS_METHODS.sandboxProxyReady,
|
|
92
|
+
resource: MCP_APPS_METHODS.sandboxResourceReady,
|
|
93
|
+
}));
|
|
94
|
+
return `<!doctype html>
|
|
95
|
+
<html lang="en">
|
|
96
|
+
<head>
|
|
97
|
+
<meta charset="utf-8">
|
|
98
|
+
<meta name="referrer" content="no-referrer">
|
|
99
|
+
<title>Sandbox</title>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<script>${runtime}</script>
|
|
103
|
+
</body>
|
|
104
|
+
</html>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EgressConfig } from "../config/schema.ts";
|
|
2
|
+
/**
|
|
3
|
+
* What a host reads from an MCP Apps view's resource before showing it: the HTML, and the egress
|
|
4
|
+
* the view declares, in Porthole's vocabulary so it goes into a config (P003).
|
|
5
|
+
*/
|
|
6
|
+
/** The style variables MCP Apps standardizes: one list, the core's (P028). */
|
|
7
|
+
export { MCP_APPS_STYLE_VARIABLES } from "../protocol/mcp-apps.ts";
|
|
8
|
+
/**
|
|
9
|
+
* A view's `_meta.ui.csp` as egress: `connectDomains` to connect, `resourceDomains` to scripts,
|
|
10
|
+
* styles, images, fonts and media. What isn't an exact origin (or `https://*.` one) is dropped,
|
|
11
|
+
* never widened. `frameDomains` and `baseUriDomains` open nothing: the shell removes nested frames
|
|
12
|
+
* (P006) and `base-uri` stays `'none'`, so a view that needs either runs without it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function viewEgress(csp: unknown): EgressConfig;
|
|
15
|
+
/** A view, as `resources/read` gives it. */
|
|
16
|
+
export interface ViewResource {
|
|
17
|
+
readonly uri: string;
|
|
18
|
+
/** The view's whole document. */
|
|
19
|
+
readonly html: string;
|
|
20
|
+
/** What the view declares it reaches (its `_meta.ui.csp`), as egress. */
|
|
21
|
+
readonly egress: EgressConfig;
|
|
22
|
+
/** Whether the view asks the host for a border and background (`_meta.ui.prefersBorder`). */
|
|
23
|
+
readonly prefersBorder?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** The MIME type of an MCP Apps view. */
|
|
26
|
+
export declare const VIEW_MIME_TYPE = "text/html;profile=mcp-app";
|
|
27
|
+
/**
|
|
28
|
+
* The view in a `resources/read` result: its first content of the MCP Apps MIME type, as text or
|
|
29
|
+
* base64. Null when there is none.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readViewResource(result: unknown): ViewResource | null;
|
package/dist/mcp/view.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { isOriginSource } from "../config/validate.js";
|
|
2
|
+
/**
|
|
3
|
+
* What a host reads from an MCP Apps view's resource before showing it: the HTML, and the egress
|
|
4
|
+
* the view declares, in Porthole's vocabulary so it goes into a config (P003).
|
|
5
|
+
*/
|
|
6
|
+
/** The style variables MCP Apps standardizes: one list, the core's (P028). */
|
|
7
|
+
export { MCP_APPS_STYLE_VARIABLES } from "../protocol/mcp-apps.js";
|
|
8
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
/** A declared domain as a CSP source Porthole accepts, or null (`'self'`, `*`, paths, quotes). */
|
|
10
|
+
function originOf(value) {
|
|
11
|
+
if (typeof value !== "string") {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const source = value.trim().toLowerCase().replace(/\/$/u, "");
|
|
15
|
+
return isOriginSource(source) ? source : null;
|
|
16
|
+
}
|
|
17
|
+
const origins = (value) => Array.isArray(value)
|
|
18
|
+
? [...new Set(value.map(originOf).filter((each) => each !== null))]
|
|
19
|
+
: [];
|
|
20
|
+
/**
|
|
21
|
+
* A view's `_meta.ui.csp` as egress: `connectDomains` to connect, `resourceDomains` to scripts,
|
|
22
|
+
* styles, images, fonts and media. What isn't an exact origin (or `https://*.` one) is dropped,
|
|
23
|
+
* never widened. `frameDomains` and `baseUriDomains` open nothing: the shell removes nested frames
|
|
24
|
+
* (P006) and `base-uri` stays `'none'`, so a view that needs either runs without it.
|
|
25
|
+
*/
|
|
26
|
+
export function viewEgress(csp) {
|
|
27
|
+
if (!isRecord(csp)) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
const connect = origins(csp.connectDomains);
|
|
31
|
+
const resource = origins(csp.resourceDomains);
|
|
32
|
+
return {
|
|
33
|
+
...(connect.length > 0 ? { connect } : {}),
|
|
34
|
+
...(resource.length > 0
|
|
35
|
+
? { script: resource, style: resource, img: resource, font: resource, media: resource }
|
|
36
|
+
: {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** The MIME type of an MCP Apps view. */
|
|
40
|
+
export const VIEW_MIME_TYPE = "text/html;profile=mcp-app";
|
|
41
|
+
/**
|
|
42
|
+
* The view in a `resources/read` result: its first content of the MCP Apps MIME type, as text or
|
|
43
|
+
* base64. Null when there is none.
|
|
44
|
+
*/
|
|
45
|
+
export function readViewResource(result) {
|
|
46
|
+
const contents = isRecord(result) && Array.isArray(result.contents) ? result.contents : [];
|
|
47
|
+
for (const content of contents) {
|
|
48
|
+
if (!isRecord(content) || typeof content.mimeType !== "string") {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (content.mimeType.replace(/\s/gu, "").toLowerCase() !== VIEW_MIME_TYPE) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const html = typeof content.text === "string"
|
|
55
|
+
? content.text
|
|
56
|
+
: typeof content.blob === "string"
|
|
57
|
+
? new TextDecoder().decode(Uint8Array.from(atob(content.blob), (char) => char.charCodeAt(0)))
|
|
58
|
+
: null;
|
|
59
|
+
if (html === null) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const meta = content["_meta"];
|
|
63
|
+
const ui = isRecord(meta) && isRecord(meta.ui) ? meta.ui : {};
|
|
64
|
+
return {
|
|
65
|
+
uri: typeof content.uri === "string" ? content.uri : "",
|
|
66
|
+
html,
|
|
67
|
+
egress: viewEgress(ui.csp),
|
|
68
|
+
...(typeof ui.prefersBorder === "boolean" ? { prefersBorder: ui.prefersBorder } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ColorScheme } from "../config/schema.ts";
|
|
2
|
+
import type { FrameSource } from "../isolation/types.ts";
|
|
3
|
+
import type { Transport } from "../transport/types.ts";
|
|
4
|
+
/**
|
|
5
|
+
* The platform-host seam (ARCHITECTURE § Platform hosts): what owns the isolation primitive and
|
|
6
|
+
* the transport. The web's is an `<iframe>` with `postMessage`; a native web view is another
|
|
7
|
+
* implementation of the same interface, not a fork.
|
|
8
|
+
*/
|
|
9
|
+
/** What to create: the frame's document and its policy attributes, all from the compiled config. */
|
|
10
|
+
export interface SurfaceSpec {
|
|
11
|
+
readonly source: FrameSource;
|
|
12
|
+
/** The frame's accessible name. */
|
|
13
|
+
readonly title: string;
|
|
14
|
+
readonly sandbox: string;
|
|
15
|
+
readonly allow: string;
|
|
16
|
+
readonly referrerPolicy: "no-referrer";
|
|
17
|
+
readonly colorScheme: ColorScheme;
|
|
18
|
+
/**
|
|
19
|
+
* Fill the container's height (a url-mode app, which reports no size), at least
|
|
20
|
+
* `initialHeightPx`, instead of taking the content's.
|
|
21
|
+
*/
|
|
22
|
+
readonly fill?: boolean;
|
|
23
|
+
/** The height to start at (a remembered height, so a frame mounting again doesn't jump). */
|
|
24
|
+
readonly initialHeightPx: number;
|
|
25
|
+
}
|
|
26
|
+
/** One isolated frame. */
|
|
27
|
+
export interface Surface {
|
|
28
|
+
readonly transport: Transport;
|
|
29
|
+
/** The platform's own element for the frame (an `HTMLIFrameElement` on the web). */
|
|
30
|
+
readonly element: unknown;
|
|
31
|
+
setHeight(px: number): void;
|
|
32
|
+
setColorScheme(scheme: ColorScheme): void;
|
|
33
|
+
/**
|
|
34
|
+
* Whether the embedding page has transient user activation now (a gesture in the frame
|
|
35
|
+
* activates its ancestors), or undefined where the platform can't tell.
|
|
36
|
+
*/
|
|
37
|
+
userActivation(): boolean | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The frame loaded a document other than the one it was given: the content navigated it. The
|
|
40
|
+
* framework starts it again from its content and reports it.
|
|
41
|
+
*/
|
|
42
|
+
onNavigated(listener: () => void): () => void;
|
|
43
|
+
/** Every load of the frame's document, the first included. */
|
|
44
|
+
onLoad(listener: () => void): () => void;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
}
|
|
47
|
+
export interface PlatformHost<Container = HTMLElement> {
|
|
48
|
+
readonly name: string;
|
|
49
|
+
/** Creates an isolated frame in `container`, its policy set before its document loads. */
|
|
50
|
+
createSurface(container: Container, spec: SurfaceSpec): Surface;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|