bunite-core 0.12.1 → 0.16.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/package.json +4 -4
- package/src/host/core/App.ts +19 -2
- package/src/host/core/BrowserView.ts +515 -38
- package/src/host/core/SurfaceBrowserIPC.ts +53 -3
- package/src/host/core/SurfaceManager.ts +603 -30
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +25 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +263 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +427 -6
- package/src/native/linux/bunite_linux_internal.h +18 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +296 -5
- package/src/native/mac/bunite_mac_ffi.mm +630 -8
- package/src/native/mac/bunite_mac_internal.h +19 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +371 -9
- package/src/native/shared/ffi_exports.h +200 -2
- package/src/native/win/native_host_cef.cpp +186 -11
- package/src/native/win/native_host_ffi.cpp +1194 -1
- package/src/native/win/native_host_internal.h +35 -0
- package/src/native/win/native_host_utils.cpp +2 -1
- package/src/native/win/process_helper_win.cpp +54 -27
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +1023 -12
- package/src/native/win-webview2/webview2_internal.h +25 -0
- package/src/native/win-webview2/webview2_runtime.cpp +403 -34
- package/src/native/win-webview2/webview2_utils.cpp +30 -12
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +97 -0
- package/src/rpc/framework.ts +340 -8
- package/src/rpc/index.ts +32 -0
- package/src/webview/native.ts +253 -51
- package/src/webview/polyfill.ts +283 -22
package/src/webview/polyfill.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Iframe fallback for web (no-op if native already registered). HTMLElement deref'd lazily so module is import-safe in Node/Bun.
|
|
2
2
|
|
|
3
|
+
import type { SurfaceEvent, SurfaceEventBase } from "../rpc/framework";
|
|
4
|
+
|
|
3
5
|
// Default sandbox omits allow-same-origin / allow-top-navigation / allow-modals /
|
|
4
6
|
// allow-popups-to-escape-sandbox — popup escape stays opt-in so a sandboxed page
|
|
5
7
|
// can't launch unsandboxed auxiliary contexts by default.
|
|
@@ -27,6 +29,69 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
27
29
|
static observedAttributes = ["src", "sandbox", "unsandboxed"];
|
|
28
30
|
|
|
29
31
|
private _iframe: HTMLIFrameElement | null = null;
|
|
32
|
+
private _titleObserver: MutationObserver | null = null;
|
|
33
|
+
private _lastTitle: string = "";
|
|
34
|
+
private _epoch: number = 0;
|
|
35
|
+
private _isLoading: boolean = false;
|
|
36
|
+
private _currentUrl: string = "";
|
|
37
|
+
// Local history stack for goBack — cross-origin contentWindow.history is
|
|
38
|
+
// inaccessible, so we track navigations ourselves.
|
|
39
|
+
private _history: string[] = [];
|
|
40
|
+
|
|
41
|
+
private isReachable(): boolean {
|
|
42
|
+
if (!this._iframe) return false;
|
|
43
|
+
try {
|
|
44
|
+
return this._iframe.contentDocument != null;
|
|
45
|
+
} catch { return false; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private modifierBag(mods?: string[]): {
|
|
49
|
+
shiftKey: boolean; ctrlKey: boolean; altKey: boolean; metaKey: boolean;
|
|
50
|
+
} {
|
|
51
|
+
return {
|
|
52
|
+
shiftKey: !!mods?.includes("shift"),
|
|
53
|
+
ctrlKey: !!mods?.includes("ctrl"),
|
|
54
|
+
altKey: !!mods?.includes("alt"),
|
|
55
|
+
metaKey: !!mods?.includes("meta"),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private emit(event: SurfaceEventBase) {
|
|
60
|
+
if (event.type === "navigate") {
|
|
61
|
+
this._epoch++;
|
|
62
|
+
// Avoid pushing duplicate / same-as-top entries (reload doesn't grow history).
|
|
63
|
+
if (this._currentUrl && this._currentUrl !== event.url &&
|
|
64
|
+
this._history[this._history.length - 1] !== this._currentUrl) {
|
|
65
|
+
this._history.push(this._currentUrl);
|
|
66
|
+
}
|
|
67
|
+
this._currentUrl = event.url;
|
|
68
|
+
} else if (event.type === "load-start") {
|
|
69
|
+
this._isLoading = true;
|
|
70
|
+
} else if (event.type === "load-finish" || event.type === "load-fail") {
|
|
71
|
+
this._isLoading = false;
|
|
72
|
+
}
|
|
73
|
+
const stamped: SurfaceEvent = { ...event, epoch: this._epoch };
|
|
74
|
+
this.dispatchEvent(new CustomEvent<SurfaceEvent>("surface-event", { detail: stamped }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private setupTitleObserver() {
|
|
78
|
+
this._titleObserver?.disconnect();
|
|
79
|
+
this._titleObserver = null;
|
|
80
|
+
if (!this.isReachable()) return;
|
|
81
|
+
const doc = this._iframe!.contentDocument!;
|
|
82
|
+
this._lastTitle = doc.title;
|
|
83
|
+
const fire = () => {
|
|
84
|
+
const t = doc.title;
|
|
85
|
+
if (t && t !== this._lastTitle) {
|
|
86
|
+
this._lastTitle = t;
|
|
87
|
+
this.emit({ type: "title-change", title: t });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const observer = new MutationObserver(fire);
|
|
91
|
+
const headEl = doc.head ?? doc.documentElement;
|
|
92
|
+
if (headEl) observer.observe(headEl, { childList: true, subtree: true, characterData: true });
|
|
93
|
+
this._titleObserver = observer;
|
|
94
|
+
}
|
|
30
95
|
|
|
31
96
|
private applySandbox(iframe: HTMLIFrameElement) {
|
|
32
97
|
if (this.hasAttribute("unsandboxed")) {
|
|
@@ -38,7 +103,7 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
38
103
|
}
|
|
39
104
|
|
|
40
105
|
private dispatchBlocked(url: string) {
|
|
41
|
-
this.
|
|
106
|
+
this.emit({ type: "load-fail", url, reason: "blocked-scheme" });
|
|
42
107
|
}
|
|
43
108
|
|
|
44
109
|
connectedCallback() {
|
|
@@ -55,6 +120,8 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
55
120
|
this.dispatchBlocked(src);
|
|
56
121
|
} else {
|
|
57
122
|
iframe.src = src;
|
|
123
|
+
this._currentUrl = src;
|
|
124
|
+
this.emit({ type: "load-start", url: src });
|
|
58
125
|
}
|
|
59
126
|
}
|
|
60
127
|
|
|
@@ -66,7 +133,10 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
66
133
|
// Suppress the spurious about:blank load that fires after a blocked
|
|
67
134
|
// navigation (or before any explicit navigate).
|
|
68
135
|
if (isBlockedSrc(url)) return;
|
|
69
|
-
|
|
136
|
+
// navigate first so load-finish carries the bumped epoch.
|
|
137
|
+
this.emit({ type: "navigate", url });
|
|
138
|
+
this.emit({ type: "load-finish", url });
|
|
139
|
+
this.setupTitleObserver();
|
|
70
140
|
});
|
|
71
141
|
|
|
72
142
|
this._iframe = iframe;
|
|
@@ -74,6 +144,8 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
74
144
|
}
|
|
75
145
|
|
|
76
146
|
disconnectedCallback() {
|
|
147
|
+
this._titleObserver?.disconnect();
|
|
148
|
+
this._titleObserver = null;
|
|
77
149
|
this._iframe?.remove();
|
|
78
150
|
this._iframe = null;
|
|
79
151
|
}
|
|
@@ -86,6 +158,7 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
86
158
|
return;
|
|
87
159
|
}
|
|
88
160
|
this._iframe.src = newValue ?? "";
|
|
161
|
+
if (newValue) this.emit({ type: "load-start", url: newValue });
|
|
89
162
|
} else if (name === "sandbox" || name === "unsandboxed") {
|
|
90
163
|
// Sandbox token changes take effect on the next navigation per HTML spec.
|
|
91
164
|
this.applySandbox(this._iframe);
|
|
@@ -97,19 +170,22 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
97
170
|
}
|
|
98
171
|
|
|
99
172
|
goBack() {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
} catch {}
|
|
173
|
+
// Same-origin path uses native history. Cross-origin throws → fall back
|
|
174
|
+
// to our tracked stack (push on every navigate; pop here).
|
|
175
|
+
try { this._iframe?.contentWindow?.history.back(); return; } catch {}
|
|
176
|
+
const prev = this._history.pop();
|
|
177
|
+
if (prev && this._iframe) this._iframe.src = prev;
|
|
103
178
|
}
|
|
104
179
|
|
|
105
180
|
reload() {
|
|
106
|
-
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
181
|
+
try { this._iframe?.contentWindow?.location.reload(); return; } catch {}
|
|
182
|
+
// Cross-origin fallback: reassigning the same src is a no-op in WHATWG;
|
|
183
|
+
// cycle via about:blank to force a fresh navigation.
|
|
184
|
+
const iframe = this._iframe;
|
|
185
|
+
if (!iframe) return;
|
|
186
|
+
const url = this._currentUrl || iframe.src;
|
|
187
|
+
iframe.src = "about:blank";
|
|
188
|
+
requestAnimationFrame(() => { if (this._iframe) this._iframe.src = url; });
|
|
113
189
|
}
|
|
114
190
|
|
|
115
191
|
setHidden(hidden: boolean) {
|
|
@@ -118,20 +194,204 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
118
194
|
}
|
|
119
195
|
}
|
|
120
196
|
|
|
121
|
-
// Automation surface —
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
async evaluate(
|
|
126
|
-
|
|
197
|
+
// Automation surface — works when the iframe is same-origin reachable
|
|
198
|
+
// (i.e. `<bunite-webview unsandboxed>` + same-origin src). Default sandbox
|
|
199
|
+
// strips `allow-same-origin`, so reachability is opt-in. `isTrusted` on
|
|
200
|
+
// synthesised DOM events is always false → `nativeInputTrusted` stays false.
|
|
201
|
+
async evaluate(script: string) {
|
|
202
|
+
if (!this.isReachable()) {
|
|
203
|
+
return { ok: false as const, code: "cross_origin" as const, message: "iframe content not same-origin" };
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const win = this._iframe!.contentWindow as Window & { eval(s: string): unknown };
|
|
207
|
+
return { ok: true as const, value: win.eval(script) };
|
|
208
|
+
} catch (e: unknown) {
|
|
209
|
+
const err = e as { name?: string; message?: string };
|
|
210
|
+
if (err?.name === "SecurityError") {
|
|
211
|
+
return { ok: false as const, code: "cross_origin" as const, message: err.message ?? "SecurityError" };
|
|
212
|
+
}
|
|
213
|
+
return { ok: false as const, code: "runtime_error" as const, message: err?.message ?? String(e) };
|
|
214
|
+
}
|
|
127
215
|
}
|
|
128
216
|
|
|
129
217
|
async capabilities() {
|
|
218
|
+
const reachable = this.isReachable();
|
|
130
219
|
return {
|
|
131
|
-
evaluate:
|
|
132
|
-
nativeInputTrusted: false,
|
|
133
|
-
|
|
220
|
+
evaluate: reachable, crossOriginEval: false, surfaceEvents: true,
|
|
221
|
+
nativeInputTrusted: false,
|
|
222
|
+
click: reachable, type: reachable, press: reachable, scroll: reachable,
|
|
223
|
+
mouse: reachable, dialogs: false, console: false,
|
|
224
|
+
screenshot: false,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async sendClick(args: {
|
|
229
|
+
x: number; y: number; button?: string; clickCount?: number; modifiers?: string[];
|
|
230
|
+
}) {
|
|
231
|
+
if (!this.isReachable()) return;
|
|
232
|
+
const doc = this._iframe!.contentDocument!;
|
|
233
|
+
const target = doc.elementFromPoint(args.x, args.y) ?? doc.body;
|
|
234
|
+
if (!target) return;
|
|
235
|
+
const init: MouseEventInit = {
|
|
236
|
+
bubbles: true, cancelable: true, view: this._iframe!.contentWindow,
|
|
237
|
+
clientX: args.x, clientY: args.y,
|
|
238
|
+
button: args.button === "right" ? 2 : args.button === "middle" ? 1 : 0,
|
|
239
|
+
detail: args.clickCount ?? 1,
|
|
240
|
+
...this.modifierBag(args.modifiers),
|
|
241
|
+
};
|
|
242
|
+
target.dispatchEvent(new MouseEvent("mousedown", init));
|
|
243
|
+
target.dispatchEvent(new MouseEvent("mouseup", init));
|
|
244
|
+
target.dispatchEvent(new MouseEvent("click", init));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async resolveAndClick(_selector: string, _opts?: unknown) {
|
|
248
|
+
return { ok: false as const, code: "not_supported" as const, message: "polyfill iframe: atomic resolveAndClick not supported" };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async getBoundingRect(selector: string, _opts?: unknown) {
|
|
252
|
+
if (!this.isReachable()) return { ok: false as const, code: "not_supported" as const, message: "iframe not reachable" };
|
|
253
|
+
try {
|
|
254
|
+
const el = this._iframe!.contentDocument!.querySelector(selector) as Element | null;
|
|
255
|
+
if (!el) return { ok: false as const, code: "not_found" as const, message: `selector ${selector} not found` };
|
|
256
|
+
const r = el.getBoundingClientRect();
|
|
257
|
+
const win = this._iframe!.contentWindow!;
|
|
258
|
+
const visible = r.width > 0 && r.height > 0 && r.bottom > 0 && r.right > 0
|
|
259
|
+
&& r.top < win.innerHeight && r.left < win.innerWidth;
|
|
260
|
+
return { ok: true as const, rect: { x: r.x, y: r.y, width: r.width, height: r.height }, visible };
|
|
261
|
+
} catch (e: any) {
|
|
262
|
+
const code = e?.name === "SecurityError" ? "cross_origin" as const : "runtime_error" as const;
|
|
263
|
+
return { ok: false as const, code, message: e?.message ?? String(e) };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async listFrames() {
|
|
268
|
+
return { ok: false as const, code: "not_supported" as const, message: "polyfill: not implemented" };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async accessibilitySnapshot(_opts?: unknown) {
|
|
272
|
+
return { ok: false as const, code: "not_supported" as const, message: "polyfill: not implemented" };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async setDownloadPolicy(_policy: unknown, _dir?: unknown) {
|
|
276
|
+
// No-op — iframe has no download lifecycle hook.
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async waitForDownload(_opts?: unknown) {
|
|
280
|
+
return { ok: false as const, code: "not_supported" as const, message: "polyfill: not implemented" };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async acceptPopup(_opts?: unknown) {
|
|
284
|
+
return { ok: false as const, code: "not_found" as const, message: "polyfill: no popup orchestration" };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async dismissPopup(_id?: unknown) {
|
|
288
|
+
// No-op.
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async extendAdoptionTimeout(_id?: unknown, _ms?: unknown) {
|
|
292
|
+
return { ok: false as const, code: "not_found" as const, message: "polyfill: no popup orchestration" };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async sendType(text: string) {
|
|
296
|
+
if (!this.isReachable()) return;
|
|
297
|
+
const doc = this._iframe!.contentDocument!;
|
|
298
|
+
const target = doc.activeElement as (HTMLInputElement | HTMLTextAreaElement | null);
|
|
299
|
+
if (!target || !("setRangeText" in target)) return;
|
|
300
|
+
// setRangeText preserves selection + caret; the `data` field on an
|
|
301
|
+
// InputEvent + bubbling lets React-style controllers detect the change.
|
|
302
|
+
const start = target.selectionStart ?? target.value.length;
|
|
303
|
+
const end = target.selectionEnd ?? target.value.length;
|
|
304
|
+
target.setRangeText(text, start, end, "end");
|
|
305
|
+
target.dispatchEvent(new InputEvent("input", { bubbles: true, data: text, inputType: "insertText" }));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async sendPress(key: string, modifiers?: string[], action?: "down" | "up" | "both") {
|
|
309
|
+
if (!this.isReachable()) return;
|
|
310
|
+
const doc = this._iframe!.contentDocument!;
|
|
311
|
+
const target = (doc.activeElement ?? doc.body) as Element | null;
|
|
312
|
+
if (!target) return;
|
|
313
|
+
const init: KeyboardEventInit = {
|
|
314
|
+
bubbles: true, cancelable: true, key, ...this.modifierBag(modifiers),
|
|
315
|
+
};
|
|
316
|
+
const a = action ?? "both";
|
|
317
|
+
if (a !== "up") target.dispatchEvent(new KeyboardEvent("keydown", init));
|
|
318
|
+
if (a === "both") target.dispatchEvent(new KeyboardEvent("keypress", init));
|
|
319
|
+
if (a !== "down") target.dispatchEvent(new KeyboardEvent("keyup", init));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async sendScroll(args: { dx: number; dy: number; x?: number; y?: number; modifiers?: string[] }) {
|
|
323
|
+
if (!this.isReachable()) return;
|
|
324
|
+
this._iframe!.contentWindow!.scrollBy(args.dx, args.dy);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async sendMouse(args: {
|
|
328
|
+
action: "move" | "down" | "up";
|
|
329
|
+
x: number; y: number;
|
|
330
|
+
button?: string;
|
|
331
|
+
modifiers?: string[];
|
|
332
|
+
}) {
|
|
333
|
+
if (!this.isReachable()) return;
|
|
334
|
+
const doc = this._iframe!.contentDocument!;
|
|
335
|
+
const target = doc.elementFromPoint(args.x, args.y) ?? doc.body;
|
|
336
|
+
if (!target) return;
|
|
337
|
+
const type = args.action === "move" ? "mousemove" : args.action === "down" ? "mousedown" : "mouseup";
|
|
338
|
+
const init: MouseEventInit = {
|
|
339
|
+
bubbles: true, cancelable: true, view: this._iframe!.contentWindow,
|
|
340
|
+
clientX: args.x, clientY: args.y,
|
|
341
|
+
button: args.button === "right" ? 2 : args.button === "middle" ? 1 : 0,
|
|
342
|
+
...this.modifierBag(args.modifiers),
|
|
134
343
|
};
|
|
344
|
+
target.dispatchEvent(new MouseEvent(type, init));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async respondToDialog(_requestId: number, _accept: boolean, _text?: string) {
|
|
348
|
+
// iframe sandbox forbids cross-frame dialog interception; nothing to do.
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async setDialogTimeout(_ms: number | null) { /* no-op */ }
|
|
352
|
+
|
|
353
|
+
async waitForSelector(selector: string, timeoutMs = 5000) {
|
|
354
|
+
if (!this.isReachable()) {
|
|
355
|
+
return { ok: false as const, code: "runtime_error" as const, message: "iframe content not same-origin" };
|
|
356
|
+
}
|
|
357
|
+
const doc = this._iframe!.contentDocument!;
|
|
358
|
+
const deadline = Date.now() + timeoutMs;
|
|
359
|
+
while (Date.now() < deadline) {
|
|
360
|
+
if (doc.querySelector(selector)) return { ok: true as const };
|
|
361
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
362
|
+
}
|
|
363
|
+
return { ok: false as const, code: "timeout" as const, message: `selector ${JSON.stringify(selector)} not found within ${timeoutMs}ms` };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async waitForFunction(expression: string, opts?: { timeoutMs?: number; pollIntervalMs?: number }) {
|
|
367
|
+
if (!this.isReachable()) {
|
|
368
|
+
return { ok: false as const, code: "runtime_error" as const, message: "iframe content not same-origin" };
|
|
369
|
+
}
|
|
370
|
+
const win = this._iframe!.contentWindow as Window & { eval(s: string): unknown };
|
|
371
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 5000);
|
|
372
|
+
const interval = opts?.pollIntervalMs ?? 50;
|
|
373
|
+
while (Date.now() < deadline) {
|
|
374
|
+
try {
|
|
375
|
+
if (win.eval(expression)) return { ok: true as const };
|
|
376
|
+
} catch (e) {
|
|
377
|
+
return { ok: false as const, code: "runtime_error" as const, message: (e as Error).message };
|
|
378
|
+
}
|
|
379
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
380
|
+
}
|
|
381
|
+
return { ok: false as const, code: "timeout" as const, message: `function did not satisfy within ${opts?.timeoutMs ?? 5000}ms` };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async getConsoleBuffer(_opts?: { clear?: boolean }) {
|
|
385
|
+
// iframe polyfill doesn't inject a preload — no console capture available.
|
|
386
|
+
return [] as { level: string; args: string[]; ts: number }[];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async getNavigationState() {
|
|
390
|
+
return { lastLoadEpoch: this._epoch, isLoading: this._isLoading, currentUrl: this._currentUrl };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async screenshot(_args?: { format?: "png" | "jpeg"; quality?: number }) {
|
|
394
|
+
return { ok: false as const, code: "not_supported" as const, message: "iframe polyfill does not support screenshot" };
|
|
135
395
|
}
|
|
136
396
|
}
|
|
137
397
|
|
|
@@ -149,7 +409,8 @@ function definePolyfillClass(): CustomElementConstructor {
|
|
|
149
409
|
* - `referrerpolicy="no-referrer"`.
|
|
150
410
|
* - `javascript:` / `data:` / `vbscript:` / `file:` / `about:` schemes blocked
|
|
151
411
|
* (with WHATWG URL-style normalization to defeat embedded-control bypass);
|
|
152
|
-
* navigation attempt dispatches `
|
|
412
|
+
* navigation attempt dispatches `surface-event` with `detail.type === "load-fail"`
|
|
413
|
+
* and `detail.reason === "blocked-scheme"`.
|
|
153
414
|
*
|
|
154
415
|
* Opt-out attributes on `<bunite-webview>` (observed — mutations re-apply):
|
|
155
416
|
* - `sandbox="..."` — override the default sandbox token string verbatim.
|