@gjsify/iframe 0.4.10 → 0.4.12

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.
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{EventTarget as e,MessageEvent as t}from"@gjsify/dom-events";var IFrameMessagePort=class extends e{constructor(...e){super(...e),this._partner=null,this._transferred=!1,this._bridge=null,this._portId=null,this._started=!1,this._queue=[],this._closed=!1}postMessage(e){if(this._transferred)throw Error(`IFrameMessagePort.postMessage: port has been transferred`);if(!this._closed){if(this._bridge&&this._portId!==null){this._bridge._sendPortMessage(this._portId,e);return}this._partner&&!this._partner._closed&&this._partner._receive(e)}}start(){if(this._started||this._closed)return;this._started=!0;let e=this._queue;this._queue=[];for(let t of e)this._dispatch(t)}close(){this._closed||(this._closed=!0,this._bridge&&this._portId!==null&&(this._bridge._closePort(this._portId),this._bridge=null,this._portId=null),this._partner=null,this._queue=[])}_receive(e){if(!this._closed){if(!this._started){this._queue.push(e);return}this._dispatch(e)}}_dispatch(e){Promise.resolve().then(()=>{this._closed||this.dispatchEvent(new t(`message`,{data:e}))})}addEventListener(e,t,n){super.addEventListener(e,t,n),e===`message`&&this.start()}get[Symbol.toStringTag](){return`MessagePort`}},IFrameMessageChannel=class{constructor(){this.port1=new IFrameMessagePort,this.port2=new IFrameMessagePort,this.port1._partner=this.port2,this.port2._partner=this.port1}get[Symbol.toStringTag](){return`MessageChannel`}};export{IFrameMessageChannel,IFrameMessagePort};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{EventTarget as e}from"@gjsify/dom-events";var IFrameWindowProxy=class extends e{constructor(e){super(),this._closed=!1,this._bridge=e}postMessage(e,t=`*`){this._closed||this._bridge.sendToWebView(e,t)}get location(){return this._bridge.getLocation()}get parent(){return globalThis}get top(){return globalThis}get self(){return this}get window(){return this}get closed(){return this._closed}_close(){this._closed=!0}get[Symbol.toStringTag](){return`IFrameWindowProxy`}};export{IFrameWindowProxy};
1
+ import"./_virtual/_rolldown/runtime.js";import{EventTarget as e}from"@gjsify/dom-events";var IFrameWindowProxy=class extends e{constructor(e){super(),this._closed=!1,this._bridge=e}postMessage(e,t=`*`,n){this._closed||this._bridge.sendToWebView(e,t,n)}get location(){return this._bridge.getLocation()}get parent(){return globalThis}get top(){return globalThis}get self(){return this}get window(){return this}get closed(){return this._closed}_close(){this._closed=!0}get[Symbol.toStringTag](){return`IFrameWindowProxy`}};export{IFrameWindowProxy};
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{HTMLIFrameElement as e}from"./html-iframe-element.js";import{IFrameWindowProxy as t}from"./iframe-window-proxy.js";import{MessageBridge as n}from"./message-bridge.js";import{IFrameBridge as r}from"./iframe-bridge.js";import{Document as i}from"@gjsify/dom-elements";i.registerElementFactory(`iframe`,()=>new e),Object.defineProperty(globalThis,`HTMLIFrameElement`,{value:e,writable:!0,configurable:!0});export{e as HTMLIFrameElement,r as IFrameBridge,t as IFrameWindowProxy,n as MessageBridge};
1
+ import"./_virtual/_rolldown/runtime.js";import{HTMLIFrameElement as e}from"./html-iframe-element.js";import{IFrameWindowProxy as t}from"./iframe-window-proxy.js";import{GJS_HOST_ORIGIN as n,MessageBridge as r}from"./message-bridge.js";import{IFrameBridge as i}from"./iframe-bridge.js";import{IFrameMessageChannel as a,IFrameMessagePort as o}from"./iframe-message-channel.js";import{Document as s}from"@gjsify/dom-elements";s.registerElementFactory(`iframe`,()=>new e),Object.defineProperty(globalThis,`HTMLIFrameElement`,{value:e,writable:!0,configurable:!0});export{n as GJS_HOST_ORIGIN,e as HTMLIFrameElement,i as IFrameBridge,a as IFrameMessageChannel,o as IFrameMessagePort,t as IFrameWindowProxy,r as MessageBridge};
@@ -1,13 +1,123 @@
1
- import"./_virtual/_rolldown/runtime.js";import{MessageEvent as e}from"@gjsify/dom-events";import t from"gi://WebKit?version=6.0";import n from"gi://Gio?version=2.0";n._promisify(t.WebView.prototype,`evaluate_javascript`,`evaluate_javascript_finish`);const r=`gjsify-iframe`,i=`(function() {
2
- var handler = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers['${r}'];
1
+ import"./_virtual/_rolldown/runtime.js";import{BINARY_SERIALIZER_INJECTED_SRC as e,decodeBinariesFromJson as t,encodeBinariesForJson as n}from"./serialize.js";import{MessageEvent as r}from"@gjsify/dom-events";import i from"gi://WebKit?version=6.0";import a from"gi://Gio?version=2.0";a._promisify(i.WebView.prototype,`evaluate_javascript`,`evaluate_javascript_finish`);const o=`gjsify-iframe`,s=`https://gjsify.local`;function normaliseTargetOrigin(e){if(e===`*`)return`*`;if(e===`/`)return null;try{return new URL(e).origin}catch{let t=Error(`Invalid target origin '${e}'`);throw t.name=`SyntaxError`,t}}const BOOTSTRAP_SCRIPT_FOR_TEST=()=>c,c=`(function() {
2
+ var handler = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers['${o}'];
3
3
  if (!handler) return;
