@zolomedia/bifrost-client 1.7.74

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.
Files changed (140) hide show
  1. package/L1_Foundation/L1_Foundation.js +13 -0
  2. package/L1_Foundation/bootstrap/bootstrap.js +11 -0
  3. package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
  4. package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
  5. package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
  6. package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
  7. package/L1_Foundation/bootstrap/module_registry.js +102 -0
  8. package/L1_Foundation/bootstrap/prism_loader.js +164 -0
  9. package/L1_Foundation/config/client_config.js +110 -0
  10. package/L1_Foundation/config/config.js +7 -0
  11. package/L1_Foundation/connection/connection.js +8 -0
  12. package/L1_Foundation/connection/websocket_connection.js +122 -0
  13. package/L1_Foundation/constants/bifrost_constants.js +284 -0
  14. package/L1_Foundation/constants/constants.js +7 -0
  15. package/L1_Foundation/logger/logger.js +10 -0
  16. package/L2_Handling/L2_Handling.js +15 -0
  17. package/L2_Handling/cache/cache.js +22 -0
  18. package/L2_Handling/cache/cache_constants.js +69 -0
  19. package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
  20. package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
  21. package/L2_Handling/cache/orchestration/orchestration.js +12 -0
  22. package/L2_Handling/cache/storage/session_manager.js +289 -0
  23. package/L2_Handling/cache/storage/storage.js +10 -0
  24. package/L2_Handling/cache/storage/storage_manager.js +590 -0
  25. package/L2_Handling/display/composite/composite.js +13 -0
  26. package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
  27. package/L2_Handling/display/composite/swiper_renderer.js +564 -0
  28. package/L2_Handling/display/composite/terminal_renderer.js +922 -0
  29. package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
  30. package/L2_Handling/display/display.js +30 -0
  31. package/L2_Handling/display/feedback/feedback.js +11 -0
  32. package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
  33. package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
  34. package/L2_Handling/display/inputs/button_renderer.js +634 -0
  35. package/L2_Handling/display/inputs/form_renderer.js +583 -0
  36. package/L2_Handling/display/inputs/input_renderer.js +658 -0
  37. package/L2_Handling/display/inputs/inputs.js +12 -0
  38. package/L2_Handling/display/navigation/menu_renderer.js +206 -0
  39. package/L2_Handling/display/navigation/navigation.js +11 -0
  40. package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
  41. package/L2_Handling/display/orchestration/orchestration.js +11 -0
  42. package/L2_Handling/display/orchestration/renderer.js +430 -0
  43. package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
  44. package/L2_Handling/display/outputs/alert_renderer.js +161 -0
  45. package/L2_Handling/display/outputs/audio_renderer.js +94 -0
  46. package/L2_Handling/display/outputs/card_renderer.js +229 -0
  47. package/L2_Handling/display/outputs/code_renderer.js +66 -0
  48. package/L2_Handling/display/outputs/dl_renderer.js +131 -0
  49. package/L2_Handling/display/outputs/header_renderer.js +162 -0
  50. package/L2_Handling/display/outputs/icon_renderer.js +107 -0
  51. package/L2_Handling/display/outputs/image_renderer.js +145 -0
  52. package/L2_Handling/display/outputs/list_renderer.js +190 -0
  53. package/L2_Handling/display/outputs/outputs.js +19 -0
  54. package/L2_Handling/display/outputs/table_renderer.js +765 -0
  55. package/L2_Handling/display/outputs/text_renderer.js +818 -0
  56. package/L2_Handling/display/outputs/typography_renderer.js +293 -0
  57. package/L2_Handling/display/outputs/video_renderer.js +116 -0
  58. package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
  59. package/L2_Handling/display/primitives/form_primitives.js +526 -0
  60. package/L2_Handling/display/primitives/generic_containers.js +109 -0
  61. package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
  62. package/L2_Handling/display/primitives/link_primitives.js +552 -0
  63. package/L2_Handling/display/primitives/lists_primitives.js +262 -0
  64. package/L2_Handling/display/primitives/media_primitives.js +383 -0
  65. package/L2_Handling/display/primitives/primitives.js +19 -0
  66. package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
  67. package/L2_Handling/display/primitives/table_primitives.js +528 -0
  68. package/L2_Handling/display/primitives/typography_primitives.js +175 -0
  69. package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
  70. package/L2_Handling/display/specialized/specialized.js +10 -0
  71. package/L2_Handling/hooks/hooks.js +9 -0
  72. package/L2_Handling/hooks/menu_integration.js +57 -0
  73. package/L2_Handling/hooks/widget_hook_manager.js +292 -0
  74. package/L2_Handling/message/message.js +8 -0
  75. package/L2_Handling/message/message_handler.js +701 -0
  76. package/L2_Handling/navigation/navigation.js +8 -0
  77. package/L2_Handling/navigation/navigation_manager.js +403 -0
  78. package/L2_Handling/zhooks/features/cache_live.js +287 -0
  79. package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
  80. package/L2_Handling/zhooks/zhooks_manager.js +65 -0
  81. package/L2_Handling/zvaf/zvaf.js +8 -0
  82. package/L2_Handling/zvaf/zvaf_manager.js +334 -0
  83. package/L3_Abstraction/L3_Abstraction.js +12 -0
  84. package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
  85. package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
  86. package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
  87. package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
  88. package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
  89. package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
  90. package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
  91. package/L3_Abstraction/renderer/renderer.js +1 -0
  92. package/L3_Abstraction/session/session.js +1 -0
  93. package/L4_Orchestration/L4_Orchestration.js +11 -0
  94. package/L4_Orchestration/client/client.js +1 -0
  95. package/L4_Orchestration/facade/facade.js +9 -0
  96. package/L4_Orchestration/facade/manager_registry.js +118 -0
  97. package/L4_Orchestration/facade/renderer_registry.js +274 -0
  98. package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
  99. package/L4_Orchestration/lifecycle/initializer.js +135 -0
  100. package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
  101. package/L4_Orchestration/rendering/facade.js +94 -0
  102. package/L4_Orchestration/rendering/rendering.js +7 -0
  103. package/LICENSE +21 -0
  104. package/README.md +82 -0
  105. package/bifrost_client.js +204 -0
  106. package/bifrost_core.js +1686 -0
  107. package/docs/ARCHITECTURE.md +111 -0
  108. package/docs/PROTOCOL.md +106 -0
  109. package/docs/RENDERERS.md +101 -0
  110. package/docs/SECURITY.md +92 -0
  111. package/package.json +24 -0
  112. package/syntax/prism-zconfig.js +41 -0
  113. package/syntax/prism-zenv.js +69 -0
  114. package/syntax/prism-zolo-theme.css +288 -0
  115. package/syntax/prism-zolo.js +380 -0
  116. package/syntax/prism-zschema.js +38 -0
  117. package/syntax/prism-zspark.js +25 -0
  118. package/syntax/prism-zui.js +68 -0
  119. package/zSys/accessibility/accessibility.js +10 -0
  120. package/zSys/accessibility/emoji_accessibility.js +173 -0
  121. package/zSys/dom/block_utils.js +122 -0
  122. package/zSys/dom/container_utils.js +370 -0
  123. package/zSys/dom/dom.js +13 -0
  124. package/zSys/dom/dom_utils.js +328 -0
  125. package/zSys/dom/encoding_utils.js +117 -0
  126. package/zSys/dom/style_utils.js +71 -0
  127. package/zSys/errors/error_display.js +299 -0
  128. package/zSys/errors/errors.js +10 -0
  129. package/zSys/theme/color_utils.js +274 -0
  130. package/zSys/theme/dark_mode_utils.js +272 -0
  131. package/zSys/theme/size_utils.js +256 -0
  132. package/zSys/theme/spacing_utils.js +405 -0
  133. package/zSys/theme/theme.js +14 -0
  134. package/zSys/theme/zbase.css +1735 -0
  135. package/zSys/theme/zbase_inject.js +161 -0
  136. package/zSys/theme/ztheme_utils.js +305 -0
  137. package/zSys/validation/error_boundary.js +201 -0
  138. package/zSys/validation/validation.js +11 -0
  139. package/zSys/validation/validation_utils.js +238 -0
  140. package/zSys/zSys.js +14 -0
