@nativewindow/ipc 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/README.md +9 -9
- package/dist/client.js +148 -121
- package/dist/index.js +224 -133
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -115,16 +115,16 @@ Create a typed channel client inside the webview for use in bundled apps.
|
|
|
115
115
|
|
|
116
116
|
### Channel Options
|
|
117
117
|
|
|
118
|
-
| Option | Type
|
|
119
|
-
| ---------------------- |
|
|
118
|
+
| Option | Type | Default | Description |
|
|
119
|
+
| ---------------------- | ---------------------------------------- | ---------- | --------------------------------------------------------------------------------------------- |
|
|
120
120
|
| `schemas` | `{ host: SchemaMap; client: SchemaMap }` | _required_ | Directional schemas — `host` for events the host sends, `client` for events the webview sends |
|
|
121
|
-
| `injectClient` | `boolean`
|
|
122
|
-
| `onValidationError` | `function`
|
|
123
|
-
| `trustedOrigins` | `string[]`
|
|
124
|
-
| `maxMessageSize` | `number`
|
|
125
|
-
| `rateLimit` | `number`
|
|
126
|
-
| `maxListenersPerEvent` | `number`
|
|
127
|
-
| `channelId` | `string`
|
|
121
|
+
| `injectClient` | `boolean` | `true` | Auto-inject client script into webview |
|
|
122
|
+
| `onValidationError` | `function` | — | Called when a payload fails validation |
|
|
123
|
+
| `trustedOrigins` | `string[]` | — | Restrict IPC to specific origins |
|
|
124
|
+
| `maxMessageSize` | `number` | `1048576` | Max message size in bytes |
|
|
125
|
+
| `rateLimit` | `number` | — | Max messages per second |
|
|
126
|
+
| `maxListenersPerEvent` | `number` | — | Max listeners per event type |
|
|
127
|
+
| `channelId` | `string` | — | Unique channel identifier |
|
|
128
128
|
|
|
129
129
|
### Schema Support
|
|
130
130
|
|
package/dist/client.js
CHANGED
|
@@ -1,129 +1,156 @@
|
|
|
1
|
-
|
|
1
|
+
//#region client.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default maximum IPC message size (1 MB).
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
var MAX_MESSAGE_SIZE = 1048576;
|
|
2
7
|
function encode(type, payload) {
|
|
3
|
-
|
|
8
|
+
return JSON.stringify({
|
|
9
|
+
$ch: type,
|
|
10
|
+
p: payload
|
|
11
|
+
});
|
|
4
12
|
}
|
|
5
|
-
|
|
13
|
+
/** @internal Keys that could pollute prototypes if merged into target objects. */
|
|
14
|
+
var DANGEROUS_KEYS = new Set([
|
|
15
|
+
"__proto__",
|
|
16
|
+
"constructor",
|
|
17
|
+
"prototype"
|
|
18
|
+
]);
|
|
6
19
|
function decode(raw) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (typeof parsed === "object" && parsed !== null && "$ch" in parsed && typeof parsed.$ch === "string") {
|
|
14
|
-
return parsed;
|
|
15
|
-
}
|
|
16
|
-
} catch {
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
20
|
+
if (raw.length > MAX_MESSAGE_SIZE) return null;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(raw, (key, value) => DANGEROUS_KEYS.has(key) ? void 0 : value);
|
|
23
|
+
if (typeof parsed === "object" && parsed !== null && "$ch" in parsed && typeof parsed.$ch === "string") return parsed;
|
|
24
|
+
} catch {}
|
|
25
|
+
return null;
|
|
19
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate a payload against a schema using `safeParse()`.
|
|
29
|
+
* Returns the parsed data on success so that schema transforms are honored.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
20
32
|
function validatePayload(schema, data) {
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
const result = schema.safeParse(data);
|
|
34
|
+
return result.success ? {
|
|
35
|
+
success: true,
|
|
36
|
+
data: result.data
|
|
37
|
+
} : { success: false };
|
|
23
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a typed channel client for use inside the webview.
|
|
41
|
+
* Call this once; it hooks into the native IPC bridge.
|
|
42
|
+
*
|
|
43
|
+
* The client sends events defined by the `client` schemas and receives
|
|
44
|
+
* events defined by the `host` schemas — the inverse of the host side.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { z } from "zod";
|
|
49
|
+
* import { createChannelClient } from "native-window-ipc/client";
|
|
50
|
+
*
|
|
51
|
+
* const ch = createChannelClient({
|
|
52
|
+
* schemas: {
|
|
53
|
+
* host: { "update-title": z.string() },
|
|
54
|
+
* client: { "user-click": z.object({ x: z.number(), y: z.number() }) },
|
|
55
|
+
* },
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* ch.send("user-click", { x: 10, y: 20 }); // only client events
|
|
59
|
+
* ch.on("update-title", (title) => { // only host events
|
|
60
|
+
* document.title = title;
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
24
64
|
function createChannelClient(options) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
} catch {
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
Object.defineProperty(window, "__channel__", {
|
|
119
|
-
value: channel,
|
|
120
|
-
writable: false,
|
|
121
|
-
configurable: false
|
|
122
|
-
});
|
|
123
|
-
} catch {
|
|
124
|
-
}
|
|
125
|
-
return channel;
|
|
65
|
+
const { schemas, onValidationError } = options;
|
|
66
|
+
const hostSchemas = schemas.host;
|
|
67
|
+
schemas.client;
|
|
68
|
+
const _push = Array.prototype.push;
|
|
69
|
+
const _indexOf = Array.prototype.indexOf;
|
|
70
|
+
const _splice = Array.prototype.splice;
|
|
71
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
72
|
+
const channel = {
|
|
73
|
+
send(...args) {
|
|
74
|
+
const [type, payload] = args;
|
|
75
|
+
window.ipc.postMessage(encode(type, payload));
|
|
76
|
+
},
|
|
77
|
+
on(type, handler) {
|
|
78
|
+
let set = listeners.get(type);
|
|
79
|
+
if (!set) {
|
|
80
|
+
set = /* @__PURE__ */ new Set();
|
|
81
|
+
listeners.set(type, set);
|
|
82
|
+
}
|
|
83
|
+
set.add(handler);
|
|
84
|
+
},
|
|
85
|
+
off(type, handler) {
|
|
86
|
+
const set = listeners.get(type);
|
|
87
|
+
if (set) set.delete(handler);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const externalListeners = [];
|
|
91
|
+
const orig = window.__native_message__;
|
|
92
|
+
try {
|
|
93
|
+
Object.defineProperty(window, "__native_message__", {
|
|
94
|
+
value(msg) {
|
|
95
|
+
const env = decode(msg);
|
|
96
|
+
if (env) {
|
|
97
|
+
if (!(env.$ch in hostSchemas)) {
|
|
98
|
+
for (const fn of externalListeners) try {
|
|
99
|
+
fn(msg);
|
|
100
|
+
} catch {}
|
|
101
|
+
orig?.(msg);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const set = listeners.get(env.$ch);
|
|
105
|
+
if (set) {
|
|
106
|
+
const schema = hostSchemas[env.$ch];
|
|
107
|
+
let validatedPayload = env.p;
|
|
108
|
+
if (schema) {
|
|
109
|
+
const result = validatePayload(schema, env.p);
|
|
110
|
+
if (!result.success) {
|
|
111
|
+
onValidationError?.(env.$ch, env.p);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
validatedPayload = result.data;
|
|
115
|
+
}
|
|
116
|
+
for (const fn of set) try {
|
|
117
|
+
fn(validatedPayload);
|
|
118
|
+
} catch {}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const fn of externalListeners) try {
|
|
123
|
+
fn(msg);
|
|
124
|
+
} catch {}
|
|
125
|
+
orig?.(msg);
|
|
126
|
+
},
|
|
127
|
+
writable: false,
|
|
128
|
+
configurable: false
|
|
129
|
+
});
|
|
130
|
+
} catch {}
|
|
131
|
+
try {
|
|
132
|
+
Object.defineProperty(window, "__native_message_listeners__", {
|
|
133
|
+
value: Object.freeze({
|
|
134
|
+
add(fn) {
|
|
135
|
+
if (typeof fn === "function") _push.call(externalListeners, fn);
|
|
136
|
+
},
|
|
137
|
+
remove(fn) {
|
|
138
|
+
const idx = _indexOf.call(externalListeners, fn);
|
|
139
|
+
if (idx !== -1) _splice.call(externalListeners, idx, 1);
|
|
140
|
+
}
|
|
141
|
+
}),
|
|
142
|
+
writable: false,
|
|
143
|
+
configurable: false
|
|
144
|
+
});
|
|
145
|
+
} catch {}
|
|
146
|
+
try {
|
|
147
|
+
Object.defineProperty(window, "__channel__", {
|
|
148
|
+
value: channel,
|
|
149
|
+
writable: false,
|
|
150
|
+
configurable: false
|
|
151
|
+
});
|
|
152
|
+
} catch {}
|
|
153
|
+
return channel;
|
|
126
154
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
155
|
+
//#endregion
|
|
156
|
+
export { createChannelClient };
|
package/dist/index.js
CHANGED
|
@@ -1,54 +1,107 @@
|
|
|
1
1
|
import { NativeWindow } from "@nativewindow/webview";
|
|
2
|
-
|
|
2
|
+
//#region index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Default maximum IPC message size (1 MB).
|
|
5
|
+
* Messages exceeding this limit are silently dropped to prevent
|
|
6
|
+
* memory exhaustion from oversized payloads.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
var MAX_MESSAGE_SIZE = 1048576;
|
|
10
|
+
/**
|
|
11
|
+
* Validate a payload against a schema using `safeParse()`.
|
|
12
|
+
* Returns the parsed data on success so that schema transforms are honored.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
3
15
|
function validatePayload(schema, data) {
|
|
4
|
-
|
|
5
|
-
|
|
16
|
+
const result = schema.safeParse(data);
|
|
17
|
+
return result.success ? {
|
|
18
|
+
success: true,
|
|
19
|
+
data: result.data
|
|
20
|
+
} : { success: false };
|
|
6
21
|
}
|
|
7
22
|
function isEnvelope(data) {
|
|
8
|
-
|
|
23
|
+
return typeof data === "object" && data !== null && "$ch" in data && typeof data.$ch === "string";
|
|
9
24
|
}
|
|
10
25
|
function encode(type, payload) {
|
|
11
|
-
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
$ch: type,
|
|
28
|
+
p: payload
|
|
29
|
+
});
|
|
12
30
|
}
|
|
13
|
-
|
|
31
|
+
/** @internal Keys that could pollute prototypes if merged into target objects. */
|
|
32
|
+
var DANGEROUS_KEYS = new Set([
|
|
33
|
+
"__proto__",
|
|
34
|
+
"constructor",
|
|
35
|
+
"prototype"
|
|
36
|
+
]);
|
|
14
37
|
function decode(raw, maxSize = MAX_MESSAGE_SIZE) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
38
|
+
if (raw.length > maxSize) return null;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(raw, (key, value) => DANGEROUS_KEYS.has(key) ? void 0 : value);
|
|
41
|
+
return isEnvelope(parsed) ? parsed : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
25
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Normalize an origin string using the WHATWG URL Standard.
|
|
48
|
+
* Lowercases scheme/host, strips default ports (80/443), and
|
|
49
|
+
* strips userinfo — matching the Rust `url` crate behavior.
|
|
50
|
+
* Returns `null` for malformed URLs or opaque origins.
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
26
53
|
function normalizeOrigin(origin) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
try {
|
|
55
|
+
const o = new URL(origin).origin;
|
|
56
|
+
return o === "null" ? null : o;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
33
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if a URL's origin matches any of the trusted origins.
|
|
63
|
+
* The `trustedOrigins` array must already be normalized via
|
|
64
|
+
* {@link normalizeOrigin} for correct comparison.
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
34
67
|
function isOriginTrusted(url, trustedOrigins) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
try {
|
|
69
|
+
const parsed = new URL(url);
|
|
70
|
+
return trustedOrigins.includes(parsed.origin);
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
41
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Returns the webview-side channel client as a self-contained JS string.
|
|
77
|
+
* This is the same logic as `client.ts`, minified for injection.
|
|
78
|
+
* Can also be embedded in a `<script>` tag manually.
|
|
79
|
+
*
|
|
80
|
+
* When `options.channelId` is provided, the injected script prefixes all
|
|
81
|
+
* `$ch` values with the channel ID, matching the host-side behavior.
|
|
82
|
+
*
|
|
83
|
+
* **Note:** The injected client does not support payload validation.
|
|
84
|
+
* For client-side validation, use the bundled {@link createChannelClient}
|
|
85
|
+
* import from `native-window-ipc/client` with the `schemas` option.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* // Without namespace (default)
|
|
90
|
+
* const script = getClientScript();
|
|
91
|
+
*
|
|
92
|
+
* // With namespace
|
|
93
|
+
* const script = getClientScript({ channelId: "abc123" });
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
42
96
|
function getClientScript(options) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return `(function(){
|
|
97
|
+
const prefix = options?.channelId ?? "";
|
|
98
|
+
return `(function(){
|
|
46
99
|
var _slice=Array.prototype.slice;
|
|
47
100
|
var _filter=Array.prototype.filter;
|
|
48
101
|
var _push=Array.prototype.push;
|
|
49
102
|
var _indexOf=Array.prototype.indexOf;
|
|
50
103
|
var _splice=Array.prototype.splice;
|
|
51
|
-
var _pfx=${
|
|
104
|
+
var _pfx=${JSON.stringify(prefix)};
|
|
52
105
|
var _l=Object.create(null);
|
|
53
106
|
var _el=[];
|
|
54
107
|
function _e(t,p){return JSON.stringify({$ch:_pfx?_pfx+":"+t:t,p:p})}
|
|
@@ -70,108 +123,146 @@ try{Object.defineProperty(window,'__native_message_listeners__',{value:Object.fr
|
|
|
70
123
|
try{Object.defineProperty(window,'__channel__',{value:Object.freeze(ch),writable:false,configurable:false})}catch(e){console.error('[native-window] Failed to define __channel__:',e)}
|
|
71
124
|
})();`;
|
|
72
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Wrap an existing NativeWindow with a typed message channel.
|
|
128
|
+
*
|
|
129
|
+
* Schemas are required — they provide both TypeScript types and runtime
|
|
130
|
+
* validation for each event. Compatible with Zod v4, Valibot v1, and
|
|
131
|
+
* any schema library implementing the `safeParse()` interface.
|
|
132
|
+
*
|
|
133
|
+
* The `schemas` option uses directional groups:
|
|
134
|
+
* - `host`: events the host sends to the client.
|
|
135
|
+
* - `client`: events the client sends to the host.
|
|
136
|
+
*
|
|
137
|
+
* On the returned channel, `send()` only accepts `host` event types and
|
|
138
|
+
* `on()`/`off()` only accept `client` event types.
|
|
139
|
+
*
|
|
140
|
+
* @security **Origin restriction:** When `trustedOrigins` is configured,
|
|
141
|
+
* both client script injection and incoming IPC messages are restricted to
|
|
142
|
+
* pages whose URL origin matches the whitelist. The native `onMessage`
|
|
143
|
+
* callback now includes the source page URL, enabling the channel to reject
|
|
144
|
+
* messages from untrusted origins. Empty source URLs (e.g. `about:blank`,
|
|
145
|
+
* `data:` URIs) are treated as untrusted when `trustedOrigins` is set.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* import { z } from "zod";
|
|
150
|
+
* import { createChannel } from "native-window-ipc";
|
|
151
|
+
*
|
|
152
|
+
* const ch = createChannel(win, {
|
|
153
|
+
* schemas: {
|
|
154
|
+
* host: { "update-title": z.string() },
|
|
155
|
+
* client: { "user-click": z.object({ x: z.number(), y: z.number() }) },
|
|
156
|
+
* },
|
|
157
|
+
* });
|
|
158
|
+
* ch.send("update-title", "Hello"); // only host events
|
|
159
|
+
* ch.on("user-click", (p) => {}); // only client events, p: { x: number; y: number }
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
73
162
|
function createChannel(win, options) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
on(type, handler) {
|
|
154
|
-
if (!(type in clientSchemas)) return;
|
|
155
|
-
let set = listeners.get(type);
|
|
156
|
-
if (!set) {
|
|
157
|
-
set = /* @__PURE__ */ new Set();
|
|
158
|
-
listeners.set(type, set);
|
|
159
|
-
}
|
|
160
|
-
if (maxListenersPerEvent !== void 0 && set.size >= maxListenersPerEvent) return;
|
|
161
|
-
set.add(handler);
|
|
162
|
-
},
|
|
163
|
-
off(type, handler) {
|
|
164
|
-
const set = listeners.get(type);
|
|
165
|
-
if (set) set.delete(handler);
|
|
166
|
-
}
|
|
167
|
-
};
|
|
163
|
+
const { schemas, injectClient = true, onValidationError, trustedOrigins, maxMessageSize, rateLimit, maxListenersPerEvent, channelId: channelIdOpt } = options;
|
|
164
|
+
schemas.host;
|
|
165
|
+
const clientSchemas = schemas.client;
|
|
166
|
+
const channelId = channelIdOpt === true ? crypto.randomUUID().replace(/-/g, "").slice(0, 16) : channelIdOpt ?? "";
|
|
167
|
+
const normalizedOrigins = trustedOrigins?.map(normalizeOrigin).filter((o) => o !== null);
|
|
168
|
+
const prefixCh = (type) => channelId ? `${channelId}:${type}` : type;
|
|
169
|
+
const unprefixCh = (ch) => {
|
|
170
|
+
if (!channelId) return ch;
|
|
171
|
+
const pfx = `${channelId}:`;
|
|
172
|
+
return ch.startsWith(pfx) ? ch.slice(pfx.length) : null;
|
|
173
|
+
};
|
|
174
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
175
|
+
let _bucketTokens = rateLimit ?? 0;
|
|
176
|
+
let _bucketLastRefill = Date.now();
|
|
177
|
+
win.onMessage((raw, sourceUrl) => {
|
|
178
|
+
if (rateLimit !== void 0 && rateLimit > 0) {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
if (now - _bucketLastRefill >= 1e3) {
|
|
181
|
+
_bucketTokens = rateLimit;
|
|
182
|
+
_bucketLastRefill = now;
|
|
183
|
+
}
|
|
184
|
+
if (_bucketTokens <= 0) return;
|
|
185
|
+
_bucketTokens--;
|
|
186
|
+
}
|
|
187
|
+
const env = decode(raw, maxMessageSize);
|
|
188
|
+
if (!env) return;
|
|
189
|
+
const eventType = unprefixCh(env.$ch);
|
|
190
|
+
if (eventType === null) return;
|
|
191
|
+
if (normalizedOrigins && normalizedOrigins.length > 0) {
|
|
192
|
+
if (!isOriginTrusted(sourceUrl, normalizedOrigins)) return;
|
|
193
|
+
}
|
|
194
|
+
const set = listeners.get(eventType);
|
|
195
|
+
if (!set) return;
|
|
196
|
+
if (!(eventType in clientSchemas)) return;
|
|
197
|
+
const schema = clientSchemas[eventType];
|
|
198
|
+
let validatedPayload = env.p;
|
|
199
|
+
if (schema) {
|
|
200
|
+
const result = validatePayload(schema, env.p);
|
|
201
|
+
if (!result.success) {
|
|
202
|
+
onValidationError?.(eventType, env.p);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
validatedPayload = result.data;
|
|
206
|
+
}
|
|
207
|
+
for (const fn of set) try {
|
|
208
|
+
fn(validatedPayload);
|
|
209
|
+
} catch {}
|
|
210
|
+
});
|
|
211
|
+
if (injectClient) {
|
|
212
|
+
if (!normalizedOrigins || normalizedOrigins.length === 0) win.unsafe.evaluateJs(getClientScript({ channelId: channelId || void 0 }));
|
|
213
|
+
win.onPageLoad((event, url) => {
|
|
214
|
+
if (event !== "finished") return;
|
|
215
|
+
if (normalizedOrigins && normalizedOrigins.length > 0) {
|
|
216
|
+
if (!isOriginTrusted(url, normalizedOrigins)) return;
|
|
217
|
+
}
|
|
218
|
+
win.unsafe.evaluateJs(getClientScript({ channelId: channelId || void 0 }));
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
window: win,
|
|
223
|
+
send(...args) {
|
|
224
|
+
const [type, payload] = args;
|
|
225
|
+
win.postMessage(encode(prefixCh(type), payload));
|
|
226
|
+
},
|
|
227
|
+
on(type, handler) {
|
|
228
|
+
if (!(type in clientSchemas)) return;
|
|
229
|
+
let set = listeners.get(type);
|
|
230
|
+
if (!set) {
|
|
231
|
+
set = /* @__PURE__ */ new Set();
|
|
232
|
+
listeners.set(type, set);
|
|
233
|
+
}
|
|
234
|
+
if (maxListenersPerEvent !== void 0 && set.size >= maxListenersPerEvent) return;
|
|
235
|
+
set.add(handler);
|
|
236
|
+
},
|
|
237
|
+
off(type, handler) {
|
|
238
|
+
const set = listeners.get(type);
|
|
239
|
+
if (set) set.delete(handler);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
168
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Create a new NativeWindow and immediately wrap it with a typed channel.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* import { z } from "zod";
|
|
249
|
+
* import { createWindow } from "native-window-ipc";
|
|
250
|
+
*
|
|
251
|
+
* const ch = createWindow(
|
|
252
|
+
* { title: "My App" },
|
|
253
|
+
* {
|
|
254
|
+
* schemas: {
|
|
255
|
+
* host: { "update-title": z.string() },
|
|
256
|
+
* client: { "user-click": z.object({ x: z.number(), y: z.number() }) },
|
|
257
|
+
* },
|
|
258
|
+
* },
|
|
259
|
+
* );
|
|
260
|
+
* ch.send("update-title", "Hello"); // only host events
|
|
261
|
+
* ch.on("user-click", (p) => {}); // only client events
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
169
264
|
function createWindow(windowOptions, channelOptions) {
|
|
170
|
-
|
|
171
|
-
return createChannel(win, channelOptions);
|
|
265
|
+
return createChannel(new NativeWindow(windowOptions), channelOptions);
|
|
172
266
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
createWindow,
|
|
176
|
-
getClientScript
|
|
177
|
-
};
|
|
267
|
+
//#endregion
|
|
268
|
+
export { createChannel, createWindow, getClientScript };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativewindow/ipc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Typesafe IPC channels for native-window (beta)",
|
|
5
5
|
"homepage": "https://nativewindow.fcannizzaro.com",
|
|
6
6
|
"bugs": {
|
|
@@ -46,16 +46,16 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/bun": "^1.3.9",
|
|
49
|
-
"arktype": "^2.
|
|
50
|
-
"valibot": "^1.
|
|
51
|
-
"vite": "^
|
|
49
|
+
"arktype": "^2.2.0",
|
|
50
|
+
"valibot": "^1.3.1",
|
|
51
|
+
"vite": "^8.0.3",
|
|
52
52
|
"vite-plugin-dts": "^4.5.4",
|
|
53
53
|
"vitest": "^4.0.18",
|
|
54
54
|
"zod": "^4.3.6"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@nativewindow/webview": "workspace:*",
|
|
58
|
-
"typescript": "^
|
|
58
|
+
"typescript": "^6.0.2",
|
|
59
59
|
"zod": "^4.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|