/** * XZBT Contract Adapter — Specification v5.3 * * Implements the normalized contract interface for SciFi-XZBT: * - Canonical external target catalog & descriptors * - Safe mutation & impulse chokepoints * - State revision & monotonic event sequence * - Authoritative source attribution * - Host communication bridge (postMessage) * * Zero external dependencies. Browser & Node compatible. */ (function () { class XZBTContractAdapter { constructor(options = {}) { this.product = options.product || 'SciFi-XZBT'; this.version = options.version || '5.3.0'; this.build = options.build || 'production'; this.contractMajor = 5; this.contractMinor = 3; this.sessionId = null; this.sessionActive = false; this.eventSequence = 0; this.stateRevision = 0; this.registryRevision = 1; this.operationQueue = Promise.resolve(); this.eventSink = null; // Surface catalog (Contract 5.3 §31.2/§31.3). Advertised in describe() // only when non-empty, so absence stays §31.3 form 1 ("absent"). this.surfaces = Array.isArray(options.surfaces) ? options.surfaces : null; this.instanceId = options.instanceId || null; // Session-independent local-change hooks (Step 6.7B). Unlike // _emitEvent, these fire regardless of sessionActive so an // exhibit-internal surface mirror keeps working with no NGN attached. this.onLocalChange = typeof options.onLocalChange === 'function' ? options.onLocalChange : null; this.onLocalAction = typeof options.onLocalAction === 'function' ? options.onLocalAction : null; // Transport hooks this.onMessage = options.onMessage || (() => {}); this.hostOrigin = options.hostOrigin || null; // optional origin filter this.trustedSources = new Set(); // Subsystem binders & state accessors supplied by app.js this.bindings = options.bindings || {}; // Capabilities: audio, speech, observation, display-text // NOTE: midi capability removed in Step 3.5 — MIDI integration belongs to XZBT-NGN. this.capabilities = new Map([ ['audio', { id: 'audio', state: 'ready' }], ['speech', { id: 'speech', state: 'available' }], ['observation', { id: 'observation', state: 'ready' }], ['display-text', { id: 'display-text', state: 'ready' }] ]); // Canonical target registry this.targets = new Map(); this.legacyIdMap = new Map(); // internal ID -> canonical ID this._initCanonicalTargets(); // Setup window listener if running in browser if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') { this._setupWindowBridge(); } } _initCanonicalTargets() { const reg = (target) => { this.targets.set(target.id, target); if (target.legacyId) { this.legacyIdMap.set(target.legacyId, target.id); } }; // --- 1. Continuous Range Mixers & Controls (14 targets) --- reg({ id: 'mix.master', legacyId: 'master-vol', label: 'Master Volume', kind: 'range', min: 0, max: 1, step: 0.01, unit: 'ratio', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.hull.level', legacyId: 'hull-vol', label: 'Hull Drone Level', kind: 'range', min: 0, max: 1, step: 0.01, unit: 'ratio', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.hull.frequency', legacyId: 'hull-freq', label: 'Hull Drone Frequency', kind: 'range', min: 30, max: 120, step: 1, unit: 'hz', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.hull.cutoff', legacyId: 'hull-cutoff', label: 'Hull Drone Cutoff', kind: 'range', min: 40, max: 400, step: 1, unit: 'hz', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.drive.level', legacyId: 'warp-vol', label: 'Drive / Core Level', kind: 'range', min: 0, max: 1, step: 0.01, unit: 'ratio', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.drive.pulse-rate', legacyId: 'warp-bpm', label: 'Drive Pulse Rate', kind: 'range', min: 20, max: 120, step: 1, unit: 'bpm', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.drive.carrier', legacyId: 'warp-carrier', label: 'Drive Carrier Frequency', kind: 'range', min: 30, max: 140, step: 0.5, unit: 'hz', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.environment.level', legacyId: 'air-vol', label: 'Environmental Airflow Level', kind: 'range', min: 0, max: 1, step: 0.01, unit: 'ratio', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.environment.cutoff', legacyId: 'air-cutoff', label: 'Environmental Filter Cutoff', kind: 'range', min: 500, max: 4000, step: 50, unit: 'hz', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.telemetry.level', legacyId: 'telemetry-vol', label: 'Telemetry Level', kind: 'range', min: 0, max: 1, step: 0.01, unit: 'ratio', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'mix.telemetry.density', legacyId: 'telemetry-density', label: 'Telemetry Density', kind: 'range', min: 0, max: 1, step: 0.05, unit: 'ratio', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'speech.robot-amount', legacyId: 'ai-robot-amount', label: 'Speech Robotic Modulation', kind: 'range', min: 0, max: 1, step: 0.01, unit: 'ratio', readable: true, writable: true, restorable: true, category: 'speech', requires: ['speech'] }); reg({ id: 'fnc.level', legacyId: 'fnc-level', label: 'Futuristic Noise Cancellation Level', kind: 'range', min: 0, max: 1, step: 0.01, unit: 'ratio', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'view.activity', legacyId: 'observation-activity', label: 'Observation Visual Activity', kind: 'range', min: 0, max: 1, step: 0.01, unit: 'ratio', readable: true, writable: true, restorable: true, category: 'observation', requires: ['observation'] }); // --- 2. Persistent State Targets (Booleans & Selections) --- reg({ id: 'transport.playing', legacyId: 'master-play', label: 'Audio Engine Online', kind: 'state', valueType: 'boolean', readable: true, writable: true, restorable: true, category: 'transport', requires: ['audio'] }); reg({ id: 'transport.muted', legacyId: 'mute', label: 'Master Mute', kind: 'state', valueType: 'boolean', readable: true, writable: true, restorable: true, category: 'transport', requires: ['audio'] }); reg({ id: 'fnc.enabled', legacyId: 'fnc-enable', label: 'Futuristic Noise Cancellation Active', kind: 'state', valueType: 'boolean', readable: true, writable: true, restorable: true, category: 'audio', requires: ['audio'] }); reg({ id: 'view.observation', legacyId: 'watch-experience', label: 'Observation Viewport Active', kind: 'state', valueType: 'boolean', readable: true, writable: true, restorable: false, category: 'observation', requires: ['observation'] }); reg({ id: 'view.viewport-frame', legacyId: 'observation-viewport', label: 'Starship Bezel Frame Framing', kind: 'state', valueType: 'boolean', readable: true, writable: true, restorable: true, category: 'observation', requires: ['observation'] }); reg({ id: 'view.pillars', legacyId: 'observation-pillars', label: 'Window Pillars / Mullions', kind: 'state', valueType: 'boolean', readable: true, writable: true, restorable: true, category: 'observation', requires: ['observation'] }); reg({ id: 'view.warp-flight', label: 'Warp Flight Mode', kind: 'state', valueType: 'boolean', readable: true, writable: true, restorable: true, category: 'observation', requires: ['observation'] }); reg({ id: 'alert.active', label: 'Active Tactical Alert Status', kind: 'selection', options: [ { value: 'none', label: 'Condition Green (Normal)' }, { value: 'yellow', label: 'Yellow Alert' }, { value: 'red', label: 'Red Alert' } ], readable: true, writable: true, restorable: true, category: 'alert', requires: ['audio'] }); reg({ id: 'universe.selected', label: 'Active Sci-Fi Universe', kind: 'selection', options: [ { value: 'starfleet', label: 'Starfleet / Galaxy' }, { value: 'whataverse', label: 'Whataverse / Tardix' }, { value: 'industrial', label: 'Industrial / Weyland' }, { value: 'bioships', label: 'Bioships / Leviathan' }, { value: 'retrofuture', label: 'Retro Future / 1960s' }, { value: 'military', label: 'Colonial Military' }, { value: 'deepspace', label: 'Deep Space Generation' }, { value: 'outlaw', label: 'Frontier & Salvage' }, { value: 'spacestations', label: 'Orbital Stations & Docks' }, { value: 'comedy', label: 'Whimsical Fleet' } ], readable: true, writable: true, restorable: true, category: 'universe', requires: ['audio'] }); reg({ id: 'preset.selected', label: 'Active Audio Preset', kind: 'selection', options: this._collectAllPresetOptions(), readable: true, writable: true, restorable: true, category: 'preset', requires: ['audio'] }); // --- 3. Impulse Targets (Speech & Ticker) --- reg({ id: 'speech.say', label: 'Speak Text Line', kind: 'impulse', readable: false, writable: false, restorable: false, category: 'speech', requires: ['speech'], arguments: [ { name: 'text', type: 'string', required: true, maxLength: 2000 } ] }); reg({ id: 'speech.generate-announcement', legacyId: 'generate-announcement', label: 'Generate Ambient AI Announcement', kind: 'impulse', readable: false, writable: false, restorable: false, category: 'speech', requires: ['speech'] }); reg({ id: 'display.ticker', label: 'Display Ticker Message (Transient)', kind: 'impulse', readable: false, writable: false, restorable: false, category: 'display', requires: ['display-text'], arguments: [ { name: 'text', type: 'string', required: true, maxLength: 512 } ] }); // --- 4. Canonical Universe Event Impulses --- const eventImpulses = [ { id: 'event.alert-red', legacyId: 'red-alert', label: 'Red Alert Impulse' }, { id: 'event.warp-jump', label: 'Warp Jump Impulse' }, { id: 'event.demat', label: 'Demat Cycle Impulse' }, { id: 'event.vortex', label: 'Time Vortex Impulse' }, { id: 'event.cloister', label: 'Cloister Bell Impulse' }, { id: 'event.flak-barrage', label: 'Flak Barrage Impulse' }, { id: 'event.ftl-jump', label: 'FTL Jump Impulse' }, { id: 'event.singularity', label: 'Singularity Engage Impulse' }, { id: 'event.solar-roar', label: 'Solar Flare Roar Impulse' }, { id: 'event.afterburner', label: 'Afterburner Ignite Impulse' }, { id: 'event.ludicrous-speed', label: 'Ludicrous Speed Impulse' }, { id: 'event.improbability-flip', label: 'Improbability Drive Flip' } ]; eventImpulses.forEach(evt => { reg({ id: evt.id, legacyId: evt.legacyId, label: evt.label, kind: 'impulse', readable: false, writable: false, restorable: false, category: 'event', requires: ['audio'] }); }); // --- 5. Canonical SFX Impulses --- const sfxImpulses = [ { id: 'sfx.comm-badge', label: 'Comm Badge Chime' }, { id: 'sfx.door-swish', label: 'Pneumatic Door Swish' }, { id: 'sfx.bosun-whistle', label: 'Bosun Pipe Call' }, { id: 'sfx.medical-monitor', label: 'Medical Vital Monitor' }, { id: 'sfx.cardassian-door', label: 'Heavy Bulkhead Door' }, { id: 'sfx.turbolift', label: 'Turbochute Whoosh' }, { id: 'sfx.replicator', label: 'Fabricator Materialize' }, { id: 'sfx.transporter', label: 'Translocator Beam' }, { id: 'sfx.console-chirp', label: 'Console Single Chirp' }, { id: 'sfx.sonic', label: 'Sonic Acoustic Screwdriver' }, { id: 'sfx.lever-clunk', label: 'Console Mechanical Lever Clunk' }, { id: 'sfx.tardix-door', label: 'Tardix Wood & Lock Door' }, { id: 'sfx.demat-switch', label: 'Demat Switch Rotor' }, { id: 'sfx.telepathic-chime', label: 'Telepathic Circuit Chime' }, { id: 'sfx.air-handler', label: 'Air Handler Valve Thud' }, { id: 'sfx.mag-boot', label: 'Mag-Boot Solenoid Latch' }, { id: 'sfx.comlock', label: 'Comlock Electronic Tone' }, { id: 'sfx.solar-roar', label: 'Solar Radiation Wind' }, { id: 'sfx.tape-clunk', label: 'Cassette Magnetic Transport Clunk' }, { id: 'sfx.afterburner', label: 'Thruster Burner Pulse' }, { id: 'sfx.geiger', label: 'Radiation Geiger Counter' }, { id: 'sfx.dock-clamp', label: 'Air Valve & Dock Clamp' }, { id: 'sfx.dradis-ping', label: 'Dradis Combat Sonar Ping' }, { id: 'sfx.hal-chime', label: 'HAL Logic Melodic Chime' } ]; sfxImpulses.forEach(sfx => { reg({ id: sfx.id, label: sfx.label, kind: 'impulse', readable: false, writable: false, restorable: false, category: 'sfx', requires: ['audio'] }); }); } _collectAllPresetOptions() { const options = []; if (typeof window !== 'undefined' && window.UniverseRegistry) { for (const uId of Object.keys(window.UniverseRegistry)) { const u = window.UniverseRegistry[uId]; if (u.presets) { for (const pId of Object.keys(u.presets)) { options.push({ value: pId, label: u.presets[pId].name || pId, universe: uId }); } } } } if (!options.length) { options.push( { value: 'tng-bridge', label: 'Interprise-G: Main Bridge', universe: 'starfleet' }, { value: 'tardix-1963', label: 'Type 40 TT: 1963 First Console', universe: 'whataverse' } ); } return options; } _generateSessionId() { return 'sess-' + Math.random().toString(36).substring(2, 10); } _setupWindowBridge() { window.addEventListener('message', (e) => { // Enforce safe origin filtering if configured if (e.origin !== window.location.origin && e.origin !== this.hostOrigin) { return; } // Enforce safe source: must be parent, opener, or self if (e.source !== window.parent && e.source !== window.opener && e.source !== window) { return; } // Ignore non-XZBT messages if (!e.data || typeof e.data !== 'object' || typeof e.data.xzbt !== 'string') { return; } if (e.data.sequence || e.data.type === 'error' || /\.result$/.test(e.data.type)) return; const replySink = (msg) => { try { if (e.source && typeof e.source.postMessage === 'function') { const targetOrigin = e.origin === 'null' ? '*' : e.origin; e.source.postMessage(msg, targetOrigin); } } catch (err) { console.warn('[XZBTContractAdapter] Failed to postMessage reply:', err); } }; this.handleMessage(e.data, 'host', replySink); }); } // Normalized event emission _emitEvent(type, payload, correlationId = null, authoritativeSource = 'system') { if (!this.sessionActive) return; this.eventSequence++; const evt = { xzbt: '5.2', type, sessionId: this.sessionId, sequence: this.eventSequence, timestamp: Date.now(), ...payload }; if (correlationId) { evt.correlationId = correlationId; } if (authoritativeSource) { evt.source = authoritativeSource; } this.onMessage(evt); if (this.eventSink) this.eventSink(evt); } _enqueue(operation) { const result = this.operationQueue.then(operation); this.operationQueue = result.catch(() => {}); return result; } _commitChanges(before, source, correlationId) { const after = this.getContractState(); const changes = [...this.targets.values()].filter(t => t.readable && t.kind !== 'impulse' && before[t.id] !== after[t.id]); if (!changes.length) return false; this.stateRevision++; for (const target of changes) { this._emitEvent(target.kind === 'selection' ? 'selection.changed' : 'state.changed', { stateRevision: this.stateRevision, target: target.id, value: after[target.id] }, correlationId, source); } // Session-independent mirror feed (Step 6.7B). _emitEvent above is // gated on sessionActive and therefore silent with no NGN attached; // onLocalChange is not, so an exhibit-internal surface mirror keeps // working standalone. Fires for every changed target, same as above. if (this.onLocalChange) { for (const target of changes) { this.onLocalChange(target.id, after[target.id], this.stateRevision); } } return true; } // Public API: Direct Mutation Chokepoint applyMutation(targetId, rawValue, source = 'system', correlationId = null) { return this._enqueue(() => this._applyMutation(targetId, rawValue, source, correlationId)); } async _applyMutation(targetId, rawValue, source, correlationId) { // Map legacy ID if necessary const canonicalId = this.legacyIdMap.get(targetId) || targetId; const target = this.targets.get(canonicalId); if (!target) { return { ok: false, code: 'UNKNOWN_TARGET', message: `Target '${targetId}' not found` }; } if (!target.writable || target.kind === 'impulse') { return { ok: false, code: 'TARGET_NOT_SETTABLE', message: `Target '${canonicalId}' is not writable` }; } // Check capability availability if (target.requires) { for (const capId of target.requires) { const cap = this.capabilities.get(capId); if (cap && cap.state === 'unsupported') { return { ok: false, code: 'CAPABILITY_UNAVAILABLE', message: `Capability '${capId}' is unavailable` }; } } } // Value validation & clamping let value = rawValue; if (target.kind === 'range') { if (typeof value !== 'number' || !Number.isFinite(value)) { return { ok: false, code: 'INVALID_VALUE', message: `Value must be a valid number` }; } if (value < target.min || value > target.max) { return { ok: false, code: 'INVALID_VALUE', message: `Value ${value} is outside range [${target.min}, ${target.max}]` }; } const steps = (value - target.min) / target.step; if (target.step && Math.abs(steps - Math.round(steps)) > 1e-7) { return { ok: false, code: 'INVALID_VALUE', message: `Value must align to step ${target.step} from ${target.min}` }; } } else if (target.kind === 'state') { if (target.valueType === 'boolean' && typeof value !== 'boolean') { return { ok: false, code: 'INVALID_VALUE', message: `Value must be a boolean` }; } } else if (target.kind === 'selection') { const validOptions = target.options.map(o => o.value); if (!validOptions.includes(value)) { return { ok: false, code: 'INVALID_VALUE', message: `Value '${value}' is not a valid option for '${canonicalId}'. Valid: [${validOptions.join(', ')}]` }; } } // Read current state to check for idempotent no-op const currentState = this.getContractState(); const currentValue = currentState[canonicalId]; if (currentValue === value) { // Idempotent no-op: succeeds immediately, NO revision increment, NO event return { ok: true, noop: true, stateRevision: this.stateRevision }; } // Special atomic handling for universe.selected: // Must also change preset.selected in the same transaction if (canonicalId === 'universe.selected') { if (!this.bindings.setUniverse) { return { ok: false, code: 'INTERNAL_ERROR', message: 'No universe setter bound' }; } await this.bindings.setUniverse(value, source); this._commitChanges(currentState, source, correlationId); return { ok: true, stateRevision: this.stateRevision }; } // Special handling for preset.selected: validate preset belongs to active universe if (canonicalId === 'preset.selected') { if (this.bindings.getUniverse && window.UniverseRegistry) { const uId = this.bindings.getUniverse(); const u = window.UniverseRegistry[uId]; if (!u || !u.presets || !u.presets[value]) { return { ok: false, code: 'INVALID_VALUE', message: `Preset '${value}' does not belong to active universe '${uId}'` }; } } if (this.bindings.selectPreset) { await this.bindings.selectPreset(value, source); } this._commitChanges(currentState, source, correlationId); return { ok: true, stateRevision: this.stateRevision }; } // Apply direct underlying setter for other targets const applyFn = this.bindings.setters ? this.bindings.setters[canonicalId] : null; if (applyFn) { try { await applyFn(value, source); } catch (err) { return { ok: false, code: 'SETTER_ERROR', message: err.message || String(err) }; } } this._commitChanges(currentState, source, correlationId); return { ok: true, stateRevision: this.stateRevision }; } // Public API: Direct Impulse Invocation Chokepoint invokeAction(targetId, args = {}, source = 'system', correlationId = null) { return this._enqueue(() => this._invokeAction(targetId, args, source, correlationId)); } async _invokeAction(targetId, args, source, correlationId) { const canonicalId = this.legacyIdMap.get(targetId) || targetId; const target = this.targets.get(canonicalId); if (!target) { return { ok: false, code: 'UNKNOWN_TARGET', message: `Target '${targetId}' not found` }; } if (target.kind !== 'impulse') { return { ok: false, code: 'TARGET_NOT_INVOKABLE', message: `Target '${canonicalId}' is not an impulse` }; } // Validate args schema & text length limits if defined const sanitizedArgs = (args && typeof args === 'object' && !Array.isArray(args)) ? { ...args } : {}; const argumentSpecs = target.arguments || []; if (Object.keys(sanitizedArgs).some(key => !argumentSpecs.some(spec => spec.name === key))) { return { ok: false, code: 'INVALID_VALUE', message: 'Unexpected argument key' }; } if (argumentSpecs.length) { for (const argSpec of argumentSpecs) { const argKey = argSpec.name; if (argSpec.required && (sanitizedArgs[argKey] === undefined || sanitizedArgs[argKey] === null)) { return { ok: false, code: 'INVALID_VALUE', message: `Missing required argument '${argKey}'` }; } if (sanitizedArgs[argKey] !== undefined) { if (argSpec.type === 'string' && typeof sanitizedArgs[argKey] !== 'string') { return { ok: false, code: 'INVALID_VALUE', message: `Argument '${argKey}' must be a string` }; } if (argSpec.maxLength && typeof sanitizedArgs[argKey] === 'string') { if (sanitizedArgs[argKey].length > argSpec.maxLength) { return { ok: false, code: 'INVALID_VALUE', message: `Argument '${argKey}' exceeds maximum length of ${argSpec.maxLength}` }; } } } } } // Contextual check: if target is a universe event or SFX, ensure capability is ready if (target.requires) { for (const capId of target.requires) { const cap = this.capabilities.get(capId); if (cap && cap.state === 'unsupported') { return { ok: false, code: 'CAPABILITY_UNAVAILABLE', message: `Capability '${capId}' is unavailable` }; } } } // Execute bound impulse handler const before = this.getContractState(); const invoker = this.bindings.invokers ? this.bindings.invokers[canonicalId] : null; if (invoker) { try { await invoker(sanitizedArgs, source); } catch (err) { return { ok: false, code: 'INVOCATION_FAILED', message: err.message || String(err) }; } } else { // Fallback for universe events or soundboard triggers if (canonicalId.startsWith('event.') && this.bindings.handleUniverseEvent) { const evtType = canonicalId.replace(/^event\./, ''); const handled = await this.bindings.handleUniverseEvent(evtType, source); if (!handled) { return { ok: false, code: 'CAPABILITY_UNAVAILABLE', message: `Event '${evtType}' not available in active universe` }; } } else if (canonicalId.startsWith('sfx.') && this.bindings.handleSoundboardTrigger) { const sfxId = 'btn-' + canonicalId.replace(/^sfx\./, ''); const handled = await this.bindings.handleSoundboardTrigger(sfxId, source); if (!handled) { return { ok: false, code: 'CAPABILITY_UNAVAILABLE', message: `SFX '${canonicalId}' not available in active universe` }; } } } this._commitChanges(before, source, correlationId); // Impulses only increment stateRevision if they change persistent state. this._emitEvent('action.executed', { target: canonicalId, args: sanitizedArgs }, correlationId, source); // Session-independent mirror feed (Step 6.7B); see _commitChanges. if (this.onLocalAction) { this.onLocalAction(canonicalId, sanitizedArgs); } return { ok: true }; } // Public API: Return contract-visible snapshot getContractState() { if (this.bindings.getState) { return this.bindings.getState(); } return {}; } // Public API: Protocol Description describe() { const description = { exhibit: { product: this.product, version: this.version, build: this.build }, contract: { major: this.contractMajor, minor: this.contractMinor }, registryRevision: this.registryRevision, stateRevision: this.stateRevision, capabilities: Array.from(this.capabilities.values()), targets: Array.from(this.targets.values()) }; if (this.surfaces && this.surfaces.length) { description.surfaces = this.surfaces; } return description; } // Update Capability state and emit capability.changed updateCapability(capabilityId, newState) { const cap = this.capabilities.get(capabilityId); if (!cap) return; if (cap.state === newState) return; cap.state = newState; this._emitEvent('capability.changed', { capability: capabilityId, state: newState }, null, 'system'); } // Add target dynamically and emit registry.changed addCustomTarget(spec) { this.targets.set(spec.id, spec); if (spec.legacyId) this.legacyIdMap.set(spec.legacyId, spec.id); this.registryRevision++; this._emitEvent('registry.changed', { registryRevision: this.registryRevision }, null, 'system'); } // Handle inbound message (envelope) from host async handleMessage(msg, transportSource = 'host', replySink = null) { const sink = replySink || this.onMessage; const sendError = (reqId, sessId, code, message) => { const err = { xzbt: '5.2', type: 'error', requestId: reqId || null, sessionId: sessId || null, ok: false, error: { code, message } }; sink(err); return err; }; const sendResult = (type, reqId, payload = {}) => { const res = { xzbt: '5.2', type, requestId, sessionId: this.sessionId, ok: true, ...payload }; sink(res); return res; }; if (!msg || typeof msg !== 'object') { return sendError(null, null, 'INVALID_MESSAGE', 'Message envelope must be an object'); } const { type, requestId, sessionId } = msg; // 1. Handshake if (type === 'hello') { const { supportedContractMajors } = msg; const isSupported = Array.isArray(supportedContractMajors) && supportedContractMajors.includes(this.contractMajor); if (!isSupported) { return sendError( requestId, null, 'UNSUPPORTED_VERSION', `Exhibit requires contract major ${this.contractMajor}` ); } // Establish session and reset sequence counter this.sessionId = this._generateSessionId(); this.sessionActive = true; this.eventSequence = 0; this.eventSink = replySink; return sendResult('hello.result', requestId, { exhibit: { product: this.product, version: this.version, build: this.build }, contract: { major: this.contractMajor, minor: this.contractMinor } }); } // 2. All other operations require an active session if (!this.sessionActive || !sessionId || sessionId !== this.sessionId) { return sendError(requestId, sessionId, 'INVALID_SESSION', 'Missing or invalid sessionId'); } // Authoritative source: ignore host-supplied source const authoritativeSource = transportSource || 'host'; switch (type) { case 'describe': return sendResult('describe.result', requestId, this.describe()); case 'state.get': { const fullState = this.getContractState(); const values = {}; for (const [id, target] of this.targets.entries()) { if (target.readable && target.kind !== 'impulse') { values[id] = fullState[id]; } } return sendResult('state.result', requestId, { stateRevision: this.stateRevision, values }); } case 'set': { const { target: targetId, value } = msg; const res = await this.applyMutation(targetId, value, authoritativeSource, requestId); if (!res.ok) { return sendError(requestId, this.sessionId, res.code, res.message); } return sendResult('set.result', requestId); } case 'invoke': { const { target: targetId, args } = msg; const res = await this.invokeAction(targetId, args, authoritativeSource, requestId); if (!res.ok) { return sendError(requestId, this.sessionId, res.code, res.message); } return sendResult('invoke.result', requestId); } default: return sendError(requestId, sessionId, 'INVALID_MESSAGE', `Unknown message type: ${type}`); } } } if (typeof module !== 'undefined' && module.exports) { module.exports = { XZBTContractAdapter }; } if (typeof window !== 'undefined') { window.XZBTContractAdapter = XZBTContractAdapter; } })();