@mario.andreschak/mcp-browser 3.46.1 → 3.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/resources.js CHANGED
@@ -67,296 +67,296 @@ export function renderBrowserAppHtml(gateway) {
67
67
  origin: gateway?.origin ?? '',
68
68
  token: gateway?.token ?? '',
69
69
  }).replace(/</g, '\\u003c');
70
- return `<!doctype html>
71
- <html>
72
- <head>
73
- <meta charset="utf-8" />
74
- <style>
75
- :root{--bg:#fff;--fg:#202124;--muted:#5f6368;--border:#dadce0;--danger:#c5221f}
76
- html[data-theme="dark"]{--bg:#202124;--fg:#e8eaed;--muted:#9aa0a6;--border:#3c4043;--danger:#f28b82}
77
- *{box-sizing:border-box}html,body{height:100%;margin:0;overflow:hidden}
78
- body{background:var(--bg);color:var(--fg);font:13px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
79
- #frame{width:100%;height:100%;border:0;display:block;background:var(--bg)}
80
- #fallback{display:none;height:100%;flex-direction:column}
81
- #fallback.show{display:flex}
82
- #frame.hide{display:none}
83
- .bar{display:flex;gap:6px;align-items:center;padding:6px;border-bottom:1px solid var(--border)}
84
- .bar input{flex:1;min-width:0;font:inherit;padding:6px 10px;border:1px solid var(--border);border-radius:14px;background:var(--bg);color:inherit}
85
- .bar button{font:inherit;padding:6px 10px;border:1px solid var(--border);border-radius:14px;background:var(--bg);color:inherit;cursor:pointer}
86
- #shot{flex:1;min-height:0;display:grid;place-items:center;overflow:hidden}
87
- #shot img{max-width:100%;max-height:100%;display:block}
88
- #note{padding:6px 10px;color:var(--muted);font-size:12px}
89
- #note.error{color:var(--danger)}
90
- </style>
91
- </head>
92
- <body>
93
- <iframe id="frame" title="Live browser view" allow="autoplay; clipboard-read; clipboard-write; fullscreen" referrerpolicy="no-referrer"></iframe>
94
- <div id="fallback">
95
- <div class="bar">
96
- <button id="fbBack" title="Back">&#8592;</button>
97
- <button id="fbForward" title="Forward">&#8594;</button>
98
- <button id="fbReload" title="Reload">&#8635;</button>
99
- <input id="fbUrl" type="text" placeholder="https://example.com" />
100
- <button id="fbGo">Go</button>
101
- </div>
102
- <div id="shot"></div>
103
- <div id="note"></div>
104
- </div>
105
- <script>
106
- (function(){
107
- "use strict";
108
- var GATEWAY = ${config};
109
- var parentWin = window.parent, idc = 1, pending = {}, sessionId = "", sessionKey = "", starting = false, ownsSession = false;
110
- var frame = document.getElementById("frame");
111
- var fallback = document.getElementById("fallback");
112
- var shot = document.getElementById("shot");
113
- var note = document.getElementById("note");
114
- var fbUrl = document.getElementById("fbUrl");
115
- var theme = "light", pollTimer = 0, usingFallback = false;
116
- var standaloneTimer = 0, toolResultTimer = 0, pendingToolInput = null;
117
- // null = host did not report its sandbox CSP grant (unknown policy);
118
- // [] = host reported a grant that excludes the gateway (denied).
119
- var grantedFrameDomains = null, viewReady = false, readyTimer = 0;
120
-
121
- function post(m){ parentWin.postMessage(m, "*"); }
122
- function rpc(method, params){
123
- return new Promise(function(resolve, reject){
124
- var id = idc++;
125
- pending[id] = { resolve: resolve, reject: reject };
126
- post({ jsonrpc: "2.0", id: id, method: method, params: params || {} });
127
- });
128
- }
129
- function notify(method, params){ post({ jsonrpc: "2.0", method: method, params: params || {} }); }
130
- function errorText(e){ return e && e.message ? e.message : String(e); }
131
- function payload(result){
132
- if (result && result.structuredContent) return result.structuredContent;
133
- try {
134
- var t = result && result.content && result.content[0] && result.content[0].text;
135
- return t ? JSON.parse(t) : {};
136
- } catch (e) { return {}; }
137
- }
138
- function failMessage(result){
139
- var p = payload(result);
140
- return p && p.error && p.error.message ? p.error.message : "Browser operation failed";
141
- }
142
- async function call(name, args){
143
- var result = await rpc("tools/call", { name: name, arguments: args || {} });
144
- if (result && result.isError) throw new Error(failMessage(result));
145
- return result;
146
- }
147
- function toView(message){
148
- if (usingFallback || !frame.contentWindow) return;
149
- try { frame.contentWindow.postMessage(Object.assign({ source: "flujo-browser-shell" }, message), "*"); } catch (e) {}
150
- }
151
- function setNote(text, isError){
152
- note.textContent = text || "";
153
- note.className = isError ? "error" : "";
154
- }
155
-
156
- /* ---------- live view ---------- */
157
- // Whether the host's effective frame-src grant covers the gateway origin.
158
- // Grants may be exact origins, the loopback port-wildcard form
159
- // (http://127.0.0.1:*), or the allow-all escape hatch.
160
- function frameGrantCovers(origin){
161
- if (grantedFrameDomains === null) return true; /* unknown policy: probe, the ready watchdog still guards */
162
- var o = String(origin || "").toLowerCase();
163
- for (var i = 0; i < grantedFrameDomains.length; i++){
164
- var d = String(grantedFrameDomains[i] || "").toLowerCase();
165
- if (d === "*" || d === o) return true;
166
- if (d.slice(-2) === ":*" && o.indexOf(d.slice(0, -1)) === 0) return true;
167
- }
168
- return false;
169
- }
170
- function mountLiveView(){
171
- frame.src = GATEWAY.origin + "/view?s=" + encodeURIComponent(sessionId) + "&t=" + encodeURIComponent(GATEWAY.token) + "&k=" + encodeURIComponent(sessionKey);
172
- // A CSP-blocked or unreachable iframe fires neither load nor error, so a
173
- // silent live view must degrade to screenshots on its own.
174
- clearTimeout(readyTimer);
175
- readyTimer = setTimeout(function(){
176
- if (!viewReady) useFallback("The live view is not reachable from this browser; showing periodic screenshots.");
177
- }, 8000);
178
- }
179
-
180
- /* ---------- screenshot fallback ---------- */
181
- function useFallback(reason){
182
- if (usingFallback) return;
183
- usingFallback = true;
184
- clearTimeout(readyTimer);
185
- frame.classList.add("hide");
186
- fallback.classList.add("show");
187
- setNote(reason || "Live streaming is unavailable; showing periodic screenshots.", true);
188
- pollOnce();
189
- }
190
- async function pollOnce(){
191
- if (!usingFallback || !sessionId) return;
192
- try {
193
- var result = await call("browser_screenshot", { sessionId: sessionId });
194
- var items = (result && result.content) || [];
195
- for (var i = 0; i < items.length; i++){
196
- if (items[i].type === "image" && items[i].data){
197
- shot.innerHTML = "";
198
- var img = document.createElement("img");
199
- img.src = "data:" + (items[i].mimeType || "image/png") + ";base64," + items[i].data;
200
- img.alt = "Browser viewport";
201
- shot.appendChild(img);
202
- break;
203
- }
204
- }
205
- var state = payload(result);
206
- if (state.url && document.activeElement !== fbUrl) fbUrl.value = state.url;
207
- setNote("Screenshot fallback \\u2014 updated " + new Date().toLocaleTimeString());
208
- } catch (e) {
209
- setNote(errorText(e), true);
210
- }
211
- clearTimeout(pollTimer);
212
- pollTimer = setTimeout(pollOnce, 2000);
213
- }
214
- function fallbackAction(kind, url){
215
- return async function(){
216
- try {
217
- if (kind === "navigate") await call("browser_navigate", { sessionId: sessionId, url: url() });
218
- else await call("browser_" + kind, { sessionId: sessionId });
219
- pollOnce();
220
- } catch (e) { setNote(errorText(e), true); }
221
- };
222
- }
223
- document.getElementById("fbBack").onclick = fallbackAction("back");
224
- document.getElementById("fbForward").onclick = fallbackAction("forward");
225
- document.getElementById("fbReload").onclick = fallbackAction("reload");
226
- document.getElementById("fbGo").onclick = fallbackAction("navigate", function(){ return fbUrl.value.trim(); });
227
- fbUrl.onkeydown = function(e){ if (e.key === "Enter"){ e.preventDefault(); document.getElementById("fbGo").click(); } };
228
-
229
- /* ---------- session ---------- */
230
- async function start(initialUrl, requestedId, owned){
231
- if (starting) return;
232
- starting = true;
233
- ownsSession = !!owned;
234
- try {
235
- var args = {};
236
- if (initialUrl) args.url = initialUrl;
237
- if (requestedId) args.sessionId = requestedId;
238
- var result = await call("browser_open", args);
239
- var state = payload(result);
240
- sessionId = state.sessionId || "";
241
- sessionKey = result && result._meta && result._meta.flujo && result._meta.flujo.gatewaySessionToken || "";
242
- if (!sessionId || !sessionKey) throw new Error("The browser session handshake did not report its private capability.");
243
- if (!GATEWAY.origin || !GATEWAY.token) useFallback("The live stream gateway is disabled, so this view falls back to screenshots.");
244
- else if (!frameGrantCovers(GATEWAY.origin)) useFallback("This deployment cannot frame the local live-view gateway; showing periodic screenshots.");
245
- else mountLiveView();
246
- } catch (e) {
247
- ownsSession = false;
248
- starting = false;
249
- useFallback(errorText(e));
250
- }
251
- }
252
-
253
- async function teardownResource(requestId){
254
- clearTimeout(pollTimer);
255
- clearTimeout(readyTimer);
256
- clearTimeout(standaloneTimer);
257
- clearTimeout(toolResultTimer);
258
- var ownedId = ownsSession ? sessionId : "";
259
- ownsSession = false;
260
- if (ownedId){
261
- try { await call("browser_close", { sessionId: ownedId }); } catch (e) {}
262
- }
263
- post({ jsonrpc: "2.0", id: requestId, result: {} });
264
- }
265
-
266
- /* ---------- commands relayed from the framed view ---------- */
267
- async function runViewCommand(data){
268
- try {
269
- if (data.type === "navigate" && data.url) await call("browser_navigate", { sessionId: sessionId, url: data.url });
270
- else if (data.type === "back") await call("browser_back", { sessionId: sessionId });
271
- else if (data.type === "forward") await call("browser_forward", { sessionId: sessionId });
272
- else if (data.type === "reload") await call("browser_reload", { sessionId: sessionId });
273
- else if (data.type === "close"){
274
- await call("browser_close", { sessionId: sessionId });
275
- notify("ui/notifications/request-teardown", {});
276
- } else if (data.type === "fullscreen"){
277
- await rpc("ui/request-display-mode", { mode: "fullscreen" });
278
- }
279
- } catch (e) {
280
- toView({ type: "error", message: errorText(e) });
281
- } finally {
282
- toView({ type: "loading", value: false });
283
- }
284
- }
285
-
286
- window.addEventListener("message", function(event){
287
- var m = event.data;
288
- if (m && m.source === "flujo-browser-view"){
289
- if (event.source !== frame.contentWindow) return;
290
- if (m.type === "ready"){ viewReady = true; clearTimeout(readyTimer); toView({ type: "theme", theme: theme }); }
291
- else void runViewCommand(m);
292
- return;
293
- }
294
- if (!m || m.jsonrpc !== "2.0") return;
295
- if (m.id !== undefined && (m.result !== undefined || m.error !== undefined)){
296
- var p = pending[m.id];
297
- if (!p) return;
298
- delete pending[m.id];
299
- if (m.error) p.reject(new Error(m.error.message || "RPC error"));
300
- else p.resolve(m.result);
301
- return;
302
- }
303
- if (m.method === "ui/notifications/host-context-changed"){
304
- theme = (m.params && m.params.theme) === "dark" ? "dark" : "light";
305
- document.documentElement.setAttribute("data-theme", theme);
306
- toView({ type: "theme", theme: theme });
307
- return;
308
- }
309
- if (m.method === "ui/notifications/tool-input"){
310
- var a = (m.params && m.params.arguments) || {};
311
- pendingToolInput = a;
312
- clearTimeout(standaloneTimer);
313
- if (!starting && typeof a.sessionId === "string" && a.sessionId){
314
- void start(typeof a.url === "string" ? a.url : "", a.sessionId, false);
315
- } else if (!starting){
316
- // Tool results carry the authoritative sessionId. Waiting for it avoids
317
- // opening a second page after a no-id browser_open created its session.
318
- clearTimeout(toolResultTimer);
319
- toolResultTimer = setTimeout(function(){
320
- if (!starting) void start(typeof a.url === "string" ? a.url : "", "", true);
321
- }, 1000);
322
- }
323
- return;
324
- }
325
- if (m.method === "ui/notifications/tool-result"){
326
- var toolState = payload(m.params);
327
- var toolSessionId = toolState && typeof toolState.sessionId === "string" ? toolState.sessionId : "";
328
- if (!starting && toolSessionId){
329
- clearTimeout(toolResultTimer);
330
- var input = pendingToolInput || {};
331
- void start(typeof input.url === "string" ? input.url : "", toolSessionId, false);
332
- }
333
- return;
334
- }
335
- if (m.method === "ping" && m.id !== undefined){ post({ jsonrpc: "2.0", id: m.id, result: {} }); return; }
336
- if (m.method === "ui/resource-teardown" && m.id !== undefined){
337
- void teardownResource(m.id);
338
- return;
339
- }
340
- });
341
-
342
- rpc("ui/initialize", {
343
- appInfo: { name: "flujo-browser-view", version: "3.0.0" },
344
- appCapabilities: { availableDisplayModes: ["inline", "fullscreen", "pip"] },
345
- protocolVersion: "${MCP_APPS_PROTOCOL_VERSION}"
346
- }).then(function(result){
347
- var sandboxCaps = result && result.capabilities && result.capabilities.sandbox;
348
- if (sandboxCaps) grantedFrameDomains = (sandboxCaps.csp && sandboxCaps.csp.frameDomains) || [];
349
- theme = (result && result.hostContext && result.hostContext.theme) === "dark" ? "dark" : "light";
350
- document.documentElement.setAttribute("data-theme", theme);
351
- notify("ui/notifications/initialized", {});
352
- notify("ui/notifications/size-changed", { width: 0, height: 640 });
353
- standaloneTimer = setTimeout(function(){ if (!starting) void start("", "", true); }, 300);
354
- }).catch(function(e){
355
- useFallback("App initialization failed: " + errorText(e));
356
- });
357
- })();
358
- </script>
359
- </body>
70
+ return `<!doctype html>
71
+ <html>
72
+ <head>
73
+ <meta charset="utf-8" />
74
+ <style>
75
+ :root{--bg:#fff;--fg:#202124;--muted:#5f6368;--border:#dadce0;--danger:#c5221f}
76
+ html[data-theme="dark"]{--bg:#202124;--fg:#e8eaed;--muted:#9aa0a6;--border:#3c4043;--danger:#f28b82}
77
+ *{box-sizing:border-box}html,body{height:100%;margin:0;overflow:hidden}
78
+ body{background:var(--bg);color:var(--fg);font:13px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
79
+ #frame{width:100%;height:100%;border:0;display:block;background:var(--bg)}
80
+ #fallback{display:none;height:100%;flex-direction:column}
81
+ #fallback.show{display:flex}
82
+ #frame.hide{display:none}
83
+ .bar{display:flex;gap:6px;align-items:center;padding:6px;border-bottom:1px solid var(--border)}
84
+ .bar input{flex:1;min-width:0;font:inherit;padding:6px 10px;border:1px solid var(--border);border-radius:14px;background:var(--bg);color:inherit}
85
+ .bar button{font:inherit;padding:6px 10px;border:1px solid var(--border);border-radius:14px;background:var(--bg);color:inherit;cursor:pointer}
86
+ #shot{flex:1;min-height:0;display:grid;place-items:center;overflow:hidden}
87
+ #shot img{max-width:100%;max-height:100%;display:block}
88
+ #note{padding:6px 10px;color:var(--muted);font-size:12px}
89
+ #note.error{color:var(--danger)}
90
+ </style>
91
+ </head>
92
+ <body>
93
+ <iframe id="frame" title="Live browser view" allow="autoplay; clipboard-read; clipboard-write; fullscreen" referrerpolicy="no-referrer"></iframe>
94
+ <div id="fallback">
95
+ <div class="bar">
96
+ <button id="fbBack" title="Back">&#8592;</button>
97
+ <button id="fbForward" title="Forward">&#8594;</button>
98
+ <button id="fbReload" title="Reload">&#8635;</button>
99
+ <input id="fbUrl" type="text" placeholder="https://example.com" />
100
+ <button id="fbGo">Go</button>
101
+ </div>
102
+ <div id="shot"></div>
103
+ <div id="note"></div>
104
+ </div>
105
+ <script>
106
+ (function(){
107
+ "use strict";
108
+ var GATEWAY = ${config};
109
+ var parentWin = window.parent, idc = 1, pending = {}, sessionId = "", sessionKey = "", starting = false, ownsSession = false;
110
+ var frame = document.getElementById("frame");
111
+ var fallback = document.getElementById("fallback");
112
+ var shot = document.getElementById("shot");
113
+ var note = document.getElementById("note");
114
+ var fbUrl = document.getElementById("fbUrl");
115
+ var theme = "light", pollTimer = 0, usingFallback = false;
116
+ var standaloneTimer = 0, toolResultTimer = 0, pendingToolInput = null;
117
+ // null = host did not report its sandbox CSP grant (unknown policy);
118
+ // [] = host reported a grant that excludes the gateway (denied).
119
+ var grantedFrameDomains = null, viewReady = false, readyTimer = 0;
120
+
121
+ function post(m){ parentWin.postMessage(m, "*"); }
122
+ function rpc(method, params){
123
+ return new Promise(function(resolve, reject){
124
+ var id = idc++;
125
+ pending[id] = { resolve: resolve, reject: reject };
126
+ post({ jsonrpc: "2.0", id: id, method: method, params: params || {} });
127
+ });
128
+ }
129
+ function notify(method, params){ post({ jsonrpc: "2.0", method: method, params: params || {} }); }
130
+ function errorText(e){ return e && e.message ? e.message : String(e); }
131
+ function payload(result){
132
+ if (result && result.structuredContent) return result.structuredContent;
133
+ try {
134
+ var t = result && result.content && result.content[0] && result.content[0].text;
135
+ return t ? JSON.parse(t) : {};
136
+ } catch (e) { return {}; }
137
+ }
138
+ function failMessage(result){
139
+ var p = payload(result);
140
+ return p && p.error && p.error.message ? p.error.message : "Browser operation failed";
141
+ }
142
+ async function call(name, args){
143
+ var result = await rpc("tools/call", { name: name, arguments: args || {} });
144
+ if (result && result.isError) throw new Error(failMessage(result));
145
+ return result;
146
+ }
147
+ function toView(message){
148
+ if (usingFallback || !frame.contentWindow) return;
149
+ try { frame.contentWindow.postMessage(Object.assign({ source: "flujo-browser-shell" }, message), "*"); } catch (e) {}
150
+ }
151
+ function setNote(text, isError){
152
+ note.textContent = text || "";
153
+ note.className = isError ? "error" : "";
154
+ }
155
+
156
+ /* ---------- live view ---------- */
157
+ // Whether the host's effective frame-src grant covers the gateway origin.
158
+ // Grants may be exact origins, the loopback port-wildcard form
159
+ // (http://127.0.0.1:*), or the allow-all escape hatch.
160
+ function frameGrantCovers(origin){
161
+ if (grantedFrameDomains === null) return true; /* unknown policy: probe, the ready watchdog still guards */
162
+ var o = String(origin || "").toLowerCase();
163
+ for (var i = 0; i < grantedFrameDomains.length; i++){
164
+ var d = String(grantedFrameDomains[i] || "").toLowerCase();
165
+ if (d === "*" || d === o) return true;
166
+ if (d.slice(-2) === ":*" && o.indexOf(d.slice(0, -1)) === 0) return true;
167
+ }
168
+ return false;
169
+ }
170
+ function mountLiveView(){
171
+ frame.src = GATEWAY.origin + "/view?s=" + encodeURIComponent(sessionId) + "&t=" + encodeURIComponent(GATEWAY.token) + "&k=" + encodeURIComponent(sessionKey);
172
+ // A CSP-blocked or unreachable iframe fires neither load nor error, so a
173
+ // silent live view must degrade to screenshots on its own.
174
+ clearTimeout(readyTimer);
175
+ readyTimer = setTimeout(function(){
176
+ if (!viewReady) useFallback("The live view is not reachable from this browser; showing periodic screenshots.");
177
+ }, 8000);
178
+ }
179
+
180
+ /* ---------- screenshot fallback ---------- */
181
+ function useFallback(reason){
182
+ if (usingFallback) return;
183
+ usingFallback = true;
184
+ clearTimeout(readyTimer);
185
+ frame.classList.add("hide");
186
+ fallback.classList.add("show");
187
+ setNote(reason || "Live streaming is unavailable; showing periodic screenshots.", true);
188
+ pollOnce();
189
+ }
190
+ async function pollOnce(){
191
+ if (!usingFallback || !sessionId) return;
192
+ try {
193
+ var result = await call("browser_screenshot", { sessionId: sessionId });
194
+ var items = (result && result.content) || [];
195
+ for (var i = 0; i < items.length; i++){
196
+ if (items[i].type === "image" && items[i].data){
197
+ shot.innerHTML = "";
198
+ var img = document.createElement("img");
199
+ img.src = "data:" + (items[i].mimeType || "image/png") + ";base64," + items[i].data;
200
+ img.alt = "Browser viewport";
201
+ shot.appendChild(img);
202
+ break;
203
+ }
204
+ }
205
+ var state = payload(result);
206
+ if (state.url && document.activeElement !== fbUrl) fbUrl.value = state.url;
207
+ setNote("Screenshot fallback \\u2014 updated " + new Date().toLocaleTimeString());
208
+ } catch (e) {
209
+ setNote(errorText(e), true);
210
+ }
211
+ clearTimeout(pollTimer);
212
+ pollTimer = setTimeout(pollOnce, 2000);
213
+ }
214
+ function fallbackAction(kind, url){
215
+ return async function(){
216
+ try {
217
+ if (kind === "navigate") await call("browser_navigate", { sessionId: sessionId, url: url() });
218
+ else await call("browser_" + kind, { sessionId: sessionId });
219
+ pollOnce();
220
+ } catch (e) { setNote(errorText(e), true); }
221
+ };
222
+ }
223
+ document.getElementById("fbBack").onclick = fallbackAction("back");
224
+ document.getElementById("fbForward").onclick = fallbackAction("forward");
225
+ document.getElementById("fbReload").onclick = fallbackAction("reload");
226
+ document.getElementById("fbGo").onclick = fallbackAction("navigate", function(){ return fbUrl.value.trim(); });
227
+ fbUrl.onkeydown = function(e){ if (e.key === "Enter"){ e.preventDefault(); document.getElementById("fbGo").click(); } };
228
+
229
+ /* ---------- session ---------- */
230
+ async function start(initialUrl, requestedId, owned){
231
+ if (starting) return;
232
+ starting = true;
233
+ ownsSession = !!owned;
234
+ try {
235
+ var args = {};
236
+ if (initialUrl) args.url = initialUrl;
237
+ if (requestedId) args.sessionId = requestedId;
238
+ var result = await call("browser_open", args);
239
+ var state = payload(result);
240
+ sessionId = state.sessionId || "";
241
+ sessionKey = result && result._meta && result._meta.flujo && result._meta.flujo.gatewaySessionToken || "";
242
+ if (!sessionId || !sessionKey) throw new Error("The browser session handshake did not report its private capability.");
243
+ if (!GATEWAY.origin || !GATEWAY.token) useFallback("The live stream gateway is disabled, so this view falls back to screenshots.");
244
+ else if (!frameGrantCovers(GATEWAY.origin)) useFallback("This deployment cannot frame the local live-view gateway; showing periodic screenshots.");
245
+ else mountLiveView();
246
+ } catch (e) {
247
+ ownsSession = false;
248
+ starting = false;
249
+ useFallback(errorText(e));
250
+ }
251
+ }
252
+
253
+ async function teardownResource(requestId){
254
+ clearTimeout(pollTimer);
255
+ clearTimeout(readyTimer);
256
+ clearTimeout(standaloneTimer);
257
+ clearTimeout(toolResultTimer);
258
+ var ownedId = ownsSession ? sessionId : "";
259
+ ownsSession = false;
260
+ if (ownedId){
261
+ try { await call("browser_close", { sessionId: ownedId }); } catch (e) {}
262
+ }
263
+ post({ jsonrpc: "2.0", id: requestId, result: {} });
264
+ }
265
+
266
+ /* ---------- commands relayed from the framed view ---------- */
267
+ async function runViewCommand(data){
268
+ try {
269
+ if (data.type === "navigate" && data.url) await call("browser_navigate", { sessionId: sessionId, url: data.url });
270
+ else if (data.type === "back") await call("browser_back", { sessionId: sessionId });
271
+ else if (data.type === "forward") await call("browser_forward", { sessionId: sessionId });
272
+ else if (data.type === "reload") await call("browser_reload", { sessionId: sessionId });
273
+ else if (data.type === "close"){
274
+ await call("browser_close", { sessionId: sessionId });
275
+ notify("ui/notifications/request-teardown", {});
276
+ } else if (data.type === "fullscreen"){
277
+ await rpc("ui/request-display-mode", { mode: "fullscreen" });
278
+ }
279
+ } catch (e) {
280
+ toView({ type: "error", message: errorText(e) });
281
+ } finally {
282
+ toView({ type: "loading", value: false });
283
+ }
284
+ }
285
+
286
+ window.addEventListener("message", function(event){
287
+ var m = event.data;
288
+ if (m && m.source === "flujo-browser-view"){
289
+ if (event.source !== frame.contentWindow) return;
290
+ if (m.type === "ready"){ viewReady = true; clearTimeout(readyTimer); toView({ type: "theme", theme: theme }); }
291
+ else void runViewCommand(m);
292
+ return;
293
+ }
294
+ if (!m || m.jsonrpc !== "2.0") return;
295
+ if (m.id !== undefined && (m.result !== undefined || m.error !== undefined)){
296
+ var p = pending[m.id];
297
+ if (!p) return;
298
+ delete pending[m.id];
299
+ if (m.error) p.reject(new Error(m.error.message || "RPC error"));
300
+ else p.resolve(m.result);
301
+ return;
302
+ }
303
+ if (m.method === "ui/notifications/host-context-changed"){
304
+ theme = (m.params && m.params.theme) === "dark" ? "dark" : "light";
305
+ document.documentElement.setAttribute("data-theme", theme);
306
+ toView({ type: "theme", theme: theme });
307
+ return;
308
+ }
309
+ if (m.method === "ui/notifications/tool-input"){
310
+ var a = (m.params && m.params.arguments) || {};
311
+ pendingToolInput = a;
312
+ clearTimeout(standaloneTimer);
313
+ if (!starting && typeof a.sessionId === "string" && a.sessionId){
314
+ void start(typeof a.url === "string" ? a.url : "", a.sessionId, false);
315
+ } else if (!starting){
316
+ // Tool results carry the authoritative sessionId. Waiting for it avoids
317
+ // opening a second page after a no-id browser_open created its session.
318
+ clearTimeout(toolResultTimer);
319
+ toolResultTimer = setTimeout(function(){
320
+ if (!starting) void start(typeof a.url === "string" ? a.url : "", "", true);
321
+ }, 1000);
322
+ }
323
+ return;
324
+ }
325
+ if (m.method === "ui/notifications/tool-result"){
326
+ var toolState = payload(m.params);
327
+ var toolSessionId = toolState && typeof toolState.sessionId === "string" ? toolState.sessionId : "";
328
+ if (!starting && toolSessionId){
329
+ clearTimeout(toolResultTimer);
330
+ var input = pendingToolInput || {};
331
+ void start(typeof input.url === "string" ? input.url : "", toolSessionId, false);
332
+ }
333
+ return;
334
+ }
335
+ if (m.method === "ping" && m.id !== undefined){ post({ jsonrpc: "2.0", id: m.id, result: {} }); return; }
336
+ if (m.method === "ui/resource-teardown" && m.id !== undefined){
337
+ void teardownResource(m.id);
338
+ return;
339
+ }
340
+ });
341
+
342
+ rpc("ui/initialize", {
343
+ appInfo: { name: "flujo-browser-view", version: "3.0.0" },
344
+ appCapabilities: { availableDisplayModes: ["inline", "fullscreen", "pip"] },
345
+ protocolVersion: "${MCP_APPS_PROTOCOL_VERSION}"
346
+ }).then(function(result){
347
+ var sandboxCaps = result && result.capabilities && result.capabilities.sandbox;
348
+ if (sandboxCaps) grantedFrameDomains = (sandboxCaps.csp && sandboxCaps.csp.frameDomains) || [];
349
+ theme = (result && result.hostContext && result.hostContext.theme) === "dark" ? "dark" : "light";
350
+ document.documentElement.setAttribute("data-theme", theme);
351
+ notify("ui/notifications/initialized", {});
352
+ notify("ui/notifications/size-changed", { width: 0, height: 640 });
353
+ standaloneTimer = setTimeout(function(){ if (!starting) void start("", "", true); }, 300);
354
+ }).catch(function(e){
355
+ useFallback("App initialization failed: " + errorText(e));
356
+ });
357
+ })();
358
+ </script>
359
+ </body>
360
360
  </html>`;
361
361
  }
362
362
  //# sourceMappingURL=resources.js.map
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { promises as fs } from 'node:fs';
1
+ import { constants as fsConstants, promises as fs } from 'node:fs';
2
2
  import { isIP } from 'node:net';
3
3
  import { lookup } from 'node:dns/promises';
4
4
  import os from 'node:os';
@@ -1021,13 +1021,45 @@ export async function browserDiagnostics(session) {
1021
1021
  ...(fingerprint ? { fingerprint } : {}),
1022
1022
  };
1023
1023
  }
1024
+ function samePreferencesFile(opened, observed) {
1025
+ return observed.isFile() && !observed.isSymbolicLink()
1026
+ && observed.ino !== BigInt(0) && observed.nlink === BigInt(1)
1027
+ && opened.dev === observed.dev && opened.ino === observed.ino
1028
+ && opened.size === observed.size && opened.mtimeNs === observed.mtimeNs
1029
+ && opened.ctimeNs === observed.ctimeNs && opened.mode === observed.mode
1030
+ && opened.uid === observed.uid && opened.gid === observed.gid;
1031
+ }
1024
1032
  async function installedProfileExtensions() {
1025
1033
  const preferencesPath = path.join(trustedProfileDir(), 'Default', 'Preferences');
1026
1034
  try {
1027
- const stat = await fs.stat(preferencesPath);
1028
- if (!stat.isFile() || stat.size > 50_000_000)
1029
- return [];
1030
- const preferences = JSON.parse(await fs.readFile(preferencesPath, 'utf8'));
1035
+ const handle = await fs.open(preferencesPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0));
1036
+ let text;
1037
+ try {
1038
+ const opened = await handle.stat({ bigint: true });
1039
+ const named = await fs.lstat(preferencesPath, { bigint: true });
1040
+ if (!samePreferencesFile(opened, opened) || !samePreferencesFile(opened, named)
1041
+ || opened.size < BigInt(0) || opened.size > BigInt(50_000_000))
1042
+ return [];
1043
+ const size = Number(opened.size);
1044
+ // Keep allocation and reads bounded to the admitted size plus a growth sentinel.
1045
+ const bytes = Buffer.alloc(size + 1);
1046
+ let consumed = 0;
1047
+ while (consumed < bytes.length) {
1048
+ const { bytesRead } = await handle.read(bytes, consumed, Math.min(bytes.length - consumed, 1024 * 1024), consumed);
1049
+ if (bytesRead === 0)
1050
+ break;
1051
+ consumed += bytesRead;
1052
+ }
1053
+ const after = await handle.stat({ bigint: true });
1054
+ const namedAfter = await fs.lstat(preferencesPath, { bigint: true });
1055
+ if (consumed !== size || !samePreferencesFile(opened, after) || !samePreferencesFile(opened, namedAfter))
1056
+ return [];
1057
+ text = bytes.subarray(0, consumed).toString('utf8');
1058
+ }
1059
+ finally {
1060
+ await handle.close();
1061
+ }
1062
+ const preferences = JSON.parse(text);
1031
1063
  const settings = preferences.extensions?.settings ?? {};
1032
1064
  return Object.entries(settings).flatMap(([id, entry]) => {
1033
1065
  const manifest = entry.manifest;