/* * XZBT Exhibit Contract 5.2 — same-origin postMessage host transport. * * This is layer 6 of the Authoring Guide's recommended separation, and it is * deliberately the thinnest file in the project. It knows how to move * contract messages across one channel and nothing else: no exhibit * semantics, no target knowledge, no state. * * It is also entirely optional. An exhibit that never receives a `hello` * behaves exactly as it would with this file deleted — that is the * standalone-first rule (Contract §2, Authoring Guide §B). * * Security (Contract §25): both `event.origin` and `event.source` are * validated on every message. The transport also assigns `source = 'host'` * itself; a `source` field inside an incoming message is ignored, never * trusted (Contract §15). */ (function () { 'use strict'; /** * @param {object} options * @param {object} options.core an XZBTContractCore.ContractCore * @param {string} [options.origin] expected host origin; defaults to the * document's own origin (same-origin) * @param {function} [options.onMessage] diagnostics hook */ function HostTransport(options) { this.core = options.core; this.expectedOrigin = options.origin || window.location.origin; this.onMessage = options.onMessage || function () {}; this.connected = false; this._bound = this._onMessage.bind(this); /* Events are pushed, not polled. The core already emits every state * change, action, and capability transition through its onEvent hook; * the transport's job is to forward them to the host. Without this the * host would see responses but never learn that anything changed * (Contract §16). */ var self = this; var coreOnEvent = this.core.onEvent; this.core.onEvent = function (event) { if (typeof coreOnEvent === 'function') coreOnEvent(event); if (self.connected) self.send(event); }; window.addEventListener('message', this._bound); } HostTransport.prototype._isTrusted = function (event) { /* Same-origin only. `file://` documents report origin "null", so a * file-opened exhibit simply never accepts host traffic — which is the * correct standalone behaviour, not a failure. */ if (event.origin !== this.expectedOrigin) return false; if (event.source !== window.parent) return false; return true; }; HostTransport.prototype._onMessage = function (event) { if (!this._isTrusted(event)) return; var message = event.data; if (!message || typeof message !== 'object') return; if (message.xzbt !== window.XZBTContractCore.VERSION) return; /* Source is assigned here, at the trusted receiving boundary. */ var response = this.core.handleRequest(message, 'host'); if (!response) return; if (response.type === 'hello.result') this.connected = true; this.onMessage(message, response); this.send(response); }; HostTransport.prototype.send = function (message) { if (window.parent === window) return; window.parent.postMessage(message, this.expectedOrigin); }; HostTransport.prototype.destroy = function () { window.removeEventListener('message', this._bound); }; window.XZBTHostTransport = HostTransport; })();