Post Step 4 Completion

This commit is contained in:
2026-09-14 07:57:18 -07:00
parent db973a86f3
commit 446bf3533e
52 changed files with 32034 additions and 3 deletions
@@ -0,0 +1,85 @@
/*
* 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;
})();