@nativewindow/webview 1.0.1 → 1.0.2
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/index.js +386 -400
- package/package.json +18 -12
package/dist/index.js
CHANGED
|
@@ -1,406 +1,392 @@
|
|
|
1
|
-
import { NativeWindow as NativeWindow$1, init, pumpEvents } from "../native-window.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { NativeWindow as NativeWindow$1, checkRuntime, ensureRuntime, init, loadHtmlOrigin, pumpEvents } from "../native-window.js";
|
|
2
|
+
//#region index.ts
|
|
3
|
+
var _pump = null;
|
|
4
|
+
var _windowCount = 0;
|
|
5
5
|
function ensureInit() {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
if (_pump) return;
|
|
7
|
+
init();
|
|
8
|
+
_pump = setInterval(() => {
|
|
9
|
+
try {
|
|
10
|
+
pumpEvents();
|
|
11
|
+
} catch (e) {
|
|
12
|
+
console.error("[native-window] pumpEvents() error:", e);
|
|
13
|
+
}
|
|
14
|
+
}, 16);
|
|
15
15
|
}
|
|
16
16
|
function stopPump() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
class NativeWindow {
|
|
23
|
-
/** @internal */
|
|
24
|
-
_native;
|
|
25
|
-
/** @internal */
|
|
26
|
-
_closed = false;
|
|
27
|
-
/** @internal */
|
|
28
|
-
_unsafe;
|
|
29
|
-
/** @internal */
|
|
30
|
-
_devtools;
|
|
31
|
-
constructor(options) {
|
|
32
|
-
ensureInit();
|
|
33
|
-
_windowCount++;
|
|
34
|
-
this._native = new NativeWindow$1(options);
|
|
35
|
-
this._native.onClose(() => this._handleClose());
|
|
36
|
-
}
|
|
37
|
-
/** @internal */
|
|
38
|
-
_handleClose() {
|
|
39
|
-
if (this._closed) return;
|
|
40
|
-
this._closed = true;
|
|
41
|
-
_windowCount--;
|
|
42
|
-
this._userCloseCallback?.();
|
|
43
|
-
if (_windowCount <= 0) {
|
|
44
|
-
_windowCount = 0;
|
|
45
|
-
setTimeout(() => stopPump(), 200);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Throws if the window has been closed.
|
|
50
|
-
* @internal
|
|
51
|
-
*/
|
|
52
|
-
_ensureOpen() {
|
|
53
|
-
if (this._closed) {
|
|
54
|
-
throw new Error("Window is closed");
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// ---- onClose with user callback support ----
|
|
58
|
-
_userCloseCallback;
|
|
59
|
-
/**
|
|
60
|
-
* Register a handler for the window close event.
|
|
61
|
-
* The pump is automatically stopped when all windows are closed.
|
|
62
|
-
*
|
|
63
|
-
* Calling this multiple times replaces the previous handler.
|
|
64
|
-
*/
|
|
65
|
-
onClose(callback) {
|
|
66
|
-
if (this._userCloseCallback) {
|
|
67
|
-
console.warn(
|
|
68
|
-
"NativeWindow: onClose() called multiple times. The previous handler will be replaced."
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
this._userCloseCallback = callback;
|
|
72
|
-
}
|
|
73
|
-
// ---- Getters ----
|
|
74
|
-
/** Unique window ID */
|
|
75
|
-
get id() {
|
|
76
|
-
return this._native.id;
|
|
77
|
-
}
|
|
78
|
-
// ---- Content loading ----
|
|
79
|
-
loadUrl(url) {
|
|
80
|
-
this._ensureOpen();
|
|
81
|
-
this._native.loadUrl(url);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Load raw HTML content into the webview.
|
|
85
|
-
*
|
|
86
|
-
* @security **Injection risk.** Never interpolate unsanitized user input
|
|
87
|
-
* into HTML strings. Use a dedicated sanitization library such as
|
|
88
|
-
* [DOMPurify](https://github.com/cure53/DOMPurify) or
|
|
89
|
-
* [sanitize-html](https://github.com/apostrophecms/sanitize-html) to
|
|
90
|
-
* sanitize untrusted content before embedding it.
|
|
91
|
-
*/
|
|
92
|
-
loadHtml(html) {
|
|
93
|
-
this._ensureOpen();
|
|
94
|
-
this._native.loadHtml(html);
|
|
95
|
-
}
|
|
96
|
-
postMessage(message) {
|
|
97
|
-
this._ensureOpen();
|
|
98
|
-
this._native.postMessage(message);
|
|
99
|
-
}
|
|
100
|
-
// ---- Unsafe operations ----
|
|
101
|
-
/**
|
|
102
|
-
* Namespace for operations that require extra care to avoid injection risks.
|
|
103
|
-
* Methods under `unsafe` execute arbitrary code in the webview context.
|
|
104
|
-
*
|
|
105
|
-
* @security Never pass unsanitized user input to these methods.
|
|
106
|
-
* Use {@link sanitizeForJs} to escape strings before embedding them in
|
|
107
|
-
* script code.
|
|
108
|
-
*/
|
|
109
|
-
get unsafe() {
|
|
110
|
-
this._ensureOpen();
|
|
111
|
-
if (!this._unsafe) {
|
|
112
|
-
this._unsafe = {
|
|
113
|
-
evaluateJs: (script) => {
|
|
114
|
-
this._ensureOpen();
|
|
115
|
-
this._native.evaluateJs(script);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
return this._unsafe;
|
|
120
|
-
}
|
|
121
|
-
// ---- Devtools ----
|
|
122
|
-
/**
|
|
123
|
-
* Namespace for controlling the browser devtools panel.
|
|
124
|
-
* Requires `devtools: true` in {@link WindowOptions} at window creation.
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* ```ts
|
|
128
|
-
* const win = new NativeWindow({ devtools: true });
|
|
129
|
-
* win.devtools.open();
|
|
130
|
-
* console.log(win.devtools.isOpen()); // true
|
|
131
|
-
* win.devtools.close();
|
|
132
|
-
* ```
|
|
133
|
-
*/
|
|
134
|
-
get devtools() {
|
|
135
|
-
this._ensureOpen();
|
|
136
|
-
if (!this._devtools) {
|
|
137
|
-
this._devtools = {
|
|
138
|
-
open: () => {
|
|
139
|
-
this._ensureOpen();
|
|
140
|
-
this._native.openDevtools();
|
|
141
|
-
},
|
|
142
|
-
close: () => {
|
|
143
|
-
this._ensureOpen();
|
|
144
|
-
this._native.closeDevtools();
|
|
145
|
-
},
|
|
146
|
-
isOpen: () => {
|
|
147
|
-
this._ensureOpen();
|
|
148
|
-
return this._native.isDevtoolsOpen();
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
return this._devtools;
|
|
153
|
-
}
|
|
154
|
-
// ---- Window control ----
|
|
155
|
-
setTitle(title) {
|
|
156
|
-
this._ensureOpen();
|
|
157
|
-
this._native.setTitle(title);
|
|
158
|
-
}
|
|
159
|
-
setSize(width, height) {
|
|
160
|
-
this._ensureOpen();
|
|
161
|
-
this._native.setSize(width, height);
|
|
162
|
-
}
|
|
163
|
-
setMinSize(width, height) {
|
|
164
|
-
this._ensureOpen();
|
|
165
|
-
this._native.setMinSize(width, height);
|
|
166
|
-
}
|
|
167
|
-
setMaxSize(width, height) {
|
|
168
|
-
this._ensureOpen();
|
|
169
|
-
this._native.setMaxSize(width, height);
|
|
170
|
-
}
|
|
171
|
-
setPosition(x, y) {
|
|
172
|
-
this._ensureOpen();
|
|
173
|
-
this._native.setPosition(x, y);
|
|
174
|
-
}
|
|
175
|
-
setResizable(resizable) {
|
|
176
|
-
this._ensureOpen();
|
|
177
|
-
this._native.setResizable(resizable);
|
|
178
|
-
}
|
|
179
|
-
setDecorations(decorations) {
|
|
180
|
-
this._ensureOpen();
|
|
181
|
-
this._native.setDecorations(decorations);
|
|
182
|
-
}
|
|
183
|
-
setAlwaysOnTop(alwaysOnTop) {
|
|
184
|
-
this._ensureOpen();
|
|
185
|
-
this._native.setAlwaysOnTop(alwaysOnTop);
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Set the window icon from a PNG or ICO file path.
|
|
189
|
-
* On macOS this is silently ignored (macOS doesn't support per-window icons).
|
|
190
|
-
* Relative paths resolve from the working directory.
|
|
191
|
-
*/
|
|
192
|
-
setIcon(path) {
|
|
193
|
-
this._ensureOpen();
|
|
194
|
-
this._native.setIcon(path);
|
|
195
|
-
}
|
|
196
|
-
// ---- Window state ----
|
|
197
|
-
show() {
|
|
198
|
-
this._ensureOpen();
|
|
199
|
-
this._native.show();
|
|
200
|
-
}
|
|
201
|
-
hide() {
|
|
202
|
-
this._ensureOpen();
|
|
203
|
-
this._native.hide();
|
|
204
|
-
}
|
|
205
|
-
close() {
|
|
206
|
-
this._ensureOpen();
|
|
207
|
-
this._native.close();
|
|
208
|
-
}
|
|
209
|
-
focus() {
|
|
210
|
-
this._ensureOpen();
|
|
211
|
-
this._native.focus();
|
|
212
|
-
}
|
|
213
|
-
maximize() {
|
|
214
|
-
this._ensureOpen();
|
|
215
|
-
this._native.maximize();
|
|
216
|
-
}
|
|
217
|
-
minimize() {
|
|
218
|
-
this._ensureOpen();
|
|
219
|
-
this._native.minimize();
|
|
220
|
-
}
|
|
221
|
-
unmaximize() {
|
|
222
|
-
this._ensureOpen();
|
|
223
|
-
this._native.unmaximize();
|
|
224
|
-
}
|
|
225
|
-
reload() {
|
|
226
|
-
this._ensureOpen();
|
|
227
|
-
this._native.reload();
|
|
228
|
-
}
|
|
229
|
-
// ---- Event handlers ----
|
|
230
|
-
/**
|
|
231
|
-
* Register a handler for messages from the webview.
|
|
232
|
-
*
|
|
233
|
-
* @security **No origin filtering.** The raw `onMessage` API does not
|
|
234
|
-
* enforce origin restrictions. If your webview navigates to untrusted
|
|
235
|
-
* URLs, validate the `sourceUrl` parameter before processing messages.
|
|
236
|
-
* For automatic origin filtering, use `createChannel()` with the
|
|
237
|
-
* `trustedOrigins` option from `native-window-ipc`.
|
|
238
|
-
*
|
|
239
|
-
* @security **No rate limiting.** Messages from the webview are delivered
|
|
240
|
-
* without throttling. A malicious page can flood the host with messages.
|
|
241
|
-
* Consider implementing application-level rate limiting if loading
|
|
242
|
-
* untrusted content.
|
|
243
|
-
*/
|
|
244
|
-
onMessage(callback) {
|
|
245
|
-
this._ensureOpen();
|
|
246
|
-
this._native.onMessage(callback);
|
|
247
|
-
}
|
|
248
|
-
onResize(callback) {
|
|
249
|
-
this._ensureOpen();
|
|
250
|
-
this._native.onResize(callback);
|
|
251
|
-
}
|
|
252
|
-
onMove(callback) {
|
|
253
|
-
this._ensureOpen();
|
|
254
|
-
this._native.onMove(callback);
|
|
255
|
-
}
|
|
256
|
-
onFocus(callback) {
|
|
257
|
-
this._ensureOpen();
|
|
258
|
-
this._native.onFocus(callback);
|
|
259
|
-
}
|
|
260
|
-
onBlur(callback) {
|
|
261
|
-
this._ensureOpen();
|
|
262
|
-
this._native.onBlur(callback);
|
|
263
|
-
}
|
|
264
|
-
onPageLoad(callback) {
|
|
265
|
-
this._ensureOpen();
|
|
266
|
-
this._native.onPageLoad(callback);
|
|
267
|
-
}
|
|
268
|
-
onTitleChanged(callback) {
|
|
269
|
-
this._ensureOpen();
|
|
270
|
-
this._native.onTitleChanged(callback);
|
|
271
|
-
}
|
|
272
|
-
onReload(callback) {
|
|
273
|
-
this._ensureOpen();
|
|
274
|
-
this._native.onReload(callback);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Register a handler for blocked navigation events.
|
|
278
|
-
* Fired when a navigation is blocked by the {@link WindowOptions.allowedHosts}
|
|
279
|
-
* restriction. Receives the URL that was blocked.
|
|
280
|
-
*
|
|
281
|
-
* @example
|
|
282
|
-
* ```ts
|
|
283
|
-
* win.onNavigationBlocked((url) => {
|
|
284
|
-
* console.log("Blocked navigation to:", url);
|
|
285
|
-
* });
|
|
286
|
-
* ```
|
|
287
|
-
*/
|
|
288
|
-
onNavigationBlocked(callback) {
|
|
289
|
-
this._ensureOpen();
|
|
290
|
-
this._native.onNavigationBlocked(callback);
|
|
291
|
-
}
|
|
292
|
-
// ---- Cookie access ----
|
|
293
|
-
/**
|
|
294
|
-
* Validate and parse a raw cookies JSON array from the native layer.
|
|
295
|
-
* Returns a cleaned {@link CookieInfo} array or `null` if the payload
|
|
296
|
-
* is malformed.
|
|
297
|
-
*
|
|
298
|
-
* @internal
|
|
299
|
-
*/
|
|
300
|
-
_validateCookies(raw) {
|
|
301
|
-
let parsed;
|
|
302
|
-
try {
|
|
303
|
-
parsed = JSON.parse(raw);
|
|
304
|
-
} catch {
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
if (!Array.isArray(parsed)) return null;
|
|
308
|
-
const cookies = [];
|
|
309
|
-
for (const item of parsed) {
|
|
310
|
-
if (typeof item !== "object" || item === null) continue;
|
|
311
|
-
const obj = item;
|
|
312
|
-
if (typeof obj.name !== "string" || typeof obj.value !== "string") {
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
if (typeof obj.domain !== "string" || typeof obj.path !== "string") {
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (typeof obj.httpOnly !== "boolean" || typeof obj.secure !== "boolean") {
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (typeof obj.sameSite !== "string" || typeof obj.expires !== "number") {
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
cookies.push({
|
|
325
|
-
name: obj.name,
|
|
326
|
-
value: obj.value,
|
|
327
|
-
domain: obj.domain,
|
|
328
|
-
path: obj.path,
|
|
329
|
-
httpOnly: obj.httpOnly,
|
|
330
|
-
secure: obj.secure,
|
|
331
|
-
sameSite: obj.sameSite,
|
|
332
|
-
expires: obj.expires
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
return cookies;
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Query cookies from the native cookie store.
|
|
339
|
-
*
|
|
340
|
-
* Returns a Promise that resolves with validated {@link CookieInfo} objects,
|
|
341
|
-
* including `HttpOnly` cookies that are invisible to `document.cookie`.
|
|
342
|
-
*
|
|
343
|
-
* - **macOS**: Uses `WKHTTPCookieStore.getAllCookies` with client-side
|
|
344
|
-
* URL filtering (domain + path match).
|
|
345
|
-
* - **Windows**: Uses `ICoreWebView2CookieManager.GetCookies` which
|
|
346
|
-
* filters by URI natively.
|
|
347
|
-
*
|
|
348
|
-
* @param url If provided, only cookies matching this URL are returned.
|
|
349
|
-
* If omitted, all cookies in the webview's cookie store are returned.
|
|
350
|
-
*
|
|
351
|
-
* @example
|
|
352
|
-
* ```ts
|
|
353
|
-
* const cookies = await win.getCookies("https://example.com");
|
|
354
|
-
* const session = cookies.find((c) => c.name === "session_id");
|
|
355
|
-
* if (session) console.log("Session:", session.value, "HttpOnly:", session.httpOnly);
|
|
356
|
-
* ```
|
|
357
|
-
*/
|
|
358
|
-
getCookies(url) {
|
|
359
|
-
this._ensureOpen();
|
|
360
|
-
return new Promise((resolve, reject) => {
|
|
361
|
-
const timeout = setTimeout(() => {
|
|
362
|
-
reject(new Error("getCookies() timed out after 10 seconds"));
|
|
363
|
-
}, 1e4);
|
|
364
|
-
this._native.onCookies((raw) => {
|
|
365
|
-
clearTimeout(timeout);
|
|
366
|
-
const validated = this._validateCookies(raw);
|
|
367
|
-
if (validated) {
|
|
368
|
-
resolve(validated);
|
|
369
|
-
} else {
|
|
370
|
-
reject(new Error("Failed to parse cookie response"));
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
this._native.getCookies(url);
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Clear cookies from the native cookie store.
|
|
378
|
-
*
|
|
379
|
-
* - If `host` is provided, only cookies whose domain matches that host
|
|
380
|
-
* are deleted (e.g. `"example.com"` deletes `.example.com` cookies).
|
|
381
|
-
* - If omitted, all cookies in the webview's cookie store are cleared.
|
|
382
|
-
*
|
|
383
|
-
* @example
|
|
384
|
-
* ```ts
|
|
385
|
-
* // Clear all cookies
|
|
386
|
-
* win.clearCookies();
|
|
387
|
-
*
|
|
388
|
-
* // Clear cookies for a specific host
|
|
389
|
-
* win.clearCookies("example.com");
|
|
390
|
-
* ```
|
|
391
|
-
*/
|
|
392
|
-
clearCookies(host) {
|
|
393
|
-
this._ensureOpen();
|
|
394
|
-
this._native.clearCookies(host);
|
|
395
|
-
}
|
|
17
|
+
if (_pump) {
|
|
18
|
+
clearInterval(_pump);
|
|
19
|
+
_pump = null;
|
|
20
|
+
}
|
|
396
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A native OS window with an embedded webview.
|
|
24
|
+
*
|
|
25
|
+
* Automatically initializes the native subsystem and starts pumping
|
|
26
|
+
* events on first construction. Stops the pump when all windows close.
|
|
27
|
+
*/
|
|
28
|
+
var NativeWindow = class {
|
|
29
|
+
/** @internal */
|
|
30
|
+
_native;
|
|
31
|
+
/** @internal */
|
|
32
|
+
_closed = false;
|
|
33
|
+
/** @internal */
|
|
34
|
+
_unsafe;
|
|
35
|
+
/** @internal */
|
|
36
|
+
_devtools;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
ensureInit();
|
|
39
|
+
_windowCount++;
|
|
40
|
+
this._native = new NativeWindow$1(options);
|
|
41
|
+
this._native.onClose(() => this._handleClose());
|
|
42
|
+
}
|
|
43
|
+
/** @internal */
|
|
44
|
+
_handleClose() {
|
|
45
|
+
if (this._closed) return;
|
|
46
|
+
this._closed = true;
|
|
47
|
+
_windowCount--;
|
|
48
|
+
this._userCloseCallback?.();
|
|
49
|
+
if (_windowCount <= 0) {
|
|
50
|
+
_windowCount = 0;
|
|
51
|
+
setTimeout(() => stopPump(), 200);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Throws if the window has been closed.
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
_ensureOpen() {
|
|
59
|
+
if (this._closed) throw new Error("Window is closed");
|
|
60
|
+
}
|
|
61
|
+
_userCloseCallback;
|
|
62
|
+
/**
|
|
63
|
+
* Register a handler for the window close event.
|
|
64
|
+
* The pump is automatically stopped when all windows are closed.
|
|
65
|
+
*
|
|
66
|
+
* Calling this multiple times replaces the previous handler.
|
|
67
|
+
*/
|
|
68
|
+
onClose(callback) {
|
|
69
|
+
if (this._userCloseCallback) console.warn("NativeWindow: onClose() called multiple times. The previous handler will be replaced.");
|
|
70
|
+
this._userCloseCallback = callback;
|
|
71
|
+
}
|
|
72
|
+
/** Unique window ID */
|
|
73
|
+
get id() {
|
|
74
|
+
return this._native.id;
|
|
75
|
+
}
|
|
76
|
+
loadUrl(url) {
|
|
77
|
+
this._ensureOpen();
|
|
78
|
+
this._native.loadUrl(url);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Load raw HTML content into the webview.
|
|
82
|
+
*
|
|
83
|
+
* @security **Injection risk.** Never interpolate unsanitized user input
|
|
84
|
+
* into HTML strings. Use a dedicated sanitization library such as
|
|
85
|
+
* [DOMPurify](https://github.com/cure53/DOMPurify) or
|
|
86
|
+
* [sanitize-html](https://github.com/apostrophecms/sanitize-html) to
|
|
87
|
+
* sanitize untrusted content before embedding it.
|
|
88
|
+
*/
|
|
89
|
+
loadHtml(html) {
|
|
90
|
+
this._ensureOpen();
|
|
91
|
+
this._native.loadHtml(html);
|
|
92
|
+
}
|
|
93
|
+
postMessage(message) {
|
|
94
|
+
this._ensureOpen();
|
|
95
|
+
this._native.postMessage(message);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Namespace for operations that require extra care to avoid injection risks.
|
|
99
|
+
* Methods under `unsafe` execute arbitrary code in the webview context.
|
|
100
|
+
*
|
|
101
|
+
* @security Never pass unsanitized user input to these methods.
|
|
102
|
+
* Use {@link sanitizeForJs} to escape strings before embedding them in
|
|
103
|
+
* script code.
|
|
104
|
+
*/
|
|
105
|
+
get unsafe() {
|
|
106
|
+
this._ensureOpen();
|
|
107
|
+
if (!this._unsafe) this._unsafe = { evaluateJs: (script) => {
|
|
108
|
+
this._ensureOpen();
|
|
109
|
+
this._native.evaluateJs(script);
|
|
110
|
+
} };
|
|
111
|
+
return this._unsafe;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Namespace for controlling the browser devtools panel.
|
|
115
|
+
* Requires `devtools: true` in {@link WindowOptions} at window creation.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* const win = new NativeWindow({ devtools: true });
|
|
120
|
+
* win.devtools.open();
|
|
121
|
+
* console.log(win.devtools.isOpen()); // true
|
|
122
|
+
* win.devtools.close();
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
get devtools() {
|
|
126
|
+
this._ensureOpen();
|
|
127
|
+
if (!this._devtools) this._devtools = {
|
|
128
|
+
open: () => {
|
|
129
|
+
this._ensureOpen();
|
|
130
|
+
this._native.openDevtools();
|
|
131
|
+
},
|
|
132
|
+
close: () => {
|
|
133
|
+
this._ensureOpen();
|
|
134
|
+
this._native.closeDevtools();
|
|
135
|
+
},
|
|
136
|
+
isOpen: () => {
|
|
137
|
+
this._ensureOpen();
|
|
138
|
+
return this._native.isDevtoolsOpen();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
return this._devtools;
|
|
142
|
+
}
|
|
143
|
+
setTitle(title) {
|
|
144
|
+
this._ensureOpen();
|
|
145
|
+
this._native.setTitle(title);
|
|
146
|
+
}
|
|
147
|
+
setSize(width, height) {
|
|
148
|
+
this._ensureOpen();
|
|
149
|
+
this._native.setSize(width, height);
|
|
150
|
+
}
|
|
151
|
+
setMinSize(width, height) {
|
|
152
|
+
this._ensureOpen();
|
|
153
|
+
this._native.setMinSize(width, height);
|
|
154
|
+
}
|
|
155
|
+
setMaxSize(width, height) {
|
|
156
|
+
this._ensureOpen();
|
|
157
|
+
this._native.setMaxSize(width, height);
|
|
158
|
+
}
|
|
159
|
+
setPosition(x, y) {
|
|
160
|
+
this._ensureOpen();
|
|
161
|
+
this._native.setPosition(x, y);
|
|
162
|
+
}
|
|
163
|
+
setResizable(resizable) {
|
|
164
|
+
this._ensureOpen();
|
|
165
|
+
this._native.setResizable(resizable);
|
|
166
|
+
}
|
|
167
|
+
setDecorations(decorations) {
|
|
168
|
+
this._ensureOpen();
|
|
169
|
+
this._native.setDecorations(decorations);
|
|
170
|
+
}
|
|
171
|
+
setAlwaysOnTop(alwaysOnTop) {
|
|
172
|
+
this._ensureOpen();
|
|
173
|
+
this._native.setAlwaysOnTop(alwaysOnTop);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Set the window icon from a PNG or ICO file path.
|
|
177
|
+
* On macOS this is silently ignored (macOS doesn't support per-window icons).
|
|
178
|
+
* Relative paths resolve from the working directory.
|
|
179
|
+
*/
|
|
180
|
+
setIcon(path) {
|
|
181
|
+
this._ensureOpen();
|
|
182
|
+
this._native.setIcon(path);
|
|
183
|
+
}
|
|
184
|
+
show() {
|
|
185
|
+
this._ensureOpen();
|
|
186
|
+
this._native.show();
|
|
187
|
+
}
|
|
188
|
+
hide() {
|
|
189
|
+
this._ensureOpen();
|
|
190
|
+
this._native.hide();
|
|
191
|
+
}
|
|
192
|
+
close() {
|
|
193
|
+
this._ensureOpen();
|
|
194
|
+
this._native.close();
|
|
195
|
+
}
|
|
196
|
+
focus() {
|
|
197
|
+
this._ensureOpen();
|
|
198
|
+
this._native.focus();
|
|
199
|
+
}
|
|
200
|
+
maximize() {
|
|
201
|
+
this._ensureOpen();
|
|
202
|
+
this._native.maximize();
|
|
203
|
+
}
|
|
204
|
+
minimize() {
|
|
205
|
+
this._ensureOpen();
|
|
206
|
+
this._native.minimize();
|
|
207
|
+
}
|
|
208
|
+
unmaximize() {
|
|
209
|
+
this._ensureOpen();
|
|
210
|
+
this._native.unmaximize();
|
|
211
|
+
}
|
|
212
|
+
reload() {
|
|
213
|
+
this._ensureOpen();
|
|
214
|
+
this._native.reload();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Register a handler for messages from the webview.
|
|
218
|
+
*
|
|
219
|
+
* @security **No origin filtering.** The raw `onMessage` API does not
|
|
220
|
+
* enforce origin restrictions. If your webview navigates to untrusted
|
|
221
|
+
* URLs, validate the `sourceUrl` parameter before processing messages.
|
|
222
|
+
* For automatic origin filtering, use `createChannel()` with the
|
|
223
|
+
* `trustedOrigins` option from `native-window-ipc`.
|
|
224
|
+
*
|
|
225
|
+
* @security **No rate limiting.** Messages from the webview are delivered
|
|
226
|
+
* without throttling. A malicious page can flood the host with messages.
|
|
227
|
+
* Consider implementing application-level rate limiting if loading
|
|
228
|
+
* untrusted content.
|
|
229
|
+
*/
|
|
230
|
+
onMessage(callback) {
|
|
231
|
+
this._ensureOpen();
|
|
232
|
+
this._native.onMessage(callback);
|
|
233
|
+
}
|
|
234
|
+
onResize(callback) {
|
|
235
|
+
this._ensureOpen();
|
|
236
|
+
this._native.onResize(callback);
|
|
237
|
+
}
|
|
238
|
+
onMove(callback) {
|
|
239
|
+
this._ensureOpen();
|
|
240
|
+
this._native.onMove(callback);
|
|
241
|
+
}
|
|
242
|
+
onFocus(callback) {
|
|
243
|
+
this._ensureOpen();
|
|
244
|
+
this._native.onFocus(callback);
|
|
245
|
+
}
|
|
246
|
+
onBlur(callback) {
|
|
247
|
+
this._ensureOpen();
|
|
248
|
+
this._native.onBlur(callback);
|
|
249
|
+
}
|
|
250
|
+
onPageLoad(callback) {
|
|
251
|
+
this._ensureOpen();
|
|
252
|
+
this._native.onPageLoad(callback);
|
|
253
|
+
}
|
|
254
|
+
onTitleChanged(callback) {
|
|
255
|
+
this._ensureOpen();
|
|
256
|
+
this._native.onTitleChanged(callback);
|
|
257
|
+
}
|
|
258
|
+
onReload(callback) {
|
|
259
|
+
this._ensureOpen();
|
|
260
|
+
this._native.onReload(callback);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Register a handler for blocked navigation events.
|
|
264
|
+
* Fired when a navigation is blocked by the {@link WindowOptions.allowedHosts}
|
|
265
|
+
* restriction. Receives the URL that was blocked.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* win.onNavigationBlocked((url) => {
|
|
270
|
+
* console.log("Blocked navigation to:", url);
|
|
271
|
+
* });
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
onNavigationBlocked(callback) {
|
|
275
|
+
this._ensureOpen();
|
|
276
|
+
this._native.onNavigationBlocked(callback);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Validate and parse a raw cookies JSON array from the native layer.
|
|
280
|
+
* Returns a cleaned {@link CookieInfo} array or `null` if the payload
|
|
281
|
+
* is malformed.
|
|
282
|
+
*
|
|
283
|
+
* @internal
|
|
284
|
+
*/
|
|
285
|
+
_validateCookies(raw) {
|
|
286
|
+
let parsed;
|
|
287
|
+
try {
|
|
288
|
+
parsed = JSON.parse(raw);
|
|
289
|
+
} catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
if (!Array.isArray(parsed)) return null;
|
|
293
|
+
const cookies = [];
|
|
294
|
+
for (const item of parsed) {
|
|
295
|
+
if (typeof item !== "object" || item === null) continue;
|
|
296
|
+
const obj = item;
|
|
297
|
+
if (typeof obj.name !== "string" || typeof obj.value !== "string") continue;
|
|
298
|
+
if (typeof obj.domain !== "string" || typeof obj.path !== "string") continue;
|
|
299
|
+
if (typeof obj.httpOnly !== "boolean" || typeof obj.secure !== "boolean") continue;
|
|
300
|
+
if (typeof obj.sameSite !== "string" || typeof obj.expires !== "number") continue;
|
|
301
|
+
cookies.push({
|
|
302
|
+
name: obj.name,
|
|
303
|
+
value: obj.value,
|
|
304
|
+
domain: obj.domain,
|
|
305
|
+
path: obj.path,
|
|
306
|
+
httpOnly: obj.httpOnly,
|
|
307
|
+
secure: obj.secure,
|
|
308
|
+
sameSite: obj.sameSite,
|
|
309
|
+
expires: obj.expires
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return cookies;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Query cookies from the native cookie store.
|
|
316
|
+
*
|
|
317
|
+
* Returns a Promise that resolves with validated {@link CookieInfo} objects,
|
|
318
|
+
* including `HttpOnly` cookies that are invisible to `document.cookie`.
|
|
319
|
+
*
|
|
320
|
+
* - **macOS**: Uses `WKHTTPCookieStore.getAllCookies` with client-side
|
|
321
|
+
* URL filtering (domain + path match).
|
|
322
|
+
* - **Windows**: Uses `ICoreWebView2CookieManager.GetCookies` which
|
|
323
|
+
* filters by URI natively.
|
|
324
|
+
*
|
|
325
|
+
* @param url If provided, only cookies matching this URL are returned.
|
|
326
|
+
* If omitted, all cookies in the webview's cookie store are returned.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* const cookies = await win.getCookies("https://example.com");
|
|
331
|
+
* const session = cookies.find((c) => c.name === "session_id");
|
|
332
|
+
* if (session) console.log("Session:", session.value, "HttpOnly:", session.httpOnly);
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
getCookies(url) {
|
|
336
|
+
this._ensureOpen();
|
|
337
|
+
return new Promise((resolve, reject) => {
|
|
338
|
+
const timeout = setTimeout(() => {
|
|
339
|
+
reject(/* @__PURE__ */ new Error("getCookies() timed out after 10 seconds"));
|
|
340
|
+
}, 1e4);
|
|
341
|
+
this._native.onCookies((raw) => {
|
|
342
|
+
clearTimeout(timeout);
|
|
343
|
+
const validated = this._validateCookies(raw);
|
|
344
|
+
if (validated) resolve(validated);
|
|
345
|
+
else reject(/* @__PURE__ */ new Error("Failed to parse cookie response"));
|
|
346
|
+
});
|
|
347
|
+
this._native.getCookies(url);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Clear cookies from the native cookie store.
|
|
352
|
+
*
|
|
353
|
+
* - If `host` is provided, only cookies whose domain matches that host
|
|
354
|
+
* are deleted (e.g. `"example.com"` deletes `.example.com` cookies).
|
|
355
|
+
* - If omitted, all cookies in the webview's cookie store are cleared.
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```ts
|
|
359
|
+
* // Clear all cookies
|
|
360
|
+
* win.clearCookies();
|
|
361
|
+
*
|
|
362
|
+
* // Clear cookies for a specific host
|
|
363
|
+
* win.clearCookies("example.com");
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
clearCookies(host) {
|
|
367
|
+
this._ensureOpen();
|
|
368
|
+
this._native.clearCookies(host);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
/**
|
|
372
|
+
* Escape a string for safe embedding inside a JavaScript string literal.
|
|
373
|
+
* Handles backslashes, double quotes, newlines, carriage returns, null
|
|
374
|
+
* bytes, closing `<\/script>` tags, Unicode line/paragraph separators
|
|
375
|
+
* (U+2028, U+2029), backticks, and `${` template expressions.
|
|
376
|
+
*
|
|
377
|
+
* Safe for use in double-quoted, single-quoted, and template literal
|
|
378
|
+
* contexts.
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```ts
|
|
382
|
+
* import { NativeWindow, sanitizeForJs } from "native-window";
|
|
383
|
+
*
|
|
384
|
+
* const userInput = 'He said "hello"\n<script>alert(1)<\/script>';
|
|
385
|
+
* win.unsafe.evaluateJs(`display("${sanitizeForJs(userInput)}")`);
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
397
388
|
function sanitizeForJs(input) {
|
|
398
|
-
|
|
389
|
+
return JSON.stringify(input).slice(1, -1).replace(/<\/script>/gi, "<\\/script>").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
399
390
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
checkRuntime,
|
|
403
|
-
ensureRuntime,
|
|
404
|
-
loadHtmlOrigin,
|
|
405
|
-
sanitizeForJs
|
|
406
|
-
};
|
|
391
|
+
//#endregion
|
|
392
|
+
export { NativeWindow, checkRuntime, ensureRuntime, loadHtmlOrigin, sanitizeForJs };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativewindow/webview",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Native OS webview windows for Bun & Node.js (beta)",
|
|
5
5
|
"homepage": "https://nativewindow.fcannizzaro.com",
|
|
6
6
|
"bugs": {
|
|
@@ -34,34 +34,40 @@
|
|
|
34
34
|
"exports": {
|
|
35
35
|
".": {
|
|
36
36
|
"types": "./dist/index.d.ts",
|
|
37
|
-
"import": "./dist/index.js"
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"default": "./dist/index.js"
|
|
38
39
|
}
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"build": "napi build --release --platform && vite build",
|
|
42
43
|
"build:debug": "napi build --platform && vite build",
|
|
43
44
|
"build:ts": "vite build",
|
|
44
|
-
"version": "napi version"
|
|
45
|
+
"version": "napi version",
|
|
46
|
+
"fuzz": "cargo +nightly fuzz list --fuzz-dir fuzz",
|
|
47
|
+
"fuzz:json-escape": "cargo +nightly fuzz run fuzz_json_escape --fuzz-dir fuzz",
|
|
48
|
+
"fuzz:extract-origin": "cargo +nightly fuzz run fuzz_extract_origin --fuzz-dir fuzz",
|
|
49
|
+
"fuzz:host-matching": "cargo +nightly fuzz run fuzz_host_matching --fuzz-dir fuzz",
|
|
50
|
+
"fuzz:url-scheme": "cargo +nightly fuzz run fuzz_url_scheme --fuzz-dir fuzz"
|
|
45
51
|
},
|
|
46
52
|
"dependencies": {
|
|
47
53
|
"zod": "^4.3.6"
|
|
48
54
|
},
|
|
49
55
|
"devDependencies": {
|
|
50
|
-
"@napi-rs/cli": "^3.
|
|
56
|
+
"@napi-rs/cli": "^3.6.0",
|
|
51
57
|
"@types/bun": "^1.3.9",
|
|
52
|
-
"vite": "^
|
|
58
|
+
"vite": "^8.0.3",
|
|
53
59
|
"vite-plugin-dts": "^4.5.4"
|
|
54
60
|
},
|
|
55
61
|
"peerDependencies": {
|
|
56
|
-
"typescript": "^
|
|
62
|
+
"typescript": "^6.0.2"
|
|
57
63
|
},
|
|
58
64
|
"optionalDependencies": {
|
|
59
|
-
"@nativewindow/webview-darwin-arm64": "1.0.
|
|
60
|
-
"@nativewindow/webview-darwin-x64": "1.0.
|
|
61
|
-
"@nativewindow/webview-linux-arm64-gnu": "1.0.
|
|
62
|
-
"@nativewindow/webview-linux-x64-gnu": "1.0.
|
|
63
|
-
"@nativewindow/webview-win32-arm64-msvc": "1.0.
|
|
64
|
-
"@nativewindow/webview-win32-x64-msvc": "1.0.
|
|
65
|
+
"@nativewindow/webview-darwin-arm64": "1.0.2",
|
|
66
|
+
"@nativewindow/webview-darwin-x64": "1.0.2",
|
|
67
|
+
"@nativewindow/webview-linux-arm64-gnu": "1.0.2",
|
|
68
|
+
"@nativewindow/webview-linux-x64-gnu": "1.0.2",
|
|
69
|
+
"@nativewindow/webview-win32-arm64-msvc": "1.0.2",
|
|
70
|
+
"@nativewindow/webview-win32-x64-msvc": "1.0.2"
|
|
65
71
|
},
|
|
66
72
|
"napi": {
|
|
67
73
|
"binaryName": "native-window",
|