@@ -0,0 +1,701 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════
3
+ * Message Handler Module - Message Processing & Correlation
4
+ * ═══════════════════════════════════════════════════════════════
5
+ *
6
+ * @module core/message_handler
7
+ * @layer 4 (Event Handlers)
8
+ */
9
+
10
+ // Constants
11
+ import { TIMEOUTS, PROTOCOL_EVENTS, PROTOCOL_REASONS } from '../../L1_Foundation/constants/bifrost_constants.js';
12
+
13
+ // zRender op-code decoder — reverse map of render_opcodes.py EVENT_TO_OP
14
+ // (op → display handler key). This table contains no business logic, wizard
15
+ // flows, or application routes — only the wire-op ⇄ display-event vocabulary.
16
+ //
17
+ // DRIFT WARNING: this is a hand-maintained mirror of the server SSOT at
18
+ // zGuard/zguard/bifrost/zBifrost_modules/render/render_opcodes.py. Keep the
19
+ // entry count in sync (currently 35). If the server adds/renames an op, an
20
+ // unknown opcode will surface via _warnUnknownOpcode() below instead of being
21
+ // silently dropped.
22
+ const _ZRENDER_OPS = {"tx":"text","hd":"header","im":"image","rt":"rich_text","ic":"icon","zu":"zURL","zt":"zTable","er":"error","wr":"warning","su":"success","inf":"info","rs":"read_string","pb":"progress_bar","mn":"zMenu","zd":"zDash","zdl":"zDialog","zi":"zInput","sep":"separator","cod":"code","lnk":"link","bdg":"badge","spn":"spinner","ls":"list","dl":"dl","btn":"button","rb":"read_bool","rp":"read_password","jsn":"json","div":"divider","crd":"card","ztrm":"zTerminal","sel":"selection","zcr":"zCrumbs","pc":"progress_complete","swi":"swiper_init"};
23
+
24
+ // Surface opcode-mirror drift loudly (once per unknown op) so a server change
25
+ // that this client hasn't mirrored is visible instead of silently swallowed.
26
+ const _warnedOpcodes = new Set();
27
+ function _warnUnknownOpcode(op) {
28
+ if (_warnedOpcodes.has(op)) return;
29
+ _warnedOpcodes.add(op);
30
+ console.warn(
31
+ `[zRender] Unknown render opcode "${op}" — client opcode map is out of sync ` +
32
+ `with the server (render_opcodes.py). Node passed through undecoded.`
33
+ );
34
+ }
35
+
36
+ function _decodeRenderNode(node) {
37
+ if (!node || typeof node !== 'object') return node;
38
+ if (Array.isArray(node)) return node.map(_decodeRenderNode);
39
+ // Node carrying an op code — decode known ops; flag unknown ones as drift.
40
+ if ('e' in node && typeof node.e === 'string') {
41
+ if (_ZRENDER_OPS[node.e]) {
42
+ const decoded = { event: _ZRENDER_OPS[node.e] };
43
+ for (const [k, v] of Object.entries(node)) {
44
+ if (k === 'e') continue;
45
+ decoded[k] = _decodeRenderNode(v);
46
+ }
47
+ return decoded;
48
+ }
49
+ _warnUnknownOpcode(node.e);
50
+ }
51
+ // Container / metadata node (or undecodable op) — recurse into all values.
52
+ const decoded = {};
53
+ for (const [k, v] of Object.entries(node)) {
54
+ decoded[k] = _decodeRenderNode(v);
55
+ }
56
+ return decoded;
57
+ }
58
+
59
+ export class MessageHandler {
60
+ constructor(logger, hooks, client = null) {
61
+ this.logger = logger;
62
+ this.hooks = hooks;
63
+ this.client = client; // Store reference to BifrostClient for client-side navigation
64
+
65
+ // Pass logger to hooks for better error handling
66
+ if (this.hooks && typeof this.hooks.logger === 'undefined') {
67
+ this.hooks.logger = logger;
68
+ }
69
+
70
+ this.requestId = 0;
71
+ this.callbacks = new Map();
72
+ this.timeout = TIMEOUTS.REQUEST_TIMEOUT; // Default timeout from constants
73
+ }
74
+
75
+ /**
76
+ * Set timeout for requests
77
+ */
78
+ setTimeout(timeout) {
79
+ this.timeout = timeout;
80
+ }
81
+
82
+ /**
83
+ * Extract session ID from HTTP cookie for session sync (WebSocket/HTTP bridge).
84
+ *
85
+ * SECURITY NOTE: this reads `session`/`sessionid` via document.cookie, which only
86
+ * works when the cookie is NOT HttpOnly. The authoritative session lives in the
87
+ * server-side store; this value is a best-effort sync hint and MUST NOT be treated
88
+ * as proof of identity (the server re-validates). Deployments that set HttpOnly
89
+ * (recommended) will simply get null here and rely on the browser to attach the
90
+ * cookie to the WS handshake — that is the safer path.
91
+ *
92
+ * @private
93
+ * @returns {string|null} Session ID or null if not found / HttpOnly
94
+ */
95
+ _getSessionIdFromCookie() {
96
+ // Parse all cookies
97
+ const cookies = document.cookie.split(';');
98
+
99
+ // Look for 'session' cookie (Flask default) or 'sessionid' (Django)
100
+ for (const cookie of cookies) {
101
+ const [name, value] = cookie.trim().split('=');
102
+ if (name === 'session' || name === 'sessionid') {
103
+ this.logger.log(`[MessageHandler] Found session cookie: ${name}=${value.substring(0, 10)}...`);
104
+ return value;
105
+ }
106
+ }
107
+
108
+ this.logger.log('[MessageHandler] [WARN] No session cookie found (user not logged in)');
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * Validate outgoing message follows protocol
114
+ * @private
115
+ */
116
+ _validateOutgoingMessage(payload) {
117
+ // Warn if using deprecated 'action' field
118
+ if (payload.action && !payload.event) {
119
+ this.logger.warn(
120
+ 'Using deprecated "action" field. Please use "event" instead.',
121
+ { action: payload.action }
122
+ );
123
+ // Auto-migrate: copy action to event
124
+ payload.event = payload.action;
125
+ }
126
+
127
+ // Warn if message has both 'action' and 'event'
128
+ if (payload.action && payload.event && payload.action !== payload.event) {
129
+ this.logger.warn(
130
+ 'Message has both "action" and "event" fields with different values. Using "event".',
131
+ { action: payload.action, event: payload.event }
132
+ );
133
+ delete payload.action;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Handle incoming message
139
+ */
140
+ handleMessage(data) {
141
+ try {
142
+ const message = JSON.parse(data);
143
+ this.logger.debug('[MessageHandler] Received message:', message.event || message.display_event || 'unknown');
144
+
145
+ // Debug zDialog messages to see full structure
146
+ if (message.display_event === PROTOCOL_EVENTS.ZDIALOG || (message.data && message.data.event === PROTOCOL_EVENTS.ZDIALOG)) {
147
+ this.logger.log('[MessageHandler] zDialog message structure:', JSON.stringify(message, null, 2));
148
+ }
149
+
150
+ // Call general message hook (with error boundary)
151
+ try {
152
+ this.hooks.call('onMessage', message);
153
+ } catch (hookError) {
154
+ this.logger.error('[MessageHandler] Error in onMessage hook:', hookError);
155
+ // Continue processing - don't let hook errors break message handling
156
+ }
157
+
158
+ // Progressive chunk rendering (zWizard chunked execution for Bifrost)
159
+ // MUST be checked BEFORE response correlation (chunks have _requestId but are NOT responses)
160
+ if (message.event === PROTOCOL_EVENTS.RENDER_CHUNK) {
161
+ this.logger.debug('[MessageHandler] Chunk event detected:', message.chunk_num);
162
+ // Decode zRender op codes back to display event names before handing to renderers
163
+ if (message.data) {
164
+ message.data = _decodeRenderNode(message.data);
165
+ }
166
+ try {
167
+ this.hooks.call('onRenderChunk', message);
168
+ } catch (hookError) {
169
+ this.logger.error('[MessageHandler] Error rendering chunk:', hookError);
170
+ this.hooks.call('onError', { type: 'chunk_render_error', error: hookError, message });
171
+ }
172
+ return;
173
+ }
174
+
175
+ // Connection info event (session data from backend) - v1.6.0
176
+ if (message.event === PROTOCOL_EVENTS.CONNECTION_INFO) {
177
+ this.logger.debug('[MessageHandler] Connection info detected');
178
+ this.hooks.call('onConnectionInfo', message.data);
179
+ // Also trigger onConnected for backward compatibility
180
+ this.hooks.call('onConnected', message.data);
181
+ return;
182
+ }
183
+
184
+ // Error event from backend (walker execution errors, validation errors, etc.)
185
+ if (message.event === PROTOCOL_EVENTS.ERROR) {
186
+ this.logger.error('[MessageHandler] Backend error received:', message.message || message.error);
187
+
188
+ // Clear navigation timeout if active
189
+ if (this.client && this.client._navigationTimeout) {
190
+ clearTimeout(this.client._navigationTimeout);
191
+ this.client._navigationTimeout = null;
192
+ }
193
+
194
+ // Show error in content area
195
+ if (this.client && this.client._zVaFElement) {
196
+ this.client._zVaFElement.innerHTML = `
197
+ <div class="zAlert zAlert-danger zmt-4">
198
+ <strong>Backend Error:</strong> ${message.message || message.error || 'Unknown error'}
199
+ ${message.details ? `<br><small>${message.details}</small>` : ''}
200
+ </div>
201
+ `;
202
+ }
203
+
204
+ // Call error hook
205
+ this.hooks.call('onError', message);
206
+ return;
207
+ }
208
+
209
+ // Navigate back event (^ bounce-back after block completion)
210
+ if (message.event === PROTOCOL_EVENTS.NAVIGATE_BACK) {
211
+ this.logger.log('[MessageHandler] NAVIGATE_BACK EVENT - triggering browser back');
212
+ this.logger.log('[MessageHandler] Reason:', message.reason);
213
+
214
+ // For bounce-back completions (e.g., after login/logout), always navigate to home via client-side nav
215
+ // This avoids double-back issues and ensures correct block loading
216
+ if (message.reason === PROTOCOL_REASONS.BOUNCE_BACK_COMPLETED) {
217
+ // Refresh NavBar after any bounce (RBAC updates after login/logout).
218
+ const refreshNav = () => {
219
+ if (typeof this.client._fetchAndPopulateNavBar === 'function') {
220
+ this.logger.log('[MessageHandler] Refreshing NavBar after bounce-back');
221
+ this.client._fetchAndPopulateNavBar().catch(err => {
222
+ this.logger.error('[MessageHandler] Failed to refresh NavBar:', err);
223
+ });
224
+ }
225
+ };
226
+
227
+ // ^ bounce block (no explicit target): return to the previous page.
228
+ if (message.back) {
229
+ this.logger.log('[MessageHandler] Bounce-back - returning to previous page');
230
+ window.history.back();
231
+ // History navigation re-renders asynchronously; refresh navbar after it settles.
232
+ setTimeout(refreshNav, 300);
233
+ return;
234
+ }
235
+
236
+ // Explicit (onSuccess) target, else home for plain bounces.
237
+ const target = message.url || '/';
238
+ this.logger.log('[MessageHandler] Bounce-back - navigating to ' + target + ' via client-side nav');
239
+ if (this.client && typeof this.client._navigateToRoute === 'function') {
240
+ this.client._navigateToRoute(target).then(refreshNav).catch(err => {
241
+ this.logger.error('[MessageHandler] Navigation failed:', err);
242
+ });
243
+ } else {
244
+ // Fallback: use window.location (will cause reload)
245
+ window.location.href = target;
246
+ }
247
+ return;
248
+ }
249
+
250
+ // For RBAC denials, prefer an explicit redirect target (SSOT: zRBAC
251
+ // onDenied / global login route resolved server-side). Falls back to
252
+ // history.back()/home when no target was provided.
253
+ if (message.reason === PROTOCOL_REASONS.RBAC_DENIED) {
254
+ if (message.url) {
255
+ this.logger.log('[MessageHandler] RBAC denied - redirecting to ' + message.url);
256
+ if (this.client && typeof this.client._navigateToRoute === 'function') {
257
+ this.client._navigateToRoute(message.url).then(() => {
258
+ if (typeof this.client._fetchAndPopulateNavBar === 'function') {
259
+ this.client._fetchAndPopulateNavBar().catch(() => {});
260
+ }
261
+ }).catch(err => {
262
+ this.logger.error('[MessageHandler] RBAC redirect failed:', err);
263
+ window.location.href = message.url;
264
+ });
265
+ } else {
266
+ window.location.href = message.url;
267
+ }
268
+ return;
269
+ }
270
+
271
+ const hasAppHistory = window.history.length > 2 ||
272
+ (window.history.length > 1 && document.referrer.includes(window.location.hostname));
273
+
274
+ if (hasAppHistory) {
275
+ this.logger.log('[MessageHandler] RBAC denied - using history.back()');
276
+ window.history.back();
277
+ } else {
278
+ this.logger.log('[MessageHandler] RBAC denied, no history - navigating to home');
279
+ if (this.client && typeof this.client._navigateToRoute === 'function') {
280
+ this.client._navigateToRoute('/');
281
+ } else {
282
+ window.location.href = '/';
283
+ }
284
+ }
285
+ return;
286
+ }
287
+
288
+ // Default: attempt history.back() for other navigate_back reasons
289
+ this.logger.log('[MessageHandler] Other reason - using history.back()');
290
+ window.history.back();
291
+ return;
292
+ }
293
+
294
+ // Wizard gate result (post-gate steps from wizard_gate_submit)
295
+ if (message.event === PROTOCOL_EVENTS.WIZARD_GATE_RESULT) {
296
+ this.logger.debug('[MessageHandler] wizard_gate_result received for gate:', message.gateKey);
297
+ this.hooks.call('onWizardGateResult', message);
298
+ return;
299
+ }
300
+
301
+ // Dashboard event (zDash display event for sidebar navigation)
302
+ if (message.event === PROTOCOL_EVENTS.ZDASH) {
303
+ this.logger.debug('[MessageHandler] zDash event detected');
304
+ this.logger.log(' [MessageHandler] Dashboard config:', message);
305
+ this.hooks.call('onZDash', message);
306
+ return;
307
+ }
308
+
309
+ // Menu event (menu navigation in Bifrost mode)
310
+ // Note: Backend sends 'zMenu' not 'menu' (matches zDash, zDialog pattern)
311
+ if (message.event === PROTOCOL_EVENTS.ZMENU) {
312
+ this.logger.debug('[MessageHandler] zMenu event detected');
313
+ this.logger.log(' [MessageHandler] Menu config:', message);
314
+ this.hooks.call('onMenu', message);
315
+ return;
316
+ }
317
+
318
+ // RBAC denial event (access denied)
319
+ if (message.event === PROTOCOL_EVENTS.RBAC_DENIED) {
320
+ this.logger.log(' [MessageHandler] RBAC ACCESS DENIED');
321
+ this.logger.log(' RBAC Access Denied:', message.message);
322
+
323
+ // Display the denial message
324
+ if (message.message) {
325
+ // Create a styled error display using zTheme classes
326
+ const errorDiv = document.createElement('div');
327
+ errorDiv.className = 'zAlert zAlert-danger zmt-4 zp-4';
328
+ errorDiv.innerHTML = `
329
+ <h3 class="zAlert-heading zmb-2"> Access Denied</h3>
330
+ <div class="zAlert-body">${message.message.replace(/\n/g, '<br>')}</div>
331
+ <hr class="zmy-3">
332
+ <p class="zmb-0 zText-muted"><small>You will be redirected back in a moment...</small></p>
333
+ `;
334
+
335
+ // Clear content area and show error
336
+ const contentArea = document.getElementById('zVaF-content');
337
+ if (contentArea) {
338
+ contentArea.innerHTML = ''; // Clear blank content
339
+ contentArea.appendChild(errorDiv);
340
+ }
341
+ }
342
+
343
+ return;
344
+ }
345
+
346
+ // Check if this is a response to a request
347
+ const requestId = message._requestId;
348
+ if (requestId !== undefined && this.callbacks.has(requestId)) {
349
+ this._handleResponse(requestId, message);
350
+ return;
351
+ }
352
+
353
+ // If a message looks like a BARE request/response (no event type) but carries
354
+ // no _requestId while callbacks are pending, that's a real backend protocol bug.
355
+ // Event-typed messages (execute_zfunc_response, execute_code_response,
356
+ // input_response, …) legitimately carry `result` and route by their own
357
+ // `event`/`requestId` below — they must NOT trip this guard or it false-fires
358
+ // on every avatar zInja (execute_zfunc) render.
359
+ if (!message.event &&
360
+ (message.result !== undefined || message.error !== undefined) &&
361
+ this.callbacks.size > 0) {
362
+ this.logger.error(
363
+ 'Received response without _requestId! Backend must echo _requestId in all responses.',
364
+ { message, pendingRequests: this.callbacks.size }
365
+ );
366
+ // Don't try to correlate - this is a backend bug that must be fixed
367
+ }
368
+
369
+ // Check for specific event types
370
+ if (message.event === PROTOCOL_EVENTS.INPUT_RESPONSE) {
371
+ return; // Handled internally by zDisplay
372
+ }
373
+
374
+ // Handle execute_zfunc_response — resolves promise in ZDisplayOrchestrator._executeZFunc
375
+ if (message.event === PROTOCOL_EVENTS.EXECUTE_ZFUNC_RESPONSE) {
376
+ this.logger.log('[MessageHandler] execute_zfunc_response received:', message.requestId);
377
+ this.hooks.call('onZFuncResponse', message);
378
+ return;
379
+ }
380
+
381
+ // Handle execute_code_response for zTerminal
382
+ if (message.event === PROTOCOL_EVENTS.EXECUTE_CODE_RESPONSE) {
383
+ this.logger.log('[MessageHandler] execute_code_response received:', message.requestId);
384
+ // Route to TerminalRenderer's static handler
385
+ const TerminalRenderer = window._TerminalRenderer;
386
+ if (TerminalRenderer && TerminalRenderer.handleExecutionResponse) {
387
+ TerminalRenderer.handleExecutionResponse(message.requestId, message);
388
+ } else if (window._zTerminalResponses && window._zTerminalResponses[message.requestId]) {
389
+ // Fallback to direct promise resolution
390
+ window._zTerminalResponses[message.requestId](message);
391
+ delete window._zTerminalResponses[message.requestId];
392
+ }
393
+ return;
394
+ }
395
+
396
+ // Handle request_input for zTerminal interactive input
397
+ if (message.event === PROTOCOL_EVENTS.REQUEST_INPUT) {
398
+ this.logger.log('[MessageHandler] request_input received:', message.requestId, message.prompt, 'isPassword:', message.isPassword);
399
+
400
+ // zFunc execution: route to inline widget rendered by ZDisplayOrchestrator
401
+ if (message.zfuncRequestId) {
402
+ this.hooks.call('onZFuncInput', message);
403
+ return;
404
+ }
405
+
406
+ // Route to TerminalRenderer's static handler
407
+ const TerminalRenderer = window._TerminalRenderer;
408
+ if (TerminalRenderer && TerminalRenderer.handleInputRequest) {
409
+ TerminalRenderer.handleInputRequest(
410
+ message.requestId,
411
+ message.prompt,
412
+ message.inputType || 'text',
413
+ message.required || false,
414
+ message.isPassword || false,
415
+ message.defaultValue || '',
416
+ message.isReadonly || false,
417
+ message.isDisabled || false,
418
+ message.placeholder || '',
419
+ message.datalist || [],
420
+ message.min ?? null,
421
+ message.max ?? null,
422
+ message.step ?? null,
423
+ );
424
+ }
425
+ return;
426
+ }
427
+
428
+ // Open a served resource in a new browser tab. Emitted by the server when
429
+ // a zTerminal swap-run (zOrigin=zBifrost) delegates an 'open' to the client
430
+ // instead of launching on the server (TRUST #35 — server never opens GUIs
431
+ // for remote visitors; the open happens in THIS browser).
432
+ if (message.event === PROTOCOL_EVENTS.OPEN_URL) {
433
+ if (message.url) {
434
+ this.logger.log('[MessageHandler] open_url received — opening new tab:', message.url);
435
+ window.open(message.url, '_blank', 'noopener,noreferrer');
436
+ }
437
+ return;
438
+ }
439
+
440
+ // Walker finished a fire-and-forget render (navigation). When the server
441
+ // attaches a zPsi anchor — a crumb-driven zBack that should land on the
442
+ // section the user navigated FROM — scroll that section into view once the
443
+ // streamed chunks have painted. SSOT: the section comes from zCrumbs on the
444
+ // server; the client only honours the anchor it is handed.
445
+ if (message.event === 'walker_complete') {
446
+ if (message.zPsi) {
447
+ const anchor = String(message.zPsi);
448
+ const root = (this.client && this.client._zVaFElement) || document;
449
+ // Chunks render asynchronously (awaited renderItems, emoji/icon loads),
450
+ // so the target section may not exist the instant walker_complete lands.
451
+ // Poll briefly until it paints, then scroll. data-zkey is stamped on
452
+ // every top-level section; _zId is the fallback for anchored blocks.
453
+ let tries = 0;
454
+ const maxTries = 30; // ~2.4s at 80ms
455
+ const tryScroll = () => {
456
+ const target = root.querySelector(`[data-zkey="${anchor}"]`)
457
+ || document.getElementById(anchor);
458
+ if (target) {
459
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
460
+ this.logger.log('[MessageHandler] zBack zPsi → scrolled to section:', anchor);
461
+ return;
462
+ }
463
+ if (++tries < maxTries) {
464
+ setTimeout(tryScroll, 80);
465
+ } else {
466
+ this.logger.warn('[MessageHandler] zBack zPsi → section not found after retries:', anchor);
467
+ }
468
+ };
469
+ requestAnimationFrame(tryScroll);
470
+ }
471
+ return;
472
+ }
473
+
474
+ // Handle real-time output lines from execute_code execution (e.g. zText / Show_Result steps)
475
+ if (message.event === PROTOCOL_EVENTS.OUTPUT) {
476
+ const TerminalRenderer = window._TerminalRenderer;
477
+ if (TerminalRenderer && TerminalRenderer.handleOutput) {
478
+ TerminalRenderer.handleOutput(message);
479
+ }
480
+ return;
481
+ }
482
+
483
+ // zTable event emitted standalone (non-walker contexts, or fallback).
484
+ // In walker/chunk mode, zTable is now injected inline into render_chunk
485
+ // (see advanced_table.py + message_walker._resolve_zdata_reads).
486
+ if (message.event === PROTOCOL_EVENTS.ZTABLE) {
487
+ this.logger.log('[MessageHandler] Standalone zTable event received (non-chunk context)');
488
+ this.hooks.call('onDisplay', { ...message, display_event: PROTOCOL_EVENTS.ZTABLE });
489
+ return;
490
+ }
491
+
492
+ // Check for display events (supports multiple formats)
493
+ // - Old: {event: 'display', data: {...}}
494
+ // - New: {display_event: 'success', data: {...}}
495
+ // - Progress: {event: 'progress_bar', ...} → treated as display event
496
+ if (message.event === PROTOCOL_EVENTS.DISPLAY || message.type === PROTOCOL_EVENTS.DISPLAY || message.display_event) {
497
+ this.logger.debug('[MessageHandler] Display event:', message.display_event);
498
+ try {
499
+ this.hooks.call('onDisplay', message); // Pass full message with display_event
500
+ } catch (hookError) {
501
+ this.logger.error('[MessageHandler] Error in display event handler:', hookError);
502
+ this.hooks.call('onError', { type: 'display_error', error: hookError, message });
503
+ }
504
+ return;
505
+ }
506
+
507
+ // Progress bar events - also route to display renderer
508
+ if (message.event === PROTOCOL_EVENTS.PROGRESS_BAR || message.event === PROTOCOL_EVENTS.PROGRESS_COMPLETE) {
509
+ message.display_event = PROTOCOL_EVENTS.PROGRESS_BAR;
510
+ this.hooks.call('onDisplay', message);
511
+ this.hooks.call('onProgressBar', message); // Also call specific hook for backwards compat
512
+ return;
513
+ }
514
+
515
+ if (message.event === PROTOCOL_EVENTS.INPUT_REQUEST || message.type === PROTOCOL_EVENTS.INPUT_REQUEST) {
516
+ this.hooks.call('onInput', message);
517
+ return;
518
+ }
519
+
520
+ if (message.event === PROTOCOL_EVENTS.PROGRESS_UPDATE) {
521
+ this.hooks.call('onProgressUpdate', message);
522
+ return;
523
+ }
524
+
525
+ if (message.event === PROTOCOL_EVENTS.PROGRESS_COMPLETE) {
526
+ this.hooks.call('onProgressComplete', message);
527
+ return;
528
+ }
529
+
530
+ // Spinner events
531
+ if (message.event === PROTOCOL_EVENTS.SPINNER_START) {
532
+ this.hooks.call('onSpinnerStart', message);
533
+ return;
534
+ }
535
+
536
+ if (message.event === PROTOCOL_EVENTS.SPINNER_STOP) {
537
+ this.hooks.call('onSpinnerStop', message);
538
+ return;
539
+ }
540
+
541
+ if (message.event === PROTOCOL_EVENTS.SWIPER_INIT) {
542
+ this.hooks.call('onSwiperInit', message);
543
+ return;
544
+ }
545
+
546
+ // App-level log event (zLogger / zos.app.log) — output to browser console
547
+ if (message.event === PROTOCOL_EVENTS.APP_LOG) {
548
+ const level = (message.level || 'INFO').toUpperCase();
549
+ const tag = message.tag ? `[${message.tag}] ` : '';
550
+ const line = `${tag}${message.message}`;
551
+ if (level === 'ERROR' || level === 'CRITICAL') console.error('[zLog]', line);
552
+ else if (level === 'WARNING') console.warn('[zLog]', line);
553
+ else if (level === 'DEBUG') console.debug('[zLog]', line);
554
+ else console.log('[zLog]', line);
555
+ return;
556
+ }
557
+
558
+ // zFunc execution signal — backend console confirmation, not a UI event
559
+ if (message.event === PROTOCOL_EVENTS.ZFUNC_EXEC) {
560
+ if (message.stdout) console.log('[zFunc stdout]', message.stdout);
561
+ console.log('[zFunc]', message.spec, '→', message.result, message.success ? '✓' : '✗');
562
+ return;
563
+ }
564
+
565
+ // Otherwise, treat as broadcast
566
+ this._handleBroadcast(message);
567
+
568
+ } catch (error) {
569
+ this.logger.error('[ERROR][ERROR][ERROR] [MessageHandler] CRITICAL ERROR:', error);
570
+ this.logger.error('[ERROR][ERROR][ERROR] [MessageHandler] Error stack:', error.stack);
571
+ this.logger.error('[ERROR][ERROR][ERROR] [MessageHandler] Raw data:', data);
572
+ this.logger.log('[ERROR] Failed to parse message', { data, error });
573
+ this.hooks.call('onError', error);
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Send a message and wait for response
579
+ */
580
+ async send(payload, sendFn, timeout = null) {
581
+ try {
582
+ // Validate message follows protocol
583
+ this._validateOutgoingMessage(payload);
584
+
585
+ // Attach session ID from HTTP cookie for session sync (WebSocket/HTTP bridge)
586
+ // Only attach for walker execution requests, not for form submissions
587
+ // (forms don't need session sync until AFTER successful login)
588
+ const sessionId = this._getSessionIdFromCookie();
589
+ if (sessionId && payload.event === PROTOCOL_EVENTS.EXECUTE_WALKER) {
590
+ payload._sessionId = sessionId;
591
+ this.logger.log('[MessageHandler] Attached session ID to walker execution');
592
+ }
593
+
594
+ const requestId = this.requestId++;
595
+ payload._requestId = requestId;
596
+
597
+ const timeoutMs = timeout || this.timeout;
598
+
599
+ return new Promise((resolve, reject) => {
600
+ const callback = { resolve, reject };
601
+
602
+ // Set timeout
603
+ callback.timeoutId = setTimeout(() => {
604
+ if (this.callbacks.has(requestId)) {
605
+ this.callbacks.delete(requestId);
606
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
607
+ }
608
+ }, timeoutMs);
609
+
610
+ this.callbacks.set(requestId, callback);
611
+
612
+ try {
613
+ // Send message
614
+ const message = JSON.stringify(payload);
615
+ this.logger.log('Sending message', payload);
616
+ sendFn(message);
617
+ } catch (sendError) {
618
+ // Clean up callback on send failure
619
+ this.callbacks.delete(requestId);
620
+ if (callback.timeoutId) {
621
+ clearTimeout(callback.timeoutId);
622
+ }
623
+ reject(new Error(`Failed to send message: ${sendError.message}`));
624
+ }
625
+ });
626
+ } catch (error) {
627
+ this.logger.error('[MessageHandler] Error in send():', error);
628
+ throw error; // Propagate to caller
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Handle response to a request
634
+ * @private
635
+ */
636
+ _handleResponse(requestId, message) {
637
+ try {
638
+ const callback = this.callbacks.get(requestId);
639
+ if (!callback) {
640
+ return;
641
+ }
642
+
643
+ this.callbacks.delete(requestId);
644
+
645
+ // Clear timeout
646
+ if (callback.timeoutId) {
647
+ clearTimeout(callback.timeoutId);
648
+ }
649
+
650
+ // Resolve or reject
651
+ if (message.error) {
652
+ callback.reject(new Error(message.error));
653
+ } else {
654
+ // Return entire message (minus _requestId) for flexibility
655
+ // Some responses use 'result' field, others use 'success', 'data', etc.
656
+ const { _requestId, ...response } = message;
657
+ callback.resolve(response.result !== undefined ? response.result : response);
658
+ }
659
+ } catch (error) {
660
+ this.logger.error('[MessageHandler] Error handling response:', error);
661
+ // Attempt to reject the callback if it still exists
662
+ const callback = this.callbacks.get(requestId);
663
+ if (callback) {
664
+ this.callbacks.delete(requestId);
665
+ if (callback.timeoutId) {
666
+ clearTimeout(callback.timeoutId);
667
+ }
668
+ callback.reject(new Error(`Response handling error: ${error.message}`));
669
+ }
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Handle broadcast message
675
+ * @private
676
+ */
677
+ _handleBroadcast(message) {
678
+ try {
679
+ this.logger.debug('Broadcast received:', message.type);
680
+ this.hooks.call('onBroadcast', message);
681
+ } catch (error) {
682
+ this.logger.error('[MessageHandler] Error handling broadcast:', error);
683
+ this.hooks.call('onError', { type: 'broadcast_error', error, message });
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Send input response to server
689
+ */
690
+ sendInputResponse(requestId, value, sendFn) {
691
+ const response = {
692
+ event: 'input_response',
693
+ requestId: requestId,
694
+ value: value
695
+ };
696
+
697
+ sendFn(JSON.stringify(response));
698
+ this.logger.log('Sent input response', response);
699
+ }
700
+ }
701
+