@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.
- package/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- package/zSys/zSys.js +14 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L4_Orchestration/rendering/facade.js
|
|
3
|
+
*
|
|
4
|
+
* Rendering Facade
|
|
5
|
+
*
|
|
6
|
+
* Thin delegation layer that ensures orchestrator is loaded and routes
|
|
7
|
+
* rendering requests. Extracted from bifrost_client.js to reduce facade bloat.
|
|
8
|
+
*
|
|
9
|
+
* All methods are simple delegations to ZDisplayOrchestrator.
|
|
10
|
+
*
|
|
11
|
+
* Extracted from bifrost_client.js (Phase 5.1)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export class RenderingFacade {
|
|
15
|
+
constructor(client) {
|
|
16
|
+
this.client = client;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render a complete block of YAML data
|
|
21
|
+
* @param {Object} blockData - Block data to render
|
|
22
|
+
*/
|
|
23
|
+
async renderBlock(blockData) {
|
|
24
|
+
await this.client._ensureZDisplayOrchestrator();
|
|
25
|
+
return this.client.zDisplayOrchestrator.renderBlock(blockData);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Progressive chunk rendering (Terminal First philosophy)
|
|
30
|
+
* Appends chunks from backend as they arrive, stops at failed gates
|
|
31
|
+
* @param {Object} message - Chunk message from backend
|
|
32
|
+
*/
|
|
33
|
+
async renderChunkProgressive(message) {
|
|
34
|
+
// Clear navigation timeout when first chunk arrives
|
|
35
|
+
if (this.client._navigationTimeout) {
|
|
36
|
+
clearTimeout(this.client._navigationTimeout);
|
|
37
|
+
this.client._navigationTimeout = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await this.client._ensureZDisplayOrchestrator();
|
|
41
|
+
return this.client.zDisplayOrchestrator.renderChunkProgressive(message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Recursively render YAML items (handles nested structures like implicit wizards)
|
|
46
|
+
* @param {Object} data - YAML data to render
|
|
47
|
+
* @param {HTMLElement} parentElement - Parent element to render into
|
|
48
|
+
*/
|
|
49
|
+
async renderItems(data, parentElement) {
|
|
50
|
+
await this.client._ensureZDisplayOrchestrator();
|
|
51
|
+
return this.client.zDisplayOrchestrator.renderItems(data, parentElement);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create container wrapper for a zKey with zTheme responsive classes
|
|
56
|
+
* Supports _zClass metadata for customization
|
|
57
|
+
* @param {string} zKey - The key name
|
|
58
|
+
* @param {Object} metadata - Metadata object (_zClass, _zStyle, _zHTML, zId)
|
|
59
|
+
*/
|
|
60
|
+
async createContainer(zKey, metadata) {
|
|
61
|
+
await this.client._ensureZDisplayOrchestrator();
|
|
62
|
+
return this.client.zDisplayOrchestrator.createContainer(zKey, metadata);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render navbar HTML (returns DOM element, doesn't inject into DOM)
|
|
67
|
+
* @param {Array} items - Navbar items (e.g., ['zVaF', 'zAbout', '^zLogin'])
|
|
68
|
+
* @returns {Promise<HTMLElement>} Navbar DOM element
|
|
69
|
+
*/
|
|
70
|
+
async renderMetaNavBarHTML(items) {
|
|
71
|
+
await this.client._ensureZDisplayOrchestrator();
|
|
72
|
+
return this.client.zDisplayOrchestrator.renderMetaNavBarHTML(items);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Render navigation bar from metadata (~zNavBar* in content)
|
|
77
|
+
* @param {Array} items - Navbar items
|
|
78
|
+
* @param {HTMLElement} parentElement - Parent element to render into
|
|
79
|
+
*/
|
|
80
|
+
async renderNavBar(items, parentElement) {
|
|
81
|
+
await this.client._ensureZDisplayOrchestrator();
|
|
82
|
+
return this.client.zDisplayOrchestrator.renderNavBar(items, parentElement);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render a single zDisplay event as DOM element
|
|
87
|
+
* @param {Object} eventData - zDisplay event data
|
|
88
|
+
* @returns {Promise<HTMLElement>} Rendered element
|
|
89
|
+
*/
|
|
90
|
+
async renderZDisplayEvent(eventData) {
|
|
91
|
+
await this.client._ensureZDisplayOrchestrator();
|
|
92
|
+
return this.client.zDisplayOrchestrator.renderZDisplayEvent(eventData);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZoloMedia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# zBifrost Client
|
|
2
|
+
|
|
3
|
+
The browser client for **zBifrost** — the WebSocket bridge between a zOS Python
|
|
4
|
+
server and the browser. It turns JSON events streamed from a zOS server into live
|
|
5
|
+
DOM, with no application code of its own.
|
|
6
|
+
|
|
7
|
+
## What it is
|
|
8
|
+
|
|
9
|
+
A thin bootstrap (`bifrost_client.js`, ~190 lines) that:
|
|
10
|
+
|
|
11
|
+
1. Reads server config injected into the page (`<script id="zui-config">`)
|
|
12
|
+
2. Opens a WebSocket to the zOS server
|
|
13
|
+
3. Receives `connection_info` carrying the core module URL
|
|
14
|
+
4. Dynamically imports `bifrost_core.js` (server-controlled version)
|
|
15
|
+
5. Hands off — all rendering is driven by what the server sends
|
|
16
|
+
|
|
17
|
+
**The server controls what the client loads. The client controls nothing about
|
|
18
|
+
your app.**
|
|
19
|
+
|
|
20
|
+
## What it is not
|
|
21
|
+
|
|
22
|
+
There is no routing logic, no `.zolo` parsing, no RBAC, no business logic, and no
|
|
23
|
+
secrets. The JS receives JSON events from Python and creates DOM elements. That is
|
|
24
|
+
the entire job. See [`docs/SECURITY.md`](docs/SECURITY.md) for the trust boundary.
|
|
25
|
+
|
|
26
|
+
## Structure
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
bifrost_client.js — thin bootstrap (~190 lines), hardcoded in the page <head>
|
|
30
|
+
bifrost_core.js — full WebSocket client, loaded dynamically at runtime
|
|
31
|
+
L1_Foundation/ — WebSocket connection, constants, config, bootstrap, registries
|
|
32
|
+
L2_Handling/ — message handling, renderers, cache, navigation, zvaf, hooks
|
|
33
|
+
L3_Abstraction/ — orchestrators (navbar, wizard gate, session)
|
|
34
|
+
L4_Orchestration/ — rendering facade, renderer/manager registries, lifecycle
|
|
35
|
+
zSys/ — DOM utils, theme utils, accessibility, validation, encoding
|
|
36
|
+
syntax/ — Prism.js grammars for .zolo / zUI / zSchema / zSpark / zEnv
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
The page loads the bootstrap and instantiates it; the server injects the config:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<script src="https://cdn.jsdelivr.net/gh/ZoloAi/zbifrost-client@v1.7.62/bifrost_client.js"></script>
|
|
45
|
+
<script>
|
|
46
|
+
window.bifrostClient = new BifrostClient(null, { autoConnect: true });
|
|
47
|
+
</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The server injects `<script id="zui-config" type="application/json">` with the
|
|
51
|
+
WebSocket config, `zBlock`, `zVaFile`, `zVaFolder`, and pre-built nav HTML, and
|
|
52
|
+
sends `connection_info.bifrost_core_url` over the socket to select the core build.
|
|
53
|
+
|
|
54
|
+
### Pinning & CDN
|
|
55
|
+
|
|
56
|
+
Releases are git tags consumed via jsDelivr. Pin an exact tag in production:
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<script src="https://cdn.jsdelivr.net/gh/ZoloAi/zbifrost-client@v1.7.62/bifrost_client.js"></script>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The matching `bifrost_core.js` version is chosen **server-side** (see the zOS
|
|
63
|
+
bridge `connection_info.bifrost_core_url`) — bump both together on each release.
|
|
64
|
+
For a CDN other than jsDelivr, pass `coreOriginAllowlist` (see
|
|
65
|
+
[`docs/SECURITY.md`](docs/SECURITY.md#core-import-origin-pinning)).
|
|
66
|
+
|
|
67
|
+
## Documentation
|
|
68
|
+
|
|
69
|
+
| Guide | What it covers |
|
|
70
|
+
|-------|----------------|
|
|
71
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Bootstrap/core split, L1–L4 + zSys layers, registries, lazy loading |
|
|
72
|
+
| [docs/PROTOCOL.md](docs/PROTOCOL.md) | The wire protocol from the client's side: `connection_info`, `render_chunk` + opcode decoding, the `PROTOCOL_EVENTS` vocabulary, message flow |
|
|
73
|
+
| [docs/RENDERERS.md](docs/RENDERERS.md) | The renderer model, the registry, how to add a renderer, the HTML-escape SSOT |
|
|
74
|
+
| [docs/SECURITY.md](docs/SECURITY.md) | Trust boundary, XSS escaping SSOT, core-import origin pinning, session-cookie handling |
|
|
75
|
+
|
|
76
|
+
> The **server-side** zBifrost mechanism (render-opcode encoding, auth, chunking)
|
|
77
|
+
> is documented in the zOS/zGuard repos — it is intentionally not shipped here.
|
|
78
|
+
> This repo documents only the open, browser-side renderer.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bifrost_client.js — Thin bootstrap for BifrostCore
|
|
3
|
+
*
|
|
4
|
+
* Phase 4: Reduced to ~180 lines.
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* 1. Read zui-config from DOM
|
|
7
|
+
* 2. Open a raw WebSocket to receive connection_info
|
|
8
|
+
* 3. connection_info.bifrost_core_url → dynamic import(url) of the real client
|
|
9
|
+
* 4. Instantiate BifrostCore and set window.bifrostClient
|
|
10
|
+
* 5. Forward registerHook() calls queued before core is ready
|
|
11
|
+
*
|
|
12
|
+
* Intelligence lives in bifrost_core.js (server-controlled URL = Phase 3B).
|
|
13
|
+
* zVaF.html does: new BifrostClient() — autoConnect defaults on; pass
|
|
14
|
+
* { autoConnect: false } to opt out (the exception, not the rule).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
(function (root) {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function readZuiConfig() {
|
|
23
|
+
try {
|
|
24
|
+
const el = document.getElementById('zui-config');
|
|
25
|
+
return el ? JSON.parse(el.textContent) : {};
|
|
26
|
+
} catch (_) { return {}; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildWsUrl(wsCfg) {
|
|
30
|
+
wsCfg = wsCfg || {};
|
|
31
|
+
const proto = wsCfg.ssl_enabled ? 'wss' : 'ws';
|
|
32
|
+
const host = wsCfg.host || '127.0.0.1';
|
|
33
|
+
const port = wsCfg.port || 8765;
|
|
34
|
+
return `${proto}://${host}:${port}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeLogger(cfg) {
|
|
38
|
+
const isProd = cfg.deployment === 'Production';
|
|
39
|
+
const prefix = '[BifrostBootstrap]';
|
|
40
|
+
return {
|
|
41
|
+
debug: isProd ? () => {} : (...a) => console.debug(prefix, ...a),
|
|
42
|
+
info: isProd ? () => {} : (...a) => console.info(prefix, ...a),
|
|
43
|
+
warn: (...a) => console.warn(prefix, ...a),
|
|
44
|
+
error: (...a) => console.error(prefix, ...a),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── BifrostClient (bootstrap) ──────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
class BifrostClient {
|
|
51
|
+
constructor(url, options = {}) {
|
|
52
|
+
// Ergonomic single-object form: new BifrostClient({ zHooks: {...} }).
|
|
53
|
+
// A non-string first arg is treated as the options bag.
|
|
54
|
+
if (url && typeof url === 'object') { options = url; url = null; }
|
|
55
|
+
|
|
56
|
+
this._cfg = readZuiConfig();
|
|
57
|
+
this._opts = options;
|
|
58
|
+
this._url = url || buildWsUrl(this._cfg.websocket);
|
|
59
|
+
this._core = null;
|
|
60
|
+
this._coreLoading = false;
|
|
61
|
+
this._pendingHooks = []; // hooks registered before core is ready
|
|
62
|
+
this.logger = makeLogger(this._cfg);
|
|
63
|
+
|
|
64
|
+
// Default-on: connect unless the caller explicitly opts out.
|
|
65
|
+
if (options.autoConnect !== false) {
|
|
66
|
+
this._bootstrap();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── bootstrap: minimal WS just to receive connection_info ─────────────────
|
|
71
|
+
|
|
72
|
+
_bootstrap() {
|
|
73
|
+
const ws = this._ws = new WebSocket(this._url);
|
|
74
|
+
|
|
75
|
+
ws.addEventListener('open', () => {
|
|
76
|
+
this.logger.info('Connected (bootstrap)');
|
|
77
|
+
// Do NOT send execute_walker here — BifrostCore will do it on its own connect.
|
|
78
|
+
// Sending here would cause a redundant server-side walker execution on an
|
|
79
|
+
// orphaned WS connection that we are about to close.
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
ws.addEventListener('message', ({ data }) => {
|
|
83
|
+
let msg;
|
|
84
|
+
try { msg = JSON.parse(data); } catch { return; }
|
|
85
|
+
|
|
86
|
+
if (msg.event === 'connection_info' && !this._core) {
|
|
87
|
+
this._loadCore(msg).catch(err => this.logger.error('Core load failed:', err));
|
|
88
|
+
}
|
|
89
|
+
// All other messages are ignored by the bootstrap; BifrostCore handles them.
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
ws.addEventListener('close', () => {
|
|
93
|
+
if (!this._core && !this._coreLoading) this.logger.warn('Bootstrap WS closed before core loaded');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
ws.addEventListener('error', () => {
|
|
97
|
+
this.logger.error('Bootstrap WS error');
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── core loading ──────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
async _loadCore(connectionInfo) {
|
|
104
|
+
this._coreLoading = true;
|
|
105
|
+
// 3B: server tells us the authoritative core URL; falls back to local.
|
|
106
|
+
// Relative ("/...") resolves against the page origin; absolute URLs are
|
|
107
|
+
// taken as-is (CDN deployments).
|
|
108
|
+
const rawUrl = connectionInfo.bifrost_core_url || '/bifrost/src/bifrost_core.js';
|
|
109
|
+
let coreUrl;
|
|
110
|
+
try {
|
|
111
|
+
coreUrl = new URL(rawUrl, window.location.origin).href;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
this._coreLoading = false;
|
|
114
|
+
this.logger.error('Invalid bifrost_core_url, refusing to load core:', rawUrl);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Origin pinning: bifrost_core_url arrives over the WebSocket and is therefore
|
|
119
|
+
// attacker-influenceable if connection_info is spoofed. Only import from the
|
|
120
|
+
// page origin, the official jsDelivr CDN (where the canonical client ships),
|
|
121
|
+
// or an explicitly configured allowlist — never an arbitrary origin, which
|
|
122
|
+
// would load attacker code into the page context.
|
|
123
|
+
const allowedOrigins = [
|
|
124
|
+
window.location.origin,
|
|
125
|
+
'https://cdn.jsdelivr.net',
|
|
126
|
+
...(this._opts.coreOriginAllowlist || []),
|
|
127
|
+
];
|
|
128
|
+
const coreOrigin = new URL(coreUrl).origin;
|
|
129
|
+
if (!allowedOrigins.includes(coreOrigin)) {
|
|
130
|
+
this._coreLoading = false;
|
|
131
|
+
this.logger.error(
|
|
132
|
+
`Refusing to load bifrost core from disallowed origin "${coreOrigin}". ` +
|
|
133
|
+
`Allowed: ${allowedOrigins.join(', ')}. ` +
|
|
134
|
+
`Set opts.coreOriginAllowlist to permit a trusted CDN.`
|
|
135
|
+
);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.logger.info('Loading core from', coreUrl);
|
|
139
|
+
|
|
140
|
+
// Close the bootstrap WS cleanly before the core opens its own
|
|
141
|
+
this._ws.close();
|
|
142
|
+
|
|
143
|
+
const mod = await import(coreUrl);
|
|
144
|
+
|
|
145
|
+
// Instantiate BifrostCore — it reads zui-config itself, connects, sends execute_walker
|
|
146
|
+
const core = new mod.BifrostCore(this._url, {
|
|
147
|
+
autoConnect: true,
|
|
148
|
+
zTheme: false, // ztheme.js already loaded by zVaF.html
|
|
149
|
+
...this._opts,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
this._core = core;
|
|
153
|
+
window.bifrostClient = core;
|
|
154
|
+
|
|
155
|
+
// Replay any hooks registered on the bootstrap before the core was ready
|
|
156
|
+
for (const { hookName, fn } of this._pendingHooks) {
|
|
157
|
+
core.registerHook(hookName, fn);
|
|
158
|
+
}
|
|
159
|
+
this._pendingHooks = [];
|
|
160
|
+
|
|
161
|
+
this.logger.info('Core attached — window.bifrostClient is now BifrostCore');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── forwarding API (called by zVaF.html / menu_integration before core ready) ──
|
|
165
|
+
|
|
166
|
+
registerHook(hookName, fn) {
|
|
167
|
+
if (this._core) {
|
|
168
|
+
this._core.registerHook(hookName, fn);
|
|
169
|
+
} else {
|
|
170
|
+
this._pendingHooks.push({ hookName, fn });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
unregisterHook(hookName) {
|
|
175
|
+
if (this._core) this._core.unregisterHook(hookName);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
send(data) {
|
|
179
|
+
if (this._core) return this._core.send(data);
|
|
180
|
+
this.logger.warn('send() called before core is ready');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
read(resource, params) {
|
|
184
|
+
if (this._core) return this._core.read(resource, params);
|
|
185
|
+
return Promise.reject(new Error('BifrostClient: core not yet loaded'));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Expose connect() for callers who do: client.connect() manually
|
|
189
|
+
async connect() {
|
|
190
|
+
// Bootstrap already opened a WS; if core is loaded, delegate
|
|
191
|
+
if (this._core) return this._core.connect();
|
|
192
|
+
// Otherwise wait for core to be ready (already bootstrapping)
|
|
193
|
+
return new Promise((resolve) => {
|
|
194
|
+
const poll = setInterval(() => {
|
|
195
|
+
if (this._core) { clearInterval(poll); resolve(); }
|
|
196
|
+
}, 50);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Expose globally — zVaF.html does: window.bifrostClient = new BifrostClient(...)
|
|
202
|
+
root.BifrostClient = BifrostClient;
|
|
203
|
+
|
|
204
|
+
}(typeof self !== 'undefined' ? self : this));
|