@agent-native/core 0.22.15 → 0.22.18
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/client/embed-auth.d.ts.map +1 -1
- package/dist/client/embed-auth.js +161 -20
- package/dist/client/embed-auth.js.map +1 -1
- package/dist/client/frame.d.ts.map +1 -1
- package/dist/client/frame.js +1 -0
- package/dist/client/frame.js.map +1 -1
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +13 -0
- package/dist/client/use-action.js.map +1 -1
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +58 -3
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-db-sync.spec.js +27 -0
- package/dist/client/use-db-sync.spec.js.map +1 -1
- package/dist/deploy/build.d.ts +30 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +31 -16
- package/dist/deploy/build.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +29 -3
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +0 -1
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/mcp/embed-app.d.ts +2 -0
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +311 -35
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +9 -1
- package/dist/server/auth.js.map +1 -1
- package/dist/server/embed-route.d.ts.map +1 -1
- package/dist/server/embed-route.js +30 -7
- package/dist/server/embed-route.js.map +1 -1
- package/dist/server/embed-session.d.ts.map +1 -1
- package/dist/server/embed-session.js +11 -1
- package/dist/server/embed-session.js.map +1 -1
- package/dist/server/security-headers.d.ts +6 -1
- package/dist/server/security-headers.d.ts.map +1 -1
- package/dist/server/security-headers.js +10 -2
- package/dist/server/security-headers.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +28 -0
- package/dist/vite/client.js.map +1 -1
- package/docs/content/external-agents.md +24 -0
- package/package.json +1 -1
package/dist/mcp/embed-app.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from "../shared/embed-auth.js";
|
|
2
2
|
const MCP_APP_IMPORT = "https://esm.sh/@modelcontextprotocol/ext-apps@1.7.2/app-with-deps";
|
|
3
3
|
export const MCP_APP_REQUEST_ORIGIN_CSP_SOURCE = "$requestOrigin";
|
|
4
|
+
const MCP_APP_WRAPPER_CHROME_HEIGHT = 44;
|
|
5
|
+
export const DEFAULT_MCP_APP_VIEWPORT_HEIGHT = 720;
|
|
6
|
+
export const DEFAULT_MCP_APP_SHELL_HEIGHT = DEFAULT_MCP_APP_VIEWPORT_HEIGHT + MCP_APP_WRAPPER_CHROME_HEIGHT;
|
|
4
7
|
function attr(value) {
|
|
5
8
|
return String(value ?? "")
|
|
6
9
|
.replace(/&/g, "&")
|
|
@@ -14,7 +17,8 @@ export function embedApp(options = {}) {
|
|
|
14
17
|
const openLabel = options.openLabel ?? "Open in app";
|
|
15
18
|
const startToolName = options.startToolName ?? "create_embed_session";
|
|
16
19
|
const embedByDefault = options.embedByDefault !== false;
|
|
17
|
-
const height = Math.max(320, Math.min(900, options.height ??
|
|
20
|
+
const height = Math.max(320, Math.min(900, options.height ?? DEFAULT_MCP_APP_SHELL_HEIGHT));
|
|
21
|
+
const viewportHeight = height - MCP_APP_WRAPPER_CHROME_HEIGHT;
|
|
18
22
|
return {
|
|
19
23
|
title,
|
|
20
24
|
...(options.description ? { description: options.description } : {}),
|
|
@@ -33,9 +37,14 @@ export function embedApp(options = {}) {
|
|
|
33
37
|
.actions { display: flex; align-items: center; gap: 6px; }
|
|
34
38
|
button { min-height: 28px; border: 1px solid color-mix(in srgb, CanvasText 14%, Canvas); border-radius: 7px; background: Canvas; color: CanvasText; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; padding: 0 9px; }
|
|
35
39
|
button:disabled { opacity: .55; cursor: default; }
|
|
36
|
-
.stage { position: relative; min-height: ${
|
|
37
|
-
iframe { display: block; width: 100%; height: ${
|
|
38
|
-
.message { display: grid; place-items: center; min-height: ${
|
|
40
|
+
.stage { position: relative; min-height: ${viewportHeight}px; }
|
|
41
|
+
iframe { display: block; width: 100%; height: ${viewportHeight}px; border: 0; background: Canvas; }
|
|
42
|
+
.message { display: grid; place-items: center; min-height: ${viewportHeight}px; padding: 18px; color: color-mix(in srgb, CanvasText 62%, Canvas); font-size: 13px; line-height: 1.45; text-align: center; }
|
|
43
|
+
.fallback { display: grid; align-content: center; justify-items: center; gap: 12px; min-height: ${viewportHeight}px; padding: 24px; background: Canvas; color: CanvasText; text-align: center; }
|
|
44
|
+
.fallback-title { max-width: 440px; font-size: 14px; font-weight: 700; }
|
|
45
|
+
.fallback-copy { max-width: 520px; color: color-mix(in srgb, CanvasText 64%, Canvas); font-size: 13px; line-height: 1.45; }
|
|
46
|
+
.fallback-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }
|
|
47
|
+
.fallback-url { max-width: min(560px, 100%); overflow-wrap: anywhere; color: color-mix(in srgb, CanvasText 76%, Canvas); font-size: 12px; }
|
|
39
48
|
</style>
|
|
40
49
|
</head>
|
|
41
50
|
<body
|
|
@@ -58,9 +67,6 @@ export function embedApp(options = {}) {
|
|
|
58
67
|
</section>
|
|
59
68
|
</main>
|
|
60
69
|
<script type="module">
|
|
61
|
-
import { App } from "${MCP_APP_IMPORT}";
|
|
62
|
-
|
|
63
|
-
const app = new App({ name: "Agent Native Embed", version: "1.0.0" }, {});
|
|
64
70
|
const body = document.body;
|
|
65
71
|
const stage = document.querySelector("[data-stage]");
|
|
66
72
|
const titleEl = document.querySelector("[data-title-label]");
|
|
@@ -69,10 +75,17 @@ export function embedApp(options = {}) {
|
|
|
69
75
|
const startTool = body.dataset.startTool || "create_embed_session";
|
|
70
76
|
const embedByDefault = body.dataset.embedDefault !== "0";
|
|
71
77
|
const chatBridgeParam = ${JSON.stringify(MCP_APP_CHAT_BRIDGE_QUERY_PARAM)};
|
|
78
|
+
const intrinsicHeight = ${height};
|
|
79
|
+
let app = null;
|
|
80
|
+
let openAiBridge = null;
|
|
72
81
|
let toolInput = {};
|
|
73
82
|
let openUrl = "";
|
|
74
83
|
let startedFor = "";
|
|
75
84
|
let appFrame = null;
|
|
85
|
+
let appFrameReady = false;
|
|
86
|
+
let appFrameReadyTimer = null;
|
|
87
|
+
let appFrameLoadTimer = null;
|
|
88
|
+
let lastFrameSrc = "";
|
|
76
89
|
|
|
77
90
|
function esc(value) {
|
|
78
91
|
return String(value ?? "")
|
|
@@ -88,8 +101,20 @@ export function embedApp(options = {}) {
|
|
|
88
101
|
try { return JSON.parse(value); } catch { return fallback; }
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
function objectValue(value) {
|
|
105
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
106
|
+
? value
|
|
107
|
+
: {};
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
function parseToolResult(params) {
|
|
92
111
|
if (!params) return {};
|
|
112
|
+
if (params.result && typeof params.result === "object") {
|
|
113
|
+
return parseToolResult(params.result);
|
|
114
|
+
}
|
|
115
|
+
if (params.toolResult && typeof params.toolResult === "object") {
|
|
116
|
+
return parseToolResult(params.toolResult);
|
|
117
|
+
}
|
|
93
118
|
if (params.structuredContent && typeof params.structuredContent === "object") {
|
|
94
119
|
return params.structuredContent;
|
|
95
120
|
}
|
|
@@ -106,10 +131,26 @@ export function embedApp(options = {}) {
|
|
|
106
131
|
}
|
|
107
132
|
|
|
108
133
|
function hostState() {
|
|
134
|
+
if (openAiBridge) {
|
|
135
|
+
return {
|
|
136
|
+
context: {
|
|
137
|
+
displayMode: openAiBridge.displayMode,
|
|
138
|
+
availableDisplayModes: typeof openAiBridge.requestDisplayMode === "function"
|
|
139
|
+
? ["inline", "fullscreen", "pip"]
|
|
140
|
+
: [],
|
|
141
|
+
maxHeight: openAiBridge.maxHeight,
|
|
142
|
+
locale: openAiBridge.locale,
|
|
143
|
+
theme: openAiBridge.theme,
|
|
144
|
+
view: openAiBridge.view
|
|
145
|
+
},
|
|
146
|
+
capabilities: { openai: true },
|
|
147
|
+
version: openAiBridge.userAgent
|
|
148
|
+
};
|
|
149
|
+
}
|
|
109
150
|
return {
|
|
110
|
-
context: app.getHostContext ? app.getHostContext() : undefined,
|
|
111
|
-
capabilities: app.getHostCapabilities ? app.getHostCapabilities() : undefined,
|
|
112
|
-
version: app.getHostVersion ? app.getHostVersion() : undefined
|
|
151
|
+
context: app && app.getHostContext ? app.getHostContext() : undefined,
|
|
152
|
+
capabilities: app && app.getHostCapabilities ? app.getHostCapabilities() : undefined,
|
|
153
|
+
version: app && app.getHostVersion ? app.getHostVersion() : undefined
|
|
113
154
|
};
|
|
114
155
|
}
|
|
115
156
|
|
|
@@ -153,12 +194,23 @@ export function embedApp(options = {}) {
|
|
|
153
194
|
}
|
|
154
195
|
|
|
155
196
|
function supportedDisplayMode(mode) {
|
|
197
|
+
if (openAiBridge && typeof openAiBridge.requestDisplayMode === "function") {
|
|
198
|
+
return mode === "inline" || mode === "fullscreen" || mode === "pip";
|
|
199
|
+
}
|
|
156
200
|
const modes = hostState().context && hostState().context.availableDisplayModes;
|
|
157
201
|
return Array.isArray(modes) && modes.includes(mode);
|
|
158
202
|
}
|
|
159
203
|
|
|
160
204
|
async function requestHostDisplayMode(mode) {
|
|
161
|
-
|
|
205
|
+
let result;
|
|
206
|
+
if (openAiBridge && typeof openAiBridge.requestDisplayMode === "function") {
|
|
207
|
+
result = await openAiBridge.requestDisplayMode({ mode });
|
|
208
|
+
} else {
|
|
209
|
+
if (!app || typeof app.requestDisplayMode !== "function") {
|
|
210
|
+
throw new Error("Display mode changes are not available in this host.");
|
|
211
|
+
}
|
|
212
|
+
result = await app.requestDisplayMode({ mode });
|
|
213
|
+
}
|
|
162
214
|
updateDisplayButton();
|
|
163
215
|
sendHostContext();
|
|
164
216
|
return result;
|
|
@@ -183,14 +235,94 @@ export function embedApp(options = {}) {
|
|
|
183
235
|
stage.innerHTML = '<div class="message">' + esc(message) + '</div>';
|
|
184
236
|
}
|
|
185
237
|
|
|
238
|
+
function clearFrameReadyTimer() {
|
|
239
|
+
if (!appFrameReadyTimer) return;
|
|
240
|
+
clearTimeout(appFrameReadyTimer);
|
|
241
|
+
appFrameReadyTimer = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function clearFrameLoadTimer() {
|
|
245
|
+
if (!appFrameLoadTimer) return;
|
|
246
|
+
clearTimeout(appFrameLoadTimer);
|
|
247
|
+
appFrameLoadTimer = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function startFrameReadyTimer(frame) {
|
|
251
|
+
clearFrameReadyTimer();
|
|
252
|
+
appFrameReadyTimer = setTimeout(() => {
|
|
253
|
+
if (!appFrameReady && appFrame === frame) renderFrameFallback();
|
|
254
|
+
}, 7000);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderFrameFallback() {
|
|
258
|
+
clearFrameReadyTimer();
|
|
259
|
+
clearFrameLoadTimer();
|
|
260
|
+
appFrame = null;
|
|
261
|
+
stage.innerHTML =
|
|
262
|
+
'<div class="fallback">' +
|
|
263
|
+
'<div class="fallback-title">Open this app in its own tab</div>' +
|
|
264
|
+
'<div class="fallback-copy">This chat host did not allow the embedded app frame to load inline. You can still open the same app route through the host or use the URL below.</div>' +
|
|
265
|
+
'<div class="fallback-actions">' +
|
|
266
|
+
'<button type="button" data-fallback-open>Open app</button>' +
|
|
267
|
+
'<button type="button" data-fallback-retry>Try inline again</button>' +
|
|
268
|
+
'</div>' +
|
|
269
|
+
(openUrl ? '<a class="fallback-url" href="' + esc(openUrl) + '" target="_blank" rel="noreferrer">' + esc(openUrl) + '</a>' : '') +
|
|
270
|
+
'</div>';
|
|
271
|
+
const fallbackOpen = stage.querySelector("[data-fallback-open]");
|
|
272
|
+
const fallbackRetry = stage.querySelector("[data-fallback-retry]");
|
|
273
|
+
if (fallbackOpen) {
|
|
274
|
+
fallbackOpen.disabled = !openUrl;
|
|
275
|
+
fallbackOpen.onclick = () => {
|
|
276
|
+
if (openUrl) void openFallbackExternal();
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (fallbackRetry) {
|
|
280
|
+
fallbackRetry.disabled = !lastFrameSrc;
|
|
281
|
+
fallbackRetry.onclick = () => {
|
|
282
|
+
if (lastFrameSrc) renderFrame(lastFrameSrc);
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function openFallbackExternal() {
|
|
288
|
+
let url = openUrl;
|
|
289
|
+
try {
|
|
290
|
+
const embedUrl = withChatBridgeParam(openUrl);
|
|
291
|
+
const result = await callEmbedSessionTool({
|
|
292
|
+
url: embedUrl,
|
|
293
|
+
chrome: typeof toolInput.chrome === "string" ? toolInput.chrome : "full"
|
|
294
|
+
});
|
|
295
|
+
const data = parseToolResult(result);
|
|
296
|
+
if (typeof data.startUrl === "string" && data.startUrl) {
|
|
297
|
+
url = data.startUrl;
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.warn("[agent-native] MCP fallback could not mint a fresh app session", err);
|
|
301
|
+
}
|
|
302
|
+
await openHostLink({ url });
|
|
303
|
+
}
|
|
304
|
+
|
|
186
305
|
function renderFrame(src) {
|
|
306
|
+
clearFrameReadyTimer();
|
|
307
|
+
clearFrameLoadTimer();
|
|
187
308
|
const frame = document.createElement("iframe");
|
|
188
309
|
frame.title = body.dataset.iframeTitle || "Agent Native app";
|
|
189
310
|
frame.src = src;
|
|
190
311
|
frame.allow = "clipboard-read; clipboard-write";
|
|
191
312
|
appFrame = frame;
|
|
192
|
-
|
|
313
|
+
appFrameReady = false;
|
|
314
|
+
lastFrameSrc = src;
|
|
315
|
+
frame.addEventListener("load", () => {
|
|
316
|
+
if (appFrame !== frame) return;
|
|
317
|
+
clearFrameLoadTimer();
|
|
318
|
+
sendFrameReadyMessages(frame);
|
|
319
|
+
startFrameReadyTimer(frame);
|
|
320
|
+
});
|
|
193
321
|
stage.replaceChildren(frame);
|
|
322
|
+
notifyHostHeight();
|
|
323
|
+
appFrameLoadTimer = setTimeout(() => {
|
|
324
|
+
if (!appFrameReady && appFrame === frame) renderFrameFallback();
|
|
325
|
+
}, 30000);
|
|
194
326
|
}
|
|
195
327
|
|
|
196
328
|
async function updateHostModelContext(data) {
|
|
@@ -199,13 +331,40 @@ export function embedApp(options = {}) {
|
|
|
199
331
|
if (data && data.structuredContent && typeof data.structuredContent === "object") {
|
|
200
332
|
params.structuredContent = data.structuredContent;
|
|
201
333
|
}
|
|
334
|
+
if (openAiBridge && typeof openAiBridge.setWidgetState === "function") {
|
|
335
|
+
openAiBridge.setWidgetState({
|
|
336
|
+
...objectValue(openAiBridge.widgetState),
|
|
337
|
+
agentNativeModelContext: params
|
|
338
|
+
});
|
|
339
|
+
return { ok: true };
|
|
340
|
+
}
|
|
341
|
+
if (!app || typeof app.updateModelContext !== "function") return { ok: false };
|
|
202
342
|
await app.updateModelContext(params);
|
|
343
|
+
return { ok: true };
|
|
203
344
|
}
|
|
204
345
|
|
|
205
346
|
async function openHostLink(data) {
|
|
206
347
|
const url = typeof (data && data.url) === "string" ? data.url : "";
|
|
207
348
|
if (!url) return { isError: true };
|
|
208
|
-
|
|
349
|
+
if (openAiBridge && typeof openAiBridge.openExternal === "function") {
|
|
350
|
+
return await openAiBridge.openExternal({ href: url, redirectUrl: false });
|
|
351
|
+
}
|
|
352
|
+
if (app && typeof app.openLink === "function") {
|
|
353
|
+
return await app.openLink({ url });
|
|
354
|
+
}
|
|
355
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
356
|
+
return { ok: true };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function notifyHostHeight() {
|
|
360
|
+
if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== "function") {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
openAiBridge.notifyIntrinsicHeight({ height: intrinsicHeight });
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.warn("[agent-native] ChatGPT rejected intrinsic height update", err);
|
|
367
|
+
}
|
|
209
368
|
}
|
|
210
369
|
|
|
211
370
|
function respondToAppFrame(requestId, work) {
|
|
@@ -236,14 +395,29 @@ export function embedApp(options = {}) {
|
|
|
236
395
|
const context = typeof chat.context === "string" ? chat.context : "";
|
|
237
396
|
if (context.trim()) {
|
|
238
397
|
try {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
398
|
+
if (openAiBridge && typeof openAiBridge.setWidgetState === "function") {
|
|
399
|
+
openAiBridge.setWidgetState({
|
|
400
|
+
...objectValue(openAiBridge.widgetState),
|
|
401
|
+
agentNativeChatContext: context
|
|
402
|
+
});
|
|
403
|
+
} else if (app && typeof app.updateModelContext === "function") {
|
|
404
|
+
await app.updateModelContext({
|
|
405
|
+
content: [{ type: "text", text: context }]
|
|
406
|
+
});
|
|
407
|
+
}
|
|
242
408
|
} catch (err) {
|
|
243
409
|
console.warn("[agent-native] MCP host rejected model context update", err);
|
|
244
410
|
}
|
|
245
411
|
}
|
|
246
412
|
try {
|
|
413
|
+
if (openAiBridge && typeof openAiBridge.sendFollowUpMessage === "function") {
|
|
414
|
+
await openAiBridge.sendFollowUpMessage({
|
|
415
|
+
prompt: context.trim() ? context.trim() + "\\n\\n" + message : message,
|
|
416
|
+
scrollToBottom: true
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (!app || typeof app.sendMessage !== "function") return;
|
|
247
421
|
const result = await app.sendMessage({
|
|
248
422
|
role: "user",
|
|
249
423
|
content: [{ type: "text", text: message }]
|
|
@@ -260,6 +434,12 @@ export function embedApp(options = {}) {
|
|
|
260
434
|
if (!appFrame || event.source !== appFrame.contentWindow) return;
|
|
261
435
|
if (!event.data) return;
|
|
262
436
|
const data = event.data.data || {};
|
|
437
|
+
if (event.data.type === "agentNative.embeddedAppReady") {
|
|
438
|
+
appFrameReady = true;
|
|
439
|
+
clearFrameLoadTimer();
|
|
440
|
+
clearFrameReadyTimer();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
263
443
|
if (event.data.type === "agentNative.submitChat") {
|
|
264
444
|
void sendHostChat(data);
|
|
265
445
|
return;
|
|
@@ -291,12 +471,9 @@ export function embedApp(options = {}) {
|
|
|
291
471
|
setMessage("Loading app");
|
|
292
472
|
try {
|
|
293
473
|
const embedUrl = withChatBridgeParam(openUrl);
|
|
294
|
-
const result = await
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
url: embedUrl,
|
|
298
|
-
chrome: typeof toolInput.chrome === "string" ? toolInput.chrome : "full"
|
|
299
|
-
}
|
|
474
|
+
const result = await callEmbedSessionTool({
|
|
475
|
+
url: embedUrl,
|
|
476
|
+
chrome: typeof toolInput.chrome === "string" ? toolInput.chrome : "full"
|
|
300
477
|
});
|
|
301
478
|
const data = parseToolResult(result);
|
|
302
479
|
if (!data.startUrl) {
|
|
@@ -311,11 +488,33 @@ export function embedApp(options = {}) {
|
|
|
311
488
|
}
|
|
312
489
|
}
|
|
313
490
|
|
|
491
|
+
async function callEmbedSessionTool(args) {
|
|
492
|
+
if (openAiBridge && typeof openAiBridge.callTool === "function") {
|
|
493
|
+
return await openAiBridge.callTool(startTool, args);
|
|
494
|
+
}
|
|
495
|
+
if (!app || typeof app.callServerTool !== "function") {
|
|
496
|
+
throw new Error("Host tool calls are not available.");
|
|
497
|
+
}
|
|
498
|
+
return await app.callServerTool({ name: startTool, arguments: args });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function updateHostOpenInAppUrl() {
|
|
502
|
+
if (!openAiBridge || !openUrl || typeof openAiBridge.setOpenInAppUrl !== "function") {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
openAiBridge.setOpenInAppUrl({ href: openUrl });
|
|
507
|
+
} catch (err) {
|
|
508
|
+
console.warn("[agent-native] ChatGPT rejected open-in-app URL", err);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
314
512
|
function updateOpenButton() {
|
|
315
513
|
openButton.disabled = !openUrl;
|
|
316
514
|
openButton.onclick = () => {
|
|
317
|
-
if (openUrl) void
|
|
515
|
+
if (openUrl) void openHostLink({ url: openUrl });
|
|
318
516
|
};
|
|
517
|
+
updateHostOpenInAppUrl();
|
|
319
518
|
}
|
|
320
519
|
|
|
321
520
|
function updateTitle(data) {
|
|
@@ -323,29 +522,106 @@ export function embedApp(options = {}) {
|
|
|
323
522
|
titleEl.textContent = String(label);
|
|
324
523
|
}
|
|
325
524
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
525
|
+
function readOpenAiBridge() {
|
|
526
|
+
return window.openai && typeof window.openai === "object"
|
|
527
|
+
? window.openai
|
|
528
|
+
: null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function openAiToolResultParams(bridge) {
|
|
532
|
+
const params = {};
|
|
533
|
+
if (bridge && bridge.toolOutput !== undefined) {
|
|
534
|
+
if (bridge.toolOutput && typeof bridge.toolOutput === "object") {
|
|
535
|
+
params.structuredContent = bridge.toolOutput;
|
|
536
|
+
} else {
|
|
537
|
+
params.content = [{ type: "text", text: String(bridge.toolOutput) }];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (bridge && bridge.toolResponseMetadata && typeof bridge.toolResponseMetadata === "object") {
|
|
541
|
+
params._meta = bridge.toolResponseMetadata;
|
|
542
|
+
}
|
|
543
|
+
return params;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function syncOpenAiBridge(bridge) {
|
|
547
|
+
if (!bridge) return false;
|
|
548
|
+
openAiBridge = bridge;
|
|
549
|
+
toolInput = objectValue(bridge.toolInput);
|
|
550
|
+
const params = openAiToolResultParams(bridge);
|
|
330
551
|
const data = parseToolResult(params);
|
|
331
552
|
openUrl = openLinkFrom(params, data);
|
|
332
553
|
updateTitle(data);
|
|
333
554
|
updateOpenButton();
|
|
334
|
-
void launchEmbed();
|
|
335
|
-
};
|
|
336
|
-
app.onhostcontextchanged = () => {
|
|
337
555
|
updateDisplayButton();
|
|
556
|
+
notifyHostHeight();
|
|
338
557
|
sendHostContext();
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
558
|
+
if (openUrl) {
|
|
559
|
+
void launchEmbed();
|
|
560
|
+
} else if (!appFrame) {
|
|
561
|
+
setMessage("Waiting for app result");
|
|
562
|
+
}
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function waitForOpenAiBridge() {
|
|
567
|
+
const existing = readOpenAiBridge();
|
|
568
|
+
if (existing) return Promise.resolve(existing);
|
|
569
|
+
return new Promise((resolve) => {
|
|
570
|
+
let settled = false;
|
|
571
|
+
const finish = (bridge) => {
|
|
572
|
+
if (settled) return;
|
|
573
|
+
settled = true;
|
|
574
|
+
window.removeEventListener("openai:set_globals", onGlobals);
|
|
575
|
+
clearTimeout(timer);
|
|
576
|
+
resolve(bridge || readOpenAiBridge());
|
|
577
|
+
};
|
|
578
|
+
const onGlobals = () => finish(readOpenAiBridge());
|
|
579
|
+
const timer = setTimeout(() => finish(null), 200);
|
|
580
|
+
window.addEventListener("openai:set_globals", onGlobals, { passive: true });
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
window.addEventListener("openai:set_globals", () => {
|
|
585
|
+
const bridge = readOpenAiBridge();
|
|
586
|
+
if (bridge && (!appFrame || openAiBridge)) syncOpenAiBridge(bridge);
|
|
587
|
+
}, { passive: true });
|
|
588
|
+
|
|
589
|
+
async function startMcpAppsBridge() {
|
|
590
|
+
const { App } = await import("${MCP_APP_IMPORT}");
|
|
591
|
+
app = new App({ name: "Agent Native Embed", version: "1.0.0" }, {});
|
|
592
|
+
app.ontoolinput = (params) => {
|
|
593
|
+
toolInput = params.arguments || {};
|
|
594
|
+
};
|
|
595
|
+
app.ontoolresult = (params) => {
|
|
596
|
+
const data = parseToolResult(params);
|
|
597
|
+
openUrl = openLinkFrom(params, data);
|
|
598
|
+
updateTitle(data);
|
|
599
|
+
updateOpenButton();
|
|
600
|
+
void launchEmbed();
|
|
601
|
+
};
|
|
602
|
+
app.onhostcontextchanged = () => {
|
|
603
|
+
updateDisplayButton();
|
|
604
|
+
sendHostContext();
|
|
605
|
+
};
|
|
606
|
+
await app.connect();
|
|
607
|
+
updateDisplayButton();
|
|
608
|
+
sendHostContext();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const initialOpenAiBridge = await waitForOpenAiBridge();
|
|
612
|
+
if (!syncOpenAiBridge(initialOpenAiBridge)) {
|
|
613
|
+
await startMcpAppsBridge();
|
|
614
|
+
}
|
|
343
615
|
</script>
|
|
344
616
|
</body>
|
|
345
617
|
</html>`,
|
|
346
618
|
csp: {
|
|
347
619
|
connectDomains: ["https://esm.sh"],
|
|
348
|
-
resourceDomains: [
|
|
620
|
+
resourceDomains: [
|
|
621
|
+
"https://esm.sh",
|
|
622
|
+
MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,
|
|
623
|
+
...(options.frameDomains ?? []),
|
|
624
|
+
],
|
|
349
625
|
frameDomains: [
|
|
350
626
|
MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,
|
|
351
627
|
...(options.frameDomains ?? []),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embed-app.js","sourceRoot":"","sources":["../../src/mcp/embed-app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,+BAA+B,EAAE,MAAM,yBAAyB,CAAC;AAE1E,MAAM,cAAc,GAClB,mEAAmE,CAAC;AAEtE,MAAM,CAAC,MAAM,iCAAiC,GAAG,gBAAgB,CAAC;AAalE,SAAS,IAAI,CAAC,KAAyB;IACrC,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,UAA2B,EAAE;IAE7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC;IAC1C,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,kBAAkB,CAAC;IAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,aAAa,CAAC;IACrD,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACtE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,KAAK,KAAK,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC;IAEnE,OAAO;QACL,KAAK;QACL,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,IAAI,EAAE,GAAG,EAAE,CAAC;;;;;;;;;oDASoC,MAAM;;;;;;+CAMX,MAAM,GAAG,EAAE;oDACN,MAAM,GAAG,EAAE;iEACE,MAAM,GAAG,EAAE;;;;oBAIxD,IAAI,CAAC,KAAK,CAAC;uBACR,IAAI,CAAC,WAAW,CAAC;qBACnB,IAAI,CAAC,SAAS,CAAC;qBACf,IAAI,CAAC,aAAa,CAAC;wBAChB,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;;;;4CAIN,IAAI,CAAC,KAAK,CAAC;;;mDAGJ,IAAI,CAAC,SAAS,CAAC;;;;;;;;2BAQvC,cAAc;;;;;;;;;;8BAUX,IAAI,CAAC,SAAS,CAAC,+BAA+B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAkRrE;QACJ,GAAG,EAAE;YACH,cAAc,EAAE,CAAC,gBAAgB,CAAC;YAClC,eAAe,EAAE,CAAC,gBAAgB,CAAC;YACnC,YAAY,EAAE;gBACZ,iCAAiC;gBACjC,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;aAChC;SACF;QACD,aAAa,EAAE,KAAK;KACrB,CAAC;AACJ,CAAC","sourcesContent":["import type { ActionMcpAppResourceConfig } from \"../action.js\";\nimport { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from \"../shared/embed-auth.js\";\n\nconst MCP_APP_IMPORT =\n \"https://esm.sh/@modelcontextprotocol/ext-apps@1.7.2/app-with-deps\";\n\nexport const MCP_APP_REQUEST_ORIGIN_CSP_SOURCE = \"$requestOrigin\";\n\nexport interface EmbedAppOptions {\n title?: string;\n description?: string;\n iframeTitle?: string;\n openLabel?: string;\n embedByDefault?: boolean;\n startToolName?: string;\n frameDomains?: string[];\n height?: number;\n}\n\nfunction attr(value: string | undefined): string {\n return String(value ?? \"\")\n .replace(/&/g, \"&\")\n .replace(/\"/g, \""\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\nexport function embedApp(\n options: EmbedAppOptions = {},\n): ActionMcpAppResourceConfig {\n const title = options.title ?? \"Open app\";\n const iframeTitle = options.iframeTitle ?? \"Agent Native app\";\n const openLabel = options.openLabel ?? \"Open in app\";\n const startToolName = options.startToolName ?? \"create_embed_session\";\n const embedByDefault = options.embedByDefault !== false;\n const height = Math.max(320, Math.min(900, options.height ?? 900));\n\n return {\n title,\n ...(options.description ? { description: options.description } : {}),\n html: () => `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <style>\n :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: Canvas; color: CanvasText; }\n * { box-sizing: border-box; }\n body { margin: 0; }\n .shell { display: grid; gap: 8px; min-height: ${height}px; padding: 0; }\n .bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 36px; padding: 6px 8px; border-bottom: 1px solid color-mix(in srgb, CanvasText 12%, Canvas); }\n .title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 700; color: color-mix(in srgb, CanvasText 72%, Canvas); }\n .actions { display: flex; align-items: center; gap: 6px; }\n button { min-height: 28px; border: 1px solid color-mix(in srgb, CanvasText 14%, Canvas); border-radius: 7px; background: Canvas; color: CanvasText; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; padding: 0 9px; }\n button:disabled { opacity: .55; cursor: default; }\n .stage { position: relative; min-height: ${height - 44}px; }\n iframe { display: block; width: 100%; height: ${height - 44}px; border: 0; background: Canvas; }\n .message { display: grid; place-items: center; min-height: ${height - 44}px; padding: 18px; color: color-mix(in srgb, CanvasText 62%, Canvas); font-size: 13px; line-height: 1.45; text-align: center; }\n </style>\n</head>\n<body\n data-app-title=\"${attr(title)}\"\n data-iframe-title=\"${attr(iframeTitle)}\"\n data-open-label=\"${attr(openLabel)}\"\n data-start-tool=\"${attr(startToolName)}\"\n data-embed-default=\"${embedByDefault ? \"1\" : \"0\"}\"\n>\n <main class=\"shell\">\n <div class=\"bar\">\n <div class=\"title\" data-title-label>${attr(title)}</div>\n <div class=\"actions\">\n <button type=\"button\" data-display hidden disabled>Fullscreen</button>\n <button type=\"button\" data-open disabled>${attr(openLabel)}</button>\n </div>\n </div>\n <section class=\"stage\" data-stage>\n <div class=\"message\">Preparing app</div>\n </section>\n </main>\n <script type=\"module\">\n import { App } from \"${MCP_APP_IMPORT}\";\n\n const app = new App({ name: \"Agent Native Embed\", version: \"1.0.0\" }, {});\n const body = document.body;\n const stage = document.querySelector(\"[data-stage]\");\n const titleEl = document.querySelector(\"[data-title-label]\");\n const openButton = document.querySelector(\"[data-open]\");\n const displayButton = document.querySelector(\"[data-display]\");\n const startTool = body.dataset.startTool || \"create_embed_session\";\n const embedByDefault = body.dataset.embedDefault !== \"0\";\n const chatBridgeParam = ${JSON.stringify(MCP_APP_CHAT_BRIDGE_QUERY_PARAM)};\n let toolInput = {};\n let openUrl = \"\";\n let startedFor = \"\";\n let appFrame = null;\n\n function esc(value) {\n return String(value ?? \"\")\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n }\n\n function parseJson(value, fallback) {\n if (value && typeof value === \"object\") return value;\n if (typeof value !== \"string\" || !value.trim()) return fallback;\n try { return JSON.parse(value); } catch { return fallback; }\n }\n\n function parseToolResult(params) {\n if (!params) return {};\n if (params.structuredContent && typeof params.structuredContent === \"object\") {\n return params.structuredContent;\n }\n const parts = Array.isArray(params.content) ? params.content : [];\n const textPart = parts.find((part) => part && part.type === \"text\" && typeof part.text === \"string\");\n return parseJson(textPart ? textPart.text : \"\", {});\n }\n\n function openLinkFrom(params, data) {\n const metaUrl = params && params._meta && params._meta[\"agent-native/openLink\"]\n ? params._meta[\"agent-native/openLink\"].webUrl\n : \"\";\n return metaUrl || data.url || data.deepLink || data.openUrl || \"\";\n }\n\n function hostState() {\n return {\n context: app.getHostContext ? app.getHostContext() : undefined,\n capabilities: app.getHostCapabilities ? app.getHostCapabilities() : undefined,\n version: app.getHostVersion ? app.getHostVersion() : undefined\n };\n }\n\n function sendToAppFrame(message) {\n if (!appFrame || !appFrame.contentWindow) return;\n try { appFrame.contentWindow.postMessage(message, \"*\"); } catch {}\n }\n\n function sendHostContext() {\n sendToAppFrame({ type: \"agentNative.mcpHostContext\", data: hostState() });\n }\n\n function sendFrameReadyMessages(frame) {\n const originPayload = { type: \"agentNative.frameOrigin\", origin: window.location.origin };\n [0, 200, 500, 1500].forEach((delay) => {\n setTimeout(() => {\n try { frame.contentWindow && frame.contentWindow.postMessage(originPayload, \"*\"); } catch {}\n sendHostContext();\n }, delay);\n });\n }\n\n function withChatBridgeParam(value) {\n if (typeof value !== \"string\" || !value) return value;\n try {\n const base = \"http://agent-native.invalid\";\n const url = value.startsWith(\"/\") ? new URL(value, base) : new URL(value);\n url.searchParams.set(chatBridgeParam, \"1\");\n return value.startsWith(\"/\")\n ? url.pathname + url.search + url.hash\n : url.toString();\n } catch {\n return value;\n }\n }\n\n function wantsEmbed() {\n if (toolInput.embed === false || toolInput.embed === \"false\") return false;\n if (embedByDefault) return true;\n return toolInput.embed === true || toolInput.embed === \"true\";\n }\n\n function supportedDisplayMode(mode) {\n const modes = hostState().context && hostState().context.availableDisplayModes;\n return Array.isArray(modes) && modes.includes(mode);\n }\n\n async function requestHostDisplayMode(mode) {\n const result = await app.requestDisplayMode({ mode });\n updateDisplayButton();\n sendHostContext();\n return result;\n }\n\n function updateDisplayButton() {\n const context = hostState().context || {};\n const nextMode = context.displayMode === \"fullscreen\" ? \"inline\" : \"fullscreen\";\n const supported = supportedDisplayMode(nextMode);\n displayButton.hidden = !supported;\n displayButton.disabled = !supported;\n displayButton.textContent = nextMode === \"fullscreen\" ? \"Fullscreen\" : \"Inline\";\n displayButton.onclick = () => {\n if (!supportedDisplayMode(nextMode)) return;\n void requestHostDisplayMode(nextMode).catch((err) => {\n console.warn(\"[agent-native] MCP host rejected display mode request\", err);\n });\n };\n }\n\n function setMessage(message) {\n stage.innerHTML = '<div class=\"message\">' + esc(message) + '</div>';\n }\n\n function renderFrame(src) {\n const frame = document.createElement(\"iframe\");\n frame.title = body.dataset.iframeTitle || \"Agent Native app\";\n frame.src = src;\n frame.allow = \"clipboard-read; clipboard-write\";\n appFrame = frame;\n frame.addEventListener(\"load\", () => sendFrameReadyMessages(frame));\n stage.replaceChildren(frame);\n }\n\n async function updateHostModelContext(data) {\n const params = {};\n if (Array.isArray(data && data.content)) params.content = data.content;\n if (data && data.structuredContent && typeof data.structuredContent === \"object\") {\n params.structuredContent = data.structuredContent;\n }\n await app.updateModelContext(params);\n }\n\n async function openHostLink(data) {\n const url = typeof (data && data.url) === \"string\" ? data.url : \"\";\n if (!url) return { isError: true };\n return await app.openLink({ url });\n }\n\n function respondToAppFrame(requestId, work) {\n if (!requestId) return;\n Promise.resolve(work)\n .then((result) => {\n sendToAppFrame({\n type: \"agentNative.mcpHost.response\",\n data: { requestId, ok: true, result }\n });\n })\n .catch((err) => {\n sendToAppFrame({\n type: \"agentNative.mcpHost.response\",\n data: {\n requestId,\n ok: false,\n error: err && err.message ? err.message : String(err)\n }\n });\n });\n }\n\n async function sendHostChat(chat) {\n if (!chat || chat.submit === false) return;\n const message = typeof chat.message === \"string\" ? chat.message : \"\";\n if (!message.trim()) return;\n const context = typeof chat.context === \"string\" ? chat.context : \"\";\n if (context.trim()) {\n try {\n await app.updateModelContext({\n content: [{ type: \"text\", text: context }]\n });\n } catch (err) {\n console.warn(\"[agent-native] MCP host rejected model context update\", err);\n }\n }\n try {\n const result = await app.sendMessage({\n role: \"user\",\n content: [{ type: \"text\", text: message }]\n });\n if (result && result.isError) {\n console.warn(\"[agent-native] MCP host rejected chat message\", result);\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP host chat bridge failed\", err);\n }\n }\n\n window.addEventListener(\"message\", (event) => {\n if (!appFrame || event.source !== appFrame.contentWindow) return;\n if (!event.data) return;\n const data = event.data.data || {};\n if (event.data.type === \"agentNative.submitChat\") {\n void sendHostChat(data);\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.updateModelContext\") {\n respondToAppFrame(data.requestId, updateHostModelContext(data));\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.openLink\") {\n respondToAppFrame(data.requestId, openHostLink(data));\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.requestDisplayMode\") {\n respondToAppFrame(data.requestId, requestHostDisplayMode(data.mode));\n }\n });\n\n async function launchEmbed() {\n if (!openUrl) {\n setMessage(\"Open link was not available.\");\n return;\n }\n if (!wantsEmbed()) {\n setMessage(\"Ready to open.\");\n return;\n }\n if (startedFor === openUrl) return;\n startedFor = openUrl;\n setMessage(\"Loading app\");\n try {\n const embedUrl = withChatBridgeParam(openUrl);\n const result = await app.callServerTool({\n name: startTool,\n arguments: {\n url: embedUrl,\n chrome: typeof toolInput.chrome === \"string\" ? toolInput.chrome : \"full\"\n }\n });\n const data = parseToolResult(result);\n if (!data.startUrl) {\n startedFor = \"\";\n setMessage(data.error || \"This app can be opened, but not embedded from this MCP server.\");\n return;\n }\n renderFrame(data.startUrl);\n } catch (err) {\n startedFor = \"\";\n setMessage(err && err.message ? err.message : \"Could not launch embedded app.\");\n }\n }\n\n function updateOpenButton() {\n openButton.disabled = !openUrl;\n openButton.onclick = () => {\n if (openUrl) void app.openLink({ url: openUrl });\n };\n }\n\n function updateTitle(data) {\n const label = data.label || data.app || data.view || body.dataset.appTitle || \"App\";\n titleEl.textContent = String(label);\n }\n\n app.ontoolinput = (params) => {\n toolInput = params.arguments || {};\n };\n app.ontoolresult = (params) => {\n const data = parseToolResult(params);\n openUrl = openLinkFrom(params, data);\n updateTitle(data);\n updateOpenButton();\n void launchEmbed();\n };\n app.onhostcontextchanged = () => {\n updateDisplayButton();\n sendHostContext();\n };\n await app.connect();\n updateDisplayButton();\n sendHostContext();\n </script>\n</body>\n</html>`,\n csp: {\n connectDomains: [\"https://esm.sh\"],\n resourceDomains: [\"https://esm.sh\"],\n frameDomains: [\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ],\n },\n prefersBorder: false,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"embed-app.js","sourceRoot":"","sources":["../../src/mcp/embed-app.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ActionMcpAppResourceConfig } from \"../action.js\";\nimport { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from \"../shared/embed-auth.js\";\n\nconst MCP_APP_IMPORT =\n \"https://esm.sh/@modelcontextprotocol/ext-apps@1.7.2/app-with-deps\";\n\nexport const MCP_APP_REQUEST_ORIGIN_CSP_SOURCE = \"$requestOrigin\";\nconst MCP_APP_WRAPPER_CHROME_HEIGHT = 44;\nexport const DEFAULT_MCP_APP_VIEWPORT_HEIGHT = 720;\nexport const DEFAULT_MCP_APP_SHELL_HEIGHT =\n DEFAULT_MCP_APP_VIEWPORT_HEIGHT + MCP_APP_WRAPPER_CHROME_HEIGHT;\n\nexport interface EmbedAppOptions {\n title?: string;\n description?: string;\n iframeTitle?: string;\n openLabel?: string;\n embedByDefault?: boolean;\n startToolName?: string;\n frameDomains?: string[];\n height?: number;\n}\n\nfunction attr(value: string | undefined): string {\n return String(value ?? \"\")\n .replace(/&/g, \"&\")\n .replace(/\"/g, \""\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\nexport function embedApp(\n options: EmbedAppOptions = {},\n): ActionMcpAppResourceConfig {\n const title = options.title ?? \"Open app\";\n const iframeTitle = options.iframeTitle ?? \"Agent Native app\";\n const openLabel = options.openLabel ?? \"Open in app\";\n const startToolName = options.startToolName ?? \"create_embed_session\";\n const embedByDefault = options.embedByDefault !== false;\n const height = Math.max(\n 320,\n Math.min(900, options.height ?? DEFAULT_MCP_APP_SHELL_HEIGHT),\n );\n const viewportHeight = height - MCP_APP_WRAPPER_CHROME_HEIGHT;\n\n return {\n title,\n ...(options.description ? { description: options.description } : {}),\n html: () => `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <style>\n :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: Canvas; color: CanvasText; }\n * { box-sizing: border-box; }\n body { margin: 0; }\n .shell { display: grid; gap: 8px; min-height: ${height}px; padding: 0; }\n .bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 36px; padding: 6px 8px; border-bottom: 1px solid color-mix(in srgb, CanvasText 12%, Canvas); }\n .title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 700; color: color-mix(in srgb, CanvasText 72%, Canvas); }\n .actions { display: flex; align-items: center; gap: 6px; }\n button { min-height: 28px; border: 1px solid color-mix(in srgb, CanvasText 14%, Canvas); border-radius: 7px; background: Canvas; color: CanvasText; cursor: pointer; font: inherit; font-size: 12px; font-weight: 700; padding: 0 9px; }\n button:disabled { opacity: .55; cursor: default; }\n .stage { position: relative; min-height: ${viewportHeight}px; }\n iframe { display: block; width: 100%; height: ${viewportHeight}px; border: 0; background: Canvas; }\n .message { display: grid; place-items: center; min-height: ${viewportHeight}px; padding: 18px; color: color-mix(in srgb, CanvasText 62%, Canvas); font-size: 13px; line-height: 1.45; text-align: center; }\n .fallback { display: grid; align-content: center; justify-items: center; gap: 12px; min-height: ${viewportHeight}px; padding: 24px; background: Canvas; color: CanvasText; text-align: center; }\n .fallback-title { max-width: 440px; font-size: 14px; font-weight: 700; }\n .fallback-copy { max-width: 520px; color: color-mix(in srgb, CanvasText 64%, Canvas); font-size: 13px; line-height: 1.45; }\n .fallback-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }\n .fallback-url { max-width: min(560px, 100%); overflow-wrap: anywhere; color: color-mix(in srgb, CanvasText 76%, Canvas); font-size: 12px; }\n </style>\n</head>\n<body\n data-app-title=\"${attr(title)}\"\n data-iframe-title=\"${attr(iframeTitle)}\"\n data-open-label=\"${attr(openLabel)}\"\n data-start-tool=\"${attr(startToolName)}\"\n data-embed-default=\"${embedByDefault ? \"1\" : \"0\"}\"\n>\n <main class=\"shell\">\n <div class=\"bar\">\n <div class=\"title\" data-title-label>${attr(title)}</div>\n <div class=\"actions\">\n <button type=\"button\" data-display hidden disabled>Fullscreen</button>\n <button type=\"button\" data-open disabled>${attr(openLabel)}</button>\n </div>\n </div>\n <section class=\"stage\" data-stage>\n <div class=\"message\">Preparing app</div>\n </section>\n </main>\n <script type=\"module\">\n const body = document.body;\n const stage = document.querySelector(\"[data-stage]\");\n const titleEl = document.querySelector(\"[data-title-label]\");\n const openButton = document.querySelector(\"[data-open]\");\n const displayButton = document.querySelector(\"[data-display]\");\n const startTool = body.dataset.startTool || \"create_embed_session\";\n const embedByDefault = body.dataset.embedDefault !== \"0\";\n const chatBridgeParam = ${JSON.stringify(MCP_APP_CHAT_BRIDGE_QUERY_PARAM)};\n const intrinsicHeight = ${height};\n let app = null;\n let openAiBridge = null;\n let toolInput = {};\n let openUrl = \"\";\n let startedFor = \"\";\n let appFrame = null;\n let appFrameReady = false;\n let appFrameReadyTimer = null;\n let appFrameLoadTimer = null;\n let lastFrameSrc = \"\";\n\n function esc(value) {\n return String(value ?? \"\")\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n }\n\n function parseJson(value, fallback) {\n if (value && typeof value === \"object\") return value;\n if (typeof value !== \"string\" || !value.trim()) return fallback;\n try { return JSON.parse(value); } catch { return fallback; }\n }\n\n function objectValue(value) {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value\n : {};\n }\n\n function parseToolResult(params) {\n if (!params) return {};\n if (params.result && typeof params.result === \"object\") {\n return parseToolResult(params.result);\n }\n if (params.toolResult && typeof params.toolResult === \"object\") {\n return parseToolResult(params.toolResult);\n }\n if (params.structuredContent && typeof params.structuredContent === \"object\") {\n return params.structuredContent;\n }\n const parts = Array.isArray(params.content) ? params.content : [];\n const textPart = parts.find((part) => part && part.type === \"text\" && typeof part.text === \"string\");\n return parseJson(textPart ? textPart.text : \"\", {});\n }\n\n function openLinkFrom(params, data) {\n const metaUrl = params && params._meta && params._meta[\"agent-native/openLink\"]\n ? params._meta[\"agent-native/openLink\"].webUrl\n : \"\";\n return metaUrl || data.url || data.deepLink || data.openUrl || \"\";\n }\n\n function hostState() {\n if (openAiBridge) {\n return {\n context: {\n displayMode: openAiBridge.displayMode,\n availableDisplayModes: typeof openAiBridge.requestDisplayMode === \"function\"\n ? [\"inline\", \"fullscreen\", \"pip\"]\n : [],\n maxHeight: openAiBridge.maxHeight,\n locale: openAiBridge.locale,\n theme: openAiBridge.theme,\n view: openAiBridge.view\n },\n capabilities: { openai: true },\n version: openAiBridge.userAgent\n };\n }\n return {\n context: app && app.getHostContext ? app.getHostContext() : undefined,\n capabilities: app && app.getHostCapabilities ? app.getHostCapabilities() : undefined,\n version: app && app.getHostVersion ? app.getHostVersion() : undefined\n };\n }\n\n function sendToAppFrame(message) {\n if (!appFrame || !appFrame.contentWindow) return;\n try { appFrame.contentWindow.postMessage(message, \"*\"); } catch {}\n }\n\n function sendHostContext() {\n sendToAppFrame({ type: \"agentNative.mcpHostContext\", data: hostState() });\n }\n\n function sendFrameReadyMessages(frame) {\n const originPayload = { type: \"agentNative.frameOrigin\", origin: window.location.origin };\n [0, 200, 500, 1500].forEach((delay) => {\n setTimeout(() => {\n try { frame.contentWindow && frame.contentWindow.postMessage(originPayload, \"*\"); } catch {}\n sendHostContext();\n }, delay);\n });\n }\n\n function withChatBridgeParam(value) {\n if (typeof value !== \"string\" || !value) return value;\n try {\n const base = \"http://agent-native.invalid\";\n const url = value.startsWith(\"/\") ? new URL(value, base) : new URL(value);\n url.searchParams.set(chatBridgeParam, \"1\");\n return value.startsWith(\"/\")\n ? url.pathname + url.search + url.hash\n : url.toString();\n } catch {\n return value;\n }\n }\n\n function wantsEmbed() {\n if (toolInput.embed === false || toolInput.embed === \"false\") return false;\n if (embedByDefault) return true;\n return toolInput.embed === true || toolInput.embed === \"true\";\n }\n\n function supportedDisplayMode(mode) {\n if (openAiBridge && typeof openAiBridge.requestDisplayMode === \"function\") {\n return mode === \"inline\" || mode === \"fullscreen\" || mode === \"pip\";\n }\n const modes = hostState().context && hostState().context.availableDisplayModes;\n return Array.isArray(modes) && modes.includes(mode);\n }\n\n async function requestHostDisplayMode(mode) {\n let result;\n if (openAiBridge && typeof openAiBridge.requestDisplayMode === \"function\") {\n result = await openAiBridge.requestDisplayMode({ mode });\n } else {\n if (!app || typeof app.requestDisplayMode !== \"function\") {\n throw new Error(\"Display mode changes are not available in this host.\");\n }\n result = await app.requestDisplayMode({ mode });\n }\n updateDisplayButton();\n sendHostContext();\n return result;\n }\n\n function updateDisplayButton() {\n const context = hostState().context || {};\n const nextMode = context.displayMode === \"fullscreen\" ? \"inline\" : \"fullscreen\";\n const supported = supportedDisplayMode(nextMode);\n displayButton.hidden = !supported;\n displayButton.disabled = !supported;\n displayButton.textContent = nextMode === \"fullscreen\" ? \"Fullscreen\" : \"Inline\";\n displayButton.onclick = () => {\n if (!supportedDisplayMode(nextMode)) return;\n void requestHostDisplayMode(nextMode).catch((err) => {\n console.warn(\"[agent-native] MCP host rejected display mode request\", err);\n });\n };\n }\n\n function setMessage(message) {\n stage.innerHTML = '<div class=\"message\">' + esc(message) + '</div>';\n }\n\n function clearFrameReadyTimer() {\n if (!appFrameReadyTimer) return;\n clearTimeout(appFrameReadyTimer);\n appFrameReadyTimer = null;\n }\n\n function clearFrameLoadTimer() {\n if (!appFrameLoadTimer) return;\n clearTimeout(appFrameLoadTimer);\n appFrameLoadTimer = null;\n }\n\n function startFrameReadyTimer(frame) {\n clearFrameReadyTimer();\n appFrameReadyTimer = setTimeout(() => {\n if (!appFrameReady && appFrame === frame) renderFrameFallback();\n }, 7000);\n }\n\n function renderFrameFallback() {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n appFrame = null;\n stage.innerHTML =\n '<div class=\"fallback\">' +\n '<div class=\"fallback-title\">Open this app in its own tab</div>' +\n '<div class=\"fallback-copy\">This chat host did not allow the embedded app frame to load inline. You can still open the same app route through the host or use the URL below.</div>' +\n '<div class=\"fallback-actions\">' +\n '<button type=\"button\" data-fallback-open>Open app</button>' +\n '<button type=\"button\" data-fallback-retry>Try inline again</button>' +\n '</div>' +\n (openUrl ? '<a class=\"fallback-url\" href=\"' + esc(openUrl) + '\" target=\"_blank\" rel=\"noreferrer\">' + esc(openUrl) + '</a>' : '') +\n '</div>';\n const fallbackOpen = stage.querySelector(\"[data-fallback-open]\");\n const fallbackRetry = stage.querySelector(\"[data-fallback-retry]\");\n if (fallbackOpen) {\n fallbackOpen.disabled = !openUrl;\n fallbackOpen.onclick = () => {\n if (openUrl) void openFallbackExternal();\n };\n }\n if (fallbackRetry) {\n fallbackRetry.disabled = !lastFrameSrc;\n fallbackRetry.onclick = () => {\n if (lastFrameSrc) renderFrame(lastFrameSrc);\n };\n }\n }\n\n async function openFallbackExternal() {\n let url = openUrl;\n try {\n const embedUrl = withChatBridgeParam(openUrl);\n const result = await callEmbedSessionTool({\n url: embedUrl,\n chrome: typeof toolInput.chrome === \"string\" ? toolInput.chrome : \"full\"\n });\n const data = parseToolResult(result);\n if (typeof data.startUrl === \"string\" && data.startUrl) {\n url = data.startUrl;\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP fallback could not mint a fresh app session\", err);\n }\n await openHostLink({ url });\n }\n\n function renderFrame(src) {\n clearFrameReadyTimer();\n clearFrameLoadTimer();\n const frame = document.createElement(\"iframe\");\n frame.title = body.dataset.iframeTitle || \"Agent Native app\";\n frame.src = src;\n frame.allow = \"clipboard-read; clipboard-write\";\n appFrame = frame;\n appFrameReady = false;\n lastFrameSrc = src;\n frame.addEventListener(\"load\", () => {\n if (appFrame !== frame) return;\n clearFrameLoadTimer();\n sendFrameReadyMessages(frame);\n startFrameReadyTimer(frame);\n });\n stage.replaceChildren(frame);\n notifyHostHeight();\n appFrameLoadTimer = setTimeout(() => {\n if (!appFrameReady && appFrame === frame) renderFrameFallback();\n }, 30000);\n }\n\n async function updateHostModelContext(data) {\n const params = {};\n if (Array.isArray(data && data.content)) params.content = data.content;\n if (data && data.structuredContent && typeof data.structuredContent === \"object\") {\n params.structuredContent = data.structuredContent;\n }\n if (openAiBridge && typeof openAiBridge.setWidgetState === \"function\") {\n openAiBridge.setWidgetState({\n ...objectValue(openAiBridge.widgetState),\n agentNativeModelContext: params\n });\n return { ok: true };\n }\n if (!app || typeof app.updateModelContext !== \"function\") return { ok: false };\n await app.updateModelContext(params);\n return { ok: true };\n }\n\n async function openHostLink(data) {\n const url = typeof (data && data.url) === \"string\" ? data.url : \"\";\n if (!url) return { isError: true };\n if (openAiBridge && typeof openAiBridge.openExternal === \"function\") {\n return await openAiBridge.openExternal({ href: url, redirectUrl: false });\n }\n if (app && typeof app.openLink === \"function\") {\n return await app.openLink({ url });\n }\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n return { ok: true };\n }\n\n function notifyHostHeight() {\n if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== \"function\") {\n return;\n }\n try {\n openAiBridge.notifyIntrinsicHeight({ height: intrinsicHeight });\n } catch (err) {\n console.warn(\"[agent-native] ChatGPT rejected intrinsic height update\", err);\n }\n }\n\n function respondToAppFrame(requestId, work) {\n if (!requestId) return;\n Promise.resolve(work)\n .then((result) => {\n sendToAppFrame({\n type: \"agentNative.mcpHost.response\",\n data: { requestId, ok: true, result }\n });\n })\n .catch((err) => {\n sendToAppFrame({\n type: \"agentNative.mcpHost.response\",\n data: {\n requestId,\n ok: false,\n error: err && err.message ? err.message : String(err)\n }\n });\n });\n }\n\n async function sendHostChat(chat) {\n if (!chat || chat.submit === false) return;\n const message = typeof chat.message === \"string\" ? chat.message : \"\";\n if (!message.trim()) return;\n const context = typeof chat.context === \"string\" ? chat.context : \"\";\n if (context.trim()) {\n try {\n if (openAiBridge && typeof openAiBridge.setWidgetState === \"function\") {\n openAiBridge.setWidgetState({\n ...objectValue(openAiBridge.widgetState),\n agentNativeChatContext: context\n });\n } else if (app && typeof app.updateModelContext === \"function\") {\n await app.updateModelContext({\n content: [{ type: \"text\", text: context }]\n });\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP host rejected model context update\", err);\n }\n }\n try {\n if (openAiBridge && typeof openAiBridge.sendFollowUpMessage === \"function\") {\n await openAiBridge.sendFollowUpMessage({\n prompt: context.trim() ? context.trim() + \"\\\\n\\\\n\" + message : message,\n scrollToBottom: true\n });\n return;\n }\n if (!app || typeof app.sendMessage !== \"function\") return;\n const result = await app.sendMessage({\n role: \"user\",\n content: [{ type: \"text\", text: message }]\n });\n if (result && result.isError) {\n console.warn(\"[agent-native] MCP host rejected chat message\", result);\n }\n } catch (err) {\n console.warn(\"[agent-native] MCP host chat bridge failed\", err);\n }\n }\n\n window.addEventListener(\"message\", (event) => {\n if (!appFrame || event.source !== appFrame.contentWindow) return;\n if (!event.data) return;\n const data = event.data.data || {};\n if (event.data.type === \"agentNative.embeddedAppReady\") {\n appFrameReady = true;\n clearFrameLoadTimer();\n clearFrameReadyTimer();\n return;\n }\n if (event.data.type === \"agentNative.submitChat\") {\n void sendHostChat(data);\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.updateModelContext\") {\n respondToAppFrame(data.requestId, updateHostModelContext(data));\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.openLink\") {\n respondToAppFrame(data.requestId, openHostLink(data));\n return;\n }\n if (event.data.type === \"agentNative.mcpHost.requestDisplayMode\") {\n respondToAppFrame(data.requestId, requestHostDisplayMode(data.mode));\n }\n });\n\n async function launchEmbed() {\n if (!openUrl) {\n setMessage(\"Open link was not available.\");\n return;\n }\n if (!wantsEmbed()) {\n setMessage(\"Ready to open.\");\n return;\n }\n if (startedFor === openUrl) return;\n startedFor = openUrl;\n setMessage(\"Loading app\");\n try {\n const embedUrl = withChatBridgeParam(openUrl);\n const result = await callEmbedSessionTool({\n url: embedUrl,\n chrome: typeof toolInput.chrome === \"string\" ? toolInput.chrome : \"full\"\n });\n const data = parseToolResult(result);\n if (!data.startUrl) {\n startedFor = \"\";\n setMessage(data.error || \"This app can be opened, but not embedded from this MCP server.\");\n return;\n }\n renderFrame(data.startUrl);\n } catch (err) {\n startedFor = \"\";\n setMessage(err && err.message ? err.message : \"Could not launch embedded app.\");\n }\n }\n\n async function callEmbedSessionTool(args) {\n if (openAiBridge && typeof openAiBridge.callTool === \"function\") {\n return await openAiBridge.callTool(startTool, args);\n }\n if (!app || typeof app.callServerTool !== \"function\") {\n throw new Error(\"Host tool calls are not available.\");\n }\n return await app.callServerTool({ name: startTool, arguments: args });\n }\n\n function updateHostOpenInAppUrl() {\n if (!openAiBridge || !openUrl || typeof openAiBridge.setOpenInAppUrl !== \"function\") {\n return;\n }\n try {\n openAiBridge.setOpenInAppUrl({ href: openUrl });\n } catch (err) {\n console.warn(\"[agent-native] ChatGPT rejected open-in-app URL\", err);\n }\n }\n\n function updateOpenButton() {\n openButton.disabled = !openUrl;\n openButton.onclick = () => {\n if (openUrl) void openHostLink({ url: openUrl });\n };\n updateHostOpenInAppUrl();\n }\n\n function updateTitle(data) {\n const label = data.label || data.app || data.view || body.dataset.appTitle || \"App\";\n titleEl.textContent = String(label);\n }\n\n function readOpenAiBridge() {\n return window.openai && typeof window.openai === \"object\"\n ? window.openai\n : null;\n }\n\n function openAiToolResultParams(bridge) {\n const params = {};\n if (bridge && bridge.toolOutput !== undefined) {\n if (bridge.toolOutput && typeof bridge.toolOutput === \"object\") {\n params.structuredContent = bridge.toolOutput;\n } else {\n params.content = [{ type: \"text\", text: String(bridge.toolOutput) }];\n }\n }\n if (bridge && bridge.toolResponseMetadata && typeof bridge.toolResponseMetadata === \"object\") {\n params._meta = bridge.toolResponseMetadata;\n }\n return params;\n }\n\n function syncOpenAiBridge(bridge) {\n if (!bridge) return false;\n openAiBridge = bridge;\n toolInput = objectValue(bridge.toolInput);\n const params = openAiToolResultParams(bridge);\n const data = parseToolResult(params);\n openUrl = openLinkFrom(params, data);\n updateTitle(data);\n updateOpenButton();\n updateDisplayButton();\n notifyHostHeight();\n sendHostContext();\n if (openUrl) {\n void launchEmbed();\n } else if (!appFrame) {\n setMessage(\"Waiting for app result\");\n }\n return true;\n }\n\n function waitForOpenAiBridge() {\n const existing = readOpenAiBridge();\n if (existing) return Promise.resolve(existing);\n return new Promise((resolve) => {\n let settled = false;\n const finish = (bridge) => {\n if (settled) return;\n settled = true;\n window.removeEventListener(\"openai:set_globals\", onGlobals);\n clearTimeout(timer);\n resolve(bridge || readOpenAiBridge());\n };\n const onGlobals = () => finish(readOpenAiBridge());\n const timer = setTimeout(() => finish(null), 200);\n window.addEventListener(\"openai:set_globals\", onGlobals, { passive: true });\n });\n }\n\n window.addEventListener(\"openai:set_globals\", () => {\n const bridge = readOpenAiBridge();\n if (bridge && (!appFrame || openAiBridge)) syncOpenAiBridge(bridge);\n }, { passive: true });\n\n async function startMcpAppsBridge() {\n const { App } = await import(\"${MCP_APP_IMPORT}\");\n app = new App({ name: \"Agent Native Embed\", version: \"1.0.0\" }, {});\n app.ontoolinput = (params) => {\n toolInput = params.arguments || {};\n };\n app.ontoolresult = (params) => {\n const data = parseToolResult(params);\n openUrl = openLinkFrom(params, data);\n updateTitle(data);\n updateOpenButton();\n void launchEmbed();\n };\n app.onhostcontextchanged = () => {\n updateDisplayButton();\n sendHostContext();\n };\n await app.connect();\n updateDisplayButton();\n sendHostContext();\n }\n\n const initialOpenAiBridge = await waitForOpenAiBridge();\n if (!syncOpenAiBridge(initialOpenAiBridge)) {\n await startMcpAppsBridge();\n }\n </script>\n</body>\n</html>`,\n csp: {\n connectDomains: [\"https://esm.sh\"],\n resourceDomains: [\n \"https://esm.sh\",\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ],\n frameDomains: [\n MCP_APP_REQUEST_ORIGIN_CSP_SOURCE,\n ...(options.frameDomains ?? []),\n ],\n },\n prefersBorder: false,\n };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/server/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { defineEventHandler, getMethod, getQuery, getRequestIP, setResponseHeader, setResponseStatus, getCookie, setCookie, deleteCookie, getHeader, } from "h3";
|
|
3
3
|
import { EMBED_START_PATH } from "../shared/embed-auth.js";
|
|
4
|
+
import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
|
|
4
5
|
import { resolveEmbedSessionFromRequest } from "./embed-session.js";
|
|
5
6
|
// In h3 v2, `event.req` IS the web Request — but in Nitro's dev server (srvx
|
|
6
7
|
// runtime), event.url and event.req share the same underlying URL object.
|
|
@@ -764,7 +765,14 @@ function applyCorsHeaders(event) {
|
|
|
764
765
|
setResponseHeader(event, "Vary", "Origin");
|
|
765
766
|
setResponseHeader(event, "Access-Control-Allow-Credentials", "true");
|
|
766
767
|
setResponseHeader(event, "Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS");
|
|
767
|
-
setResponseHeader(event, "Access-Control-Allow-Headers",
|
|
768
|
+
setResponseHeader(event, "Access-Control-Allow-Headers", [
|
|
769
|
+
"Content-Type",
|
|
770
|
+
"Authorization",
|
|
771
|
+
"X-Requested-With",
|
|
772
|
+
"X-Request-Source",
|
|
773
|
+
"X-Agent-Native-CSRF",
|
|
774
|
+
EMBED_TARGET_HEADER,
|
|
775
|
+
].join(","));
|
|
768
776
|
return { hasOrigin: true, allowed: true };
|
|
769
777
|
}
|
|
770
778
|
function createAuthCorsHandler() {
|