4
+
5
+ // Idempotency guard: WebKit auto-injects this script at INJECTION_TIME.START
6
+ // for every page load. On rapid reload, srcdoc remount, or
7
+ // history.replaceState shenanigans, the script may run twice in the
8
+ // same window without an intervening real navigation. If we
9
+ // overwrite our previous override blindly we'd capture OUR override
10
+ // as origPostMessage, losing the real one — and any consumer using
11
+ // __gjsifyBridge.origPostMessage would loop forever.
12
+ if (window.__gjsifyBridge && window.__gjsifyBridge.__bridgeVersion === 1) return;
13
+
14
+ var GJS_HOST_ORIGIN = '${s}';
15
+
16
+ ${e}
17
+
18
+ // Per-WebView registry of proxy ports created during GJS → WebView
19
+ // postMessage. Keyed by the GJS-allocated portId. Each entry has a
20
+ // .deliver(payload) hook the GJS host can invoke via evaluate_javascript
21
+ // to dispatch a 'message' event on the proxy port.
22
+ var __gjsifyPorts = {};
23
+
24
+ function __makeProxyPort(portId) {
25
+ var listeners = [];
26
+ var started = false;
27
+ var queued = [];
28
+ function dispatch(d) {
29
+ var ev = { data: d, type: 'message' };
30
+ for (var i = 0; i < listeners.length; i++) {
31
+ try { listeners[i].call(undefined, ev); } catch (_) {}
32
+ }
33
+ }
34
+ function drain() {
35
+ while (queued.length > 0) dispatch(queued.shift());
36
+ }
37
+ var port = {
38
+ postMessage: function(d) {
39
+ handler.postMessage(JSON.stringify({
40
+ __gjsifyPortMessage: portId,
41
+ payload: __encodeBin(d)
42
+ }));
43
+ },
44
+ addEventListener: function(type, fn) {
45
+ if (type !== 'message' || typeof fn !== 'function') return;
46
+ listeners.push(fn);
47
+ if (!started) { started = true; drain(); }
48
+ },
49
+ removeEventListener: function(type, fn) {
50
+ if (type !== 'message') return;
51
+ var i = listeners.indexOf(fn);
52
+ if (i !== -1) listeners.splice(i, 1);
53
+ },
54
+ start: function() { if (!started) { started = true; drain(); } },
55
+ close: function() {
56
+ handler.postMessage(JSON.stringify({ __gjsifyPortClose: portId }));
57
+ listeners = [];
58
+ queued = [];
59
+ delete __gjsifyPorts[portId];
60
+ },
61
+ };
62
+ Object.defineProperty(port, 'onmessage', {
63
+ get: function() { return port.__onmessage || null; },
64
+ set: function(fn) {
65
+ if (port.__onmessage) port.removeEventListener('message', port.__onmessage);
66
+ port.__onmessage = fn;
67
+ if (typeof fn === 'function') port.addEventListener('message', fn);
68
+ },
69
+ });
70
+ __gjsifyPorts[portId] = {
71
+ deliver: function(d) {
72
+ if (started) dispatch(d); else queued.push(d);
73
+ },
74
+ };
75
+ return port;
76
+ }
77
+
78
+ // Walk an incoming GJS → WebView payload for {__gjsifyPort: id}
79
+ // placeholders and replace each with a proxy port instance.
80
+ function __substitutePorts(v) {
81
+ if (v === null || typeof v !== 'object') return v;
82
+ if (typeof v.__gjsifyPort === 'number') return __makeProxyPort(v.__gjsifyPort);
83
+ if (Array.isArray(v)) { for (var i=0;i<v.length;i++) v[i]=__substitutePorts(v[i]); return v; }
84
+ if (__classOf(v) === 'Object') {
85
+ for (var k in v) if (Object.prototype.hasOwnProperty.call(v,k)) v[k]=__substitutePorts(v[k]);
86
+ return v;
87
+ }
88
+ return v;
89
+ }
90
+ window.__gjsifyPortRegistry = __gjsifyPorts;
91
+ window.__gjsifySubstitutePorts = __substitutePorts;
92
+
93
+ function normaliseOrigin(t) {
94
+ if (t === '*') return '*';
95
+ if (t === '/') return location.origin; // source own-origin shortcut
96
+ try { return new URL(t).origin; }
97
+ catch (_) {
98
+ var err = new SyntaxError("Invalid target origin '" + t + "'");
99
+ throw err;
100
+ }
101
+ }
102
+
4
103
  function bridgePostMessage(data, targetOrigin) {
104
+ var t = targetOrigin || '*';
105
+ var resolved = normaliseOrigin(t);
106
+ // Drop silently if the targetOrigin doesn't match GJS host origin.
107
+ // '*' matches anything; otherwise must equal GJS_HOST_ORIGIN.
108
+ if (resolved !== '*' && resolved !== GJS_HOST_ORIGIN) return;
5
109
  handler.postMessage(JSON.stringify({
6
- data: data,
7
- targetOrigin: targetOrigin || '*',
110
+ data: __encodeBin(data),
111
+ targetOrigin: resolved,
8
112
  origin: location.origin
9
113
  }));
10
114
  }
115
+
116
+ // GJS → WebView messages come in via window.dispatchEvent(new MessageEvent(...))
117
+ // injected by evaluate_javascript. We can't intercept that path, so the
118
+ // injection itself decodes placeholders before constructing MessageEvent —
119
+ // see MessageBridge.sendToWebView in message-bridge.ts.
120
+
11
121
  // In a WebKit.WebView loaded via srcdoc, window.parent === window (no real iframe nesting).
12
122
  // window.parent is [LegacyUnforgeable] — cannot be redefined with defineProperty.
13
123
  // Instead, override window.postMessage directly. Since window.parent === window,
@@ -16,6 +126,11 @@ import"./_virtual/_rolldown/runtime.js";import{MessageEvent as e}from"@gjsify/do
16
126
  window.postMessage = function(data, targetOrigin) {
17
127
  bridgePostMessage(data, targetOrigin);
18
128
  };
19
- // Also expose on a safe namespace for explicit use
20
- window.__gjsifyBridge = { postMessage: bridgePostMessage, origPostMessage: origPostMessage };
21
- })();`;var MessageBridge=class{constructor(e){this._windowProxy=null,this._currentUri=`about:blank`,this._signalId=null,this._webView=e,this._userContentManager=e.get_user_content_manager(),this._setupReceiver(),this._injectBootstrapScript()}setWindowProxy(e){this._windowProxy=e}updateUri(e){this._currentUri=e}getLocation(){let e;try{e=new URL(this._currentUri).origin}catch{e=`null`}return{href:this._currentUri,origin:e}}sendToWebView(e,t){let n=JSON.stringify(e),r=JSON.stringify(`gjsify`),i=`window.dispatchEvent(new MessageEvent('message', { data: JSON.parse(${JSON.stringify(n)}), origin: ${r} }));`;this._webView.evaluate_javascript(i,-1,null,null,null).catch(()=>{})}destroy(){this._signalId!==null&&(this._userContentManager.disconnect(this._signalId),this._signalId=null),this._userContentManager.unregister_script_message_handler(r,null),this._windowProxy=null}_setupReceiver(){this._userContentManager.register_script_message_handler(r,null),this._signalId=this._userContentManager.connect(`script-message-received::${r}`,(t,n)=>{if(this._windowProxy)try{let t=n.to_string(),r=JSON.parse(t),i=new e(`message`,{data:r.data,origin:r.origin});this._windowProxy.dispatchEvent(i)}catch(e){console.error(`[IFrame MessageBridge] Error processing message:`,e)}})}_injectBootstrapScript(){let e=new t.UserScript(i,t.UserContentInjectedFrames.ALL_FRAMES,t.UserScriptInjectionTime.START,null,null);this._userContentManager.add_script(e)}};export{MessageBridge};
129
+ // Also expose on a safe namespace for explicit use. Version-tag
130
+ // the bridge so the idempotency guard above can detect a re-run.
131
+ window.__gjsifyBridge = {
132
+ __bridgeVersion: 1,
133
+ postMessage: bridgePostMessage,
134
+ origPostMessage: origPostMessage,
135
+ };
136
+ })();`;var MessageBridge=class{constructor(e){this._windowProxy=null,this._currentUri=`about:blank`,this._signalId=null,this._ports=new Map,this._nextPortId=1,this._webView=e,this._userContentManager=e.get_user_content_manager(),this._setupReceiver(),this._injectBootstrapScript()}setWindowProxy(e){this._windowProxy=e}updateUri(e){this._currentUri=e}getLocation(){let e;try{e=new URL(this._currentUri).origin}catch{e=`null`}return{href:this._currentUri,origin:e}}_registerTransferredPort(e){if(e._transferred)throw Error(`IFrameMessagePort: already transferred`);let t=e._partner;if(!t)throw Error(`IFrameMessagePort: partner missing — port already transferred or closed`);let n=this._nextPortId++;return e._transferred=!0,e._partner=null,t._partner=null,t._bridge=this,t._portId=n,this._ports.set(n,t),n}_sendPortMessage(t,r){let i=n(r),a=JSON.stringify(i),o=`(function(){${e}var p = window.__gjsifyPortRegistry && window.__gjsifyPortRegistry[${t}]; if (p) p.deliver(__decodeBin(JSON.parse(${JSON.stringify(a)})));})();`;this._webView.evaluate_javascript(o,-1,null,null,null).catch(()=>{})}_closePort(e){this._ports.delete(e);let t=`(function(){ if (window.__gjsifyPortRegistry) delete window.__gjsifyPortRegistry[${e}]; })();`;this._webView.evaluate_javascript(t,-1,null,null,null).catch(()=>{})}sendToWebView(t,r,i){let a=normaliseTargetOrigin(r);if(a!==`*`){let e=this.getLocation().origin;if((a??`https://gjsify.local`)!==e)return}let o=t;if(i&&i.length>0){let e=new Map;for(let t of i){let n=this._registerTransferredPort(t);e.set(t,n)}o=substitutePorts(t,e)}let c=n(o),l=JSON.stringify(c),u=JSON.stringify(s),d=`(function(){${e}var d = __decodeBin(JSON.parse(${JSON.stringify(l)})); if (window.__gjsifySubstitutePorts) d = window.__gjsifySubstitutePorts(d); window.dispatchEvent(new MessageEvent('message', { data: d, origin: ${u} }));})();`;this._webView.evaluate_javascript(d,-1,null,null,null).catch(()=>{})}destroy(){this._signalId!==null&&(this._userContentManager.disconnect(this._signalId),this._signalId=null),this._userContentManager.unregister_script_message_handler(o,null),this._windowProxy=null}_setupReceiver(){this._userContentManager.register_script_message_handler(o,null),this._signalId=this._userContentManager.connect(`script-message-received::${o}`,(e,n)=>{if(this._windowProxy)try{let e=n.to_string(),i=JSON.parse(e);if(typeof i.__gjsifyPortMessage==`number`){let e=i,n=this._ports.get(e.__gjsifyPortMessage);n&&n._receive(t(e.payload));return}if(typeof i.__gjsifyPortClose==`number`){let e=i,t=this._ports.get(e.__gjsifyPortClose);t&&t.close(),this._ports.delete(e.__gjsifyPortClose);return}let a=i,o=a.targetOrigin;if(o!==`*`&&o!==`https://gjsify.local`)return;let s=new r(`message`,{data:t(a.data),origin:a.origin});this._windowProxy.dispatchEvent(s)}catch(e){console.error(`[IFrame MessageBridge] Error processing message:`,e)}})}_injectBootstrapScript(){let e=new i.UserScript(c,i.UserContentInjectedFrames.ALL_FRAMES,i.UserScriptInjectionTime.START,null,null);this._userContentManager.add_script(e)}};function substitutePorts(e,t){let n=new WeakMap;function walk(e){if(typeof e!=`object`||!e)return e;if(e[Symbol.toStringTag]===`MessagePort`&&t.has(e))return{__gjsifyPort:t.get(e)};if(n.has(e))return n.get(e);if(Array.isArray(e)){let t=[];n.set(e,t);for(let n=0;n<e.length;n++)t[n]=walk(e[n]);return t}if(Object.prototype.toString.call(e).slice(8,-1)===`Object`){let t={};n.set(e,t);for(let n of Object.keys(e))t[n]=walk(e[n]);return t}return e}return walk(e)}export{BOOTSTRAP_SCRIPT_FOR_TEST,s as GJS_HOST_ORIGIN,MessageBridge,normaliseTargetOrigin};
@@ -0,0 +1,58 @@
1
+ import"./_virtual/_rolldown/runtime.js";const e=[`ArrayBuffer`,`Uint8Array`,`Uint8ClampedArray`,`Int8Array`,`Uint16Array`,`Int16Array`,`Uint32Array`,`Int32Array`,`BigUint64Array`,`BigInt64Array`,`Float32Array`,`Float64Array`,`DataView`];function classOf(e){return Object.prototype.toString.call(e).slice(8,-1)}function isBinaryType(t){return e.includes(t)}function bytesToBase64(e){let t=e;if(typeof t.toBase64==`function`)return t.toBase64();let n=``;for(let t=0;t<e.length;t++)n+=String.fromCharCode(e[t]);return globalThis.btoa(n)}function base64ToBytes(e){let t=Uint8Array;if(typeof t.fromBase64==`function`)return t.fromBase64(e);let n=globalThis.atob(e),r=new Uint8Array(n.length);for(let e=0;e<n.length;e++)r[e]=n.charCodeAt(e);return r}function makePlaceholder(e){let t=classOf(e);if(e instanceof ArrayBuffer)return{__gjsifyBin:`b64`,type:t,data:bytesToBase64(new Uint8Array(e))};if(e instanceof DataView)return{__gjsifyBin:`b64`,type:`DataView`,data:bytesToBase64(new Uint8Array(e.buffer,e.byteOffset,e.byteLength))};let n=e;return{__gjsifyBin:`b64`,type:t,data:bytesToBase64(new Uint8Array(n.buffer,n.byteOffset,n.byteLength)),length:n.length}}function reconstructFromPlaceholder(e){let t=base64ToBytes(e.data);switch(e.type){case`ArrayBuffer`:return t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength);case`DataView`:return new DataView(t.buffer,t.byteOffset,t.byteLength);case`Uint8Array`:return t;case`Uint8ClampedArray`:return new Uint8ClampedArray(t.buffer,t.byteOffset,t.byteLength);case`Int8Array`:return new Int8Array(t.buffer,t.byteOffset,t.byteLength);case`Uint16Array`:return new Uint16Array(t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength));case`Int16Array`:return new Int16Array(t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength));case`Uint32Array`:return new Uint32Array(t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength));case`Int32Array`:return new Int32Array(t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength));case`BigUint64Array`:return new BigUint64Array(t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength));case`BigInt64Array`:return new BigInt64Array(t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength));case`Float32Array`:return new Float32Array(t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength));case`Float64Array`:return new Float64Array(t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength))}}function isPlaceholder(e){return typeof e==`object`&&!!e&&e.__gjsifyBin===`b64`&&typeof e.type==`string`&&typeof e.data==`string`}function encodeBinariesForJson(e){let t=new WeakMap;function walk(e){if(typeof e!=`object`||!e)return e;let n=classOf(e);if(isBinaryType(n))return makePlaceholder(e);if(t.has(e))return t.get(e);if(Array.isArray(e)){let n=[];t.set(e,n);for(let t=0;t<e.length;t++)n[t]=walk(e[t]);return n}if(n===`Object`){let n={};t.set(e,n);for(let t of Object.keys(e))n[t]=walk(e[t]);return n}return e}return walk(e)}function decodeBinariesFromJson(e){let t=new WeakSet;function walk(e){if(typeof e!=`object`||!e)return e;if(isPlaceholder(e))return reconstructFromPlaceholder(e);if(t.has(e))return e;if(t.add(e),Array.isArray(e)){for(let t=0;t<e.length;t++)e[t]=walk(e[t]);return e}if(classOf(e)===`Object`){for(let t of Object.keys(e))e[t]=walk(e[t]);return e}return e}return walk(e)}const t=`
2
+ var __binKey = '__gjsifyBin';
3
+ var __binVal = 'b64';
4
+ var __binCtors = ${JSON.stringify(e)};
5
+ function __classOf(v){ return Object.prototype.toString.call(v).slice(8,-1); }
6
+ function __isBin(n){ return __binCtors.indexOf(n) !== -1; }
7
+ function __b64enc(bytes){
8
+ if (typeof bytes.toBase64 === 'function') return bytes.toBase64();
9
+ var s=''; for (var i=0;i<bytes.length;i++) s+=String.fromCharCode(bytes[i]);
10
+ return btoa(s);
11
+ }
12
+ function __b64dec(s){
13
+ if (typeof Uint8Array.fromBase64 === 'function') return Uint8Array.fromBase64(s);
14
+ var bin = atob(s); var u = new Uint8Array(bin.length);
15
+ for (var i=0;i<bin.length;i++) u[i] = bin.charCodeAt(i);
16
+ return u;
17
+ }
18
+ function __mkPlaceholder(v){
19
+ var t = __classOf(v);
20
+ if (v instanceof ArrayBuffer) return {__gjsifyBin:__binVal, type:t, data:__b64enc(new Uint8Array(v))};
21
+ if (v instanceof DataView) return {__gjsifyBin:__binVal, type:'DataView', data:__b64enc(new Uint8Array(v.buffer, v.byteOffset, v.byteLength))};
22
+ var ta = v; return {__gjsifyBin:__binVal, type:t, data:__b64enc(new Uint8Array(ta.buffer, ta.byteOffset, ta.byteLength)), length:ta.length};
23
+ }
24
+ function __reconstruct(p){
25
+ var bytes = __b64dec(p.data);
26
+ switch (p.type) {
27
+ case 'ArrayBuffer': return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
28
+ case 'DataView': return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
29
+ case 'Uint8Array': return bytes;
30
+ case 'Uint8ClampedArray': return new Uint8ClampedArray(bytes.buffer, bytes.byteOffset, bytes.byteLength);
31
+ case 'Int8Array': return new Int8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
32
+ case 'Uint16Array': return new Uint16Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
33
+ case 'Int16Array': return new Int16Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
34
+ case 'Uint32Array': return new Uint32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
35
+ case 'Int32Array': return new Int32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
36
+ case 'BigUint64Array': return new BigUint64Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
37
+ case 'BigInt64Array': return new BigInt64Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
38
+ case 'Float32Array': return new Float32Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
39
+ case 'Float64Array': return new Float64Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
40
+ }
41
+ }
42
+ function __isPlaceholder(v){ return v && typeof v==='object' && v[__binKey]===__binVal && typeof v.type==='string' && typeof v.data==='string'; }
43
+ function __encodeBin(v){
44
+ if (v === null || typeof v !== 'object') return v;
45
+ var c = __classOf(v);
46
+ if (__isBin(c)) return __mkPlaceholder(v);
47
+ if (Array.isArray(v)) { var o=[]; for (var i=0;i<v.length;i++) o[i]=__encodeBin(v[i]); return o; }
48
+ if (c==='Object') { var o={}; for (var k in v) if (Object.prototype.hasOwnProperty.call(v,k)) o[k]=__encodeBin(v[k]); return o; }
49
+ return v;
50
+ }
51
+ function __decodeBin(v){
52
+ if (v === null || typeof v !== 'object') return v;
53
+ if (__isPlaceholder(v)) return __reconstruct(v);
54
+ if (Array.isArray(v)) { for (var i=0;i<v.length;i++) v[i]=__decodeBin(v[i]); return v; }
55
+ if (__classOf(v)==='Object') { for (var k in v) if (Object.prototype.hasOwnProperty.call(v,k)) v[k]=__decodeBin(v[k]); return v; }
56
+ return v;
57
+ }
58
+ `;export{t as BINARY_SERIALIZER_INJECTED_SRC,decodeBinariesFromJson,encodeBinariesForJson};
@@ -0,0 +1,38 @@
1
+ import { EventTarget } from '@gjsify/dom-events';
2
+ import type { MessageBridge } from './message-bridge.js';
3
+ export declare class IFrameMessagePort extends EventTarget {
4
+ /** @internal In-process partner; null when this port has been transferred. */
5
+ _partner: IFrameMessagePort | null;
6
+ /** @internal Set once this port has been passed in a transferList — can no longer post locally. */
7
+ _transferred: boolean;
8
+ /** @internal Set when the partner of this port has been transferred — this port now routes via the bridge. */
9
+ _bridge: MessageBridge | null;
10
+ /** @internal Bridge-side identifier when this port is wired to a remote endpoint. */
11
+ _portId: number | null;
12
+ /** @internal W3C: dispatching is gated until .start() (or first addEventListener('message')). */
13
+ _started: boolean;
14
+ /** @internal Messages received before .start() */
15
+ _queue: unknown[];
16
+ /** @internal Closed by close() */
17
+ _closed: boolean;
18
+ postMessage(data: unknown): void;
19
+ /**
20
+ * W3C: enables dispatching of queued messages. Explicit `start()`
21
+ * is optional — adding a 'message' listener via addEventListener
22
+ * auto-starts. This method exists for explicit control + .onmessage
23
+ * setter compatibility (not implemented yet).
24
+ */
25
+ start(): void;
26
+ close(): void;
27
+ /** @internal Called by the bridge or the in-process partner. */
28
+ _receive(data: unknown): void;
29
+ private _dispatch;
30
+ addEventListener(type: string, listener: any, options?: any): void;
31
+ get [Symbol.toStringTag](): string;
32
+ }
33
+ export declare class IFrameMessageChannel {
34
+ readonly port1: IFrameMessagePort;
35
+ readonly port2: IFrameMessagePort;
36
+ constructor();
37
+ get [Symbol.toStringTag](): string;
38
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -1,5 +1,6 @@
1
1
  import { EventTarget } from '@gjsify/dom-events';
2
2
  import type { MessageBridge } from './message-bridge.js';
3
+ import type { IFrameMessagePort } from './iframe-message-channel.js';
3
4
  /**
4
5
  * Lightweight Window-like proxy returned by `HTMLIFrameElement.contentWindow`.
5
6
  *
@@ -20,10 +21,16 @@ export declare class IFrameWindowProxy extends EventTarget {
20
21
  /**
21
22
  * Send a message to the iframe content.
22
23
  *
23
- * @param message - Data to send (must be JSON-serializable)
24
- * @param targetOrigin - Target origin for the message. Default: '*'
24
+ * @param message - Data to send (must be JSON-serializable + base64-encodable
25
+ * binaries see @gjsify/iframe/serialize for supported binary types).
26
+ * @param targetOrigin - Target origin for the message. Default: '*'.
27
+ * @param transfer - Optional list of `IFrameMessagePort` instances to
28
+ * transfer. Each transferred port is detached locally; its surviving
29
+ * partner becomes the GJS-side endpoint of a bidirectional channel
30
+ * routed through the bridge. The WebView receives proxy ports under
31
+ * `MessageEvent.data` wherever the original ports appeared in `message`.
25
32
  */
26
- postMessage(message: unknown, targetOrigin?: string): void;
33
+ postMessage(message: unknown, targetOrigin?: string, transfer?: IFrameMessagePort[]): void;
27
34
  /**
28
35
  * Read-only location reflecting the current WebView URI.
29
36
  */
@@ -1,5 +1,6 @@
1
1
  export { HTMLIFrameElement } from './html-iframe-element.js';
2
2
  export { IFrameBridge } from './iframe-bridge.js';
3
3
  export { IFrameWindowProxy } from './iframe-window-proxy.js';
4
- export { MessageBridge } from './message-bridge.js';
4
+ export { MessageBridge, GJS_HOST_ORIGIN } from './message-bridge.js';
5
+ export { IFrameMessageChannel, IFrameMessagePort } from './iframe-message-channel.js';
5
6
  export type { IFrameBridgeOptions, IFrameReadyCallback, IFrameMessageData } from './types/index.js';
@@ -1,5 +1,46 @@
1
1
  import WebKit from 'gi://WebKit?version=6.0';
2
2
  import type { IFrameWindowProxy } from './iframe-window-proxy.js';
3
+ import type { IFrameMessagePort } from './iframe-message-channel.js';
4
+ /**
5
+ * Synthetic origin attached to messages travelling FROM the GJS host
6
+ * INTO the WebView. The WebView can use this in a targetOrigin filter to
7
+ * accept messages only when they originate from its hosting GJS process
8
+ * (vs. any other code that might inject script via developer tools).
9
+ *
10
+ * Uses the `https://` scheme so WHATWG URL parsing gives a real origin
11
+ * string (non-special schemes like `gjsify://` return `null` as origin
12
+ * per the URL spec, which would break the equality comparison the
13
+ * bridge does on every message). `.local` is the standard RFC 6762
14
+ * suffix for non-routable mDNS names, so it can't collide with a real
15
+ * site.
16
+ */
17
+ export declare const GJS_HOST_ORIGIN = "https://gjsify.local";
18
+ /**
19
+ * Per HTML spec for window.postMessage:
20
+ * - '*' → no origin restriction
21
+ * - URL string → only deliver if destination origin matches URL.origin
22
+ * - '/' → only deliver if destination origin matches source origin
23
+ * - other → throw SyntaxError
24
+ *
25
+ * Returns the canonical origin string (e.g. 'https://example.com'),
26
+ * `'*'`, or `null` if the input is `'/'`. Throws `SyntaxError` for
27
+ * malformed input.
28
+ */
29
+ export declare function normaliseTargetOrigin(targetOrigin: string): string | null;
30
+ /**
31
+ * Bootstrap script injected into every WebView page at document start.
32
+ * Provides the `window.parent.postMessage()` bridge from WebView content back to GJS.
33
+ *
34
+ * The script:
35
+ * 1. Gets the WebKit message handler registered under CHANNEL_NAME
36
+ * 2. Creates a parent proxy with a postMessage() that sends via the WebKit handler
37
+ * 3. Overrides window.parent to point to the proxy
38
+ */
39
+ /**
40
+ * @internal Exposed only for unit testing — verifies the bootstrap
41
+ * idempotency guard survives refactors. Not part of the public API.
42
+ */
43
+ export declare const BOOTSTRAP_SCRIPT_FOR_TEST: () => string;
3
44
  /**
4
45
  * Manages bidirectional postMessage communication between GJS and a WebKit.WebView.
5
46
  *
@@ -17,6 +58,11 @@ export declare class MessageBridge {
17
58
  private _windowProxy;
18
59
  private _currentUri;
19
60
  private _signalId;
61
+ /** GJS-side endpoints of transferred ports, keyed by per-bridge id.
62
+ * When the WebView sends a `{__gjsifyPortMessage: id, payload}` envelope,
63
+ * we route the payload to `_ports.get(id)._receive(decoded)`. */
64
+ private _ports;
65
+ private _nextPortId;
20
66
  constructor(webView: WebKit.WebView);
21
67
  /** Connect the IFrameWindowProxy that will receive messages from the WebView */
22
68
  setWindowProxy(proxy: IFrameWindowProxy): void;
@@ -31,7 +77,26 @@ export declare class MessageBridge {
31
77
  * Send a message from GJS to the WebView content.
32
78
  * Dispatches a standard MessageEvent on the WebView's window object.
33
79
  */
34
- sendToWebView(data: unknown, _targetOrigin: string): void;
80
+ /**
81
+ * Register a port pair for cross-bridge transfer. Called by
82
+ * IFrameWindowProxy.postMessage when it sees an IFrameMessagePort
83
+ * in the transferList. Returns the port-id placeholder that should
84
+ * be substituted into the outgoing payload.
85
+ *
86
+ * Marks the transferred port as detached locally and wires its
87
+ * surviving partner so the partner's postMessage routes back over
88
+ * the bridge.
89
+ */
90
+ _registerTransferredPort(port: IFrameMessagePort): number;
91
+ /** @internal Called by IFrameMessagePort.postMessage when its partner
92
+ * was transferred to the WebView. Dispatches the data onto the
93
+ * WebView-side proxy port via evaluate_javascript. */
94
+ _sendPortMessage(portId: number, data: unknown): void;
95
+ /** @internal Called by IFrameMessagePort.close to tear down the
96
+ * bridge-side registration. The WebView side keeps its proxy port
97
+ * alive in user-script land but subsequent .deliver calls go nowhere. */
98
+ _closePort(portId: number): void;
99
+ sendToWebView(data: unknown, targetOrigin: string, transfer?: IFrameMessagePort[]): void;
35
100
  /** Clean up signal handlers */
36
101
  destroy(): void;
37
102
  /**
@@ -39,6 +104,12 @@ export declare class MessageBridge {
39
104
  * Registers a script message handler and connects to the signal.
40
105
  */
41
106
  private _setupReceiver;
107
+ /**
108
+ * Walk the user payload and replace each IFrameMessagePort instance
109
+ * (transferred via the transferList) with a {__gjsifyPort: id}
110
+ * placeholder. Non-transferred ports are passed through untouched —
111
+ * matches W3C semantics where only ports in transferList are detached.
112
+ */
42
113
  /**
43
114
  * Inject the bootstrap script into the WebView so that
44
115
  * window.parent.postMessage() bridges back to GJS.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Walk `value`, substituting any binary buffer/view with a base64
3
+ * placeholder. Returns the substituted tree (copy-on-write — input
4
+ * untouched). Cycles produce cycle-preserving output.
5
+ */
6
+ export declare function encodeBinariesForJson(value: unknown): unknown;
7
+ /**
8
+ * Reverse of `encodeBinariesForJson`. Walks the tree in place and
9
+ * replaces every placeholder with a reconstructed binary view. Returns
10
+ * the same tree reference (for parity with the encoder's contract).
11
+ */
12
+ export declare function decodeBinariesFromJson(value: unknown): unknown;
13
+ /**
14
+ * Inlined twin of the encode/decode pair as a string. Injected into the
15
+ * WebView bootstrap so the WebKit-side window.postMessage override can
16
+ * encode binaries before handing the JSON to GJS, and decode incoming
17
+ * GJS-originated placeholders back into typed arrays before dispatching
18
+ * MessageEvent.
19
+ *
20
+ * Kept in sync with the TS source above by manual review — small enough
21
+ * to audit at a glance, and the alternative (build-time string template)
22
+ * would tangle the bridge build pipeline for marginal gain.
23
+ */
24
+ export declare const BINARY_SERIALIZER_INJECTED_SRC: string;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/iframe",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "HTMLIFrameElement for GJS, backed by WebKit.WebView",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -37,12 +37,12 @@
37
37
  "@girs/gtk-4.0": "4.23.0-4.0.0-rc.15",
38
38
  "@girs/javascriptcore-6.0": "2.52.1-4.0.0-rc.15",
39
39
  "@girs/webkit-6.0": "2.52.1-4.0.0-rc.15",
40
- "@gjsify/dom-elements": "^0.4.10",
41
- "@gjsify/dom-events": "^0.4.10"
40
+ "@gjsify/dom-elements": "^0.4.12",
41
+ "@gjsify/dom-events": "^0.4.12"
42
42
  },
43
43
  "devDependencies": {
44
- "@gjsify/cli": "^0.4.10",
45
- "@gjsify/unit": "^0.4.10",
44
+ "@gjsify/cli": "^0.4.12",
45
+ "@gjsify/unit": "^0.4.12",
46
46
  "@types/node": "^25.6.2",
47
47
  "typescript": "^6.0.3"
48
48
  }