@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,111 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
The client is split into a tiny **bootstrap** and a lazily-loaded **core**, layered
|
|
4
|
+
`L1 → L4` with a cross-cutting `zSys` utility layer. Nothing here knows about your
|
|
5
|
+
application — it only knows how to open a socket, decode events, and build DOM.
|
|
6
|
+
|
|
7
|
+
## Bootstrap / core split
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
page <head>
|
|
11
|
+
└─ bifrost_client.js (BifrostClient, ~190 LOC, hardcoded & pinned in HTML)
|
|
12
|
+
1. read #zui-config (server-injected JSON)
|
|
13
|
+
2. open WebSocket (ws[s]://host:port from config)
|
|
14
|
+
3. recv connection_info { bifrost_core_url, nav_html, session, ... }
|
|
15
|
+
4. import(bifrost_core_url) ← server picks the core version
|
|
16
|
+
5. new BifrostCore(url, opts) → window.bifrostClient
|
|
17
|
+
6. replay any hooks queued before the core was ready
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Why two files:
|
|
21
|
+
|
|
22
|
+
- **`bifrost_client.js`** is small, stable, and pinned in the page. It almost never
|
|
23
|
+
changes, so it can be cached hard and rarely needs a redeploy of the host page.
|
|
24
|
+
- **`bifrost_core.js`** holds all the logic and is selected **by the server** at
|
|
25
|
+
connect time (`connection_info.bifrost_core_url`). The server therefore controls
|
|
26
|
+
exactly which renderer build every client runs — no page edit required to ship a
|
|
27
|
+
new client.
|
|
28
|
+
|
|
29
|
+
The bootstrap origin-pins that import; see
|
|
30
|
+
[SECURITY.md](SECURITY.md#core-import-origin-pinning).
|
|
31
|
+
|
|
32
|
+
> `bifrost_core.js` is intentionally written **without top-level `import`** so it can
|
|
33
|
+
> also be consumed as a plain `<script>` (UMD-style). Its lazily-loaded submodules
|
|
34
|
+
> (everything under `L1`–`L4`/`zSys`) are standard ES modules pulled in via dynamic
|
|
35
|
+
> `import()`.
|
|
36
|
+
|
|
37
|
+
## Layers
|
|
38
|
+
|
|
39
|
+
| Layer | Path | Responsibility |
|
|
40
|
+
|-------|------|----------------|
|
|
41
|
+
| **L1 — Foundation** | `L1_Foundation/` | WebSocket connection, constants (incl. the protocol vocabulary), config parsing, bootstrap, module registry, CDN/Prism loaders |
|
|
42
|
+
| **L2 — Handling** | `L2_Handling/` | Message handling & correlation, the renderer set, cache, navigation, zVaF mounting, hooks |
|
|
43
|
+
| **L3 — Abstraction** | `L3_Abstraction/` | Orchestrators that compose handlers (navbar, wizard gate, session) |
|
|
44
|
+
| **L4 — Orchestration** | `L4_Orchestration/` | The public client facade, renderer/manager **registries**, lifecycle |
|
|
45
|
+
| **zSys** | `zSys/` | Cross-cutting utilities: DOM, theme, accessibility, validation, encoding (HTML escape SSOT) |
|
|
46
|
+
|
|
47
|
+
Imports flow **downward** (`L4 → L1 → zSys`); lower layers never import higher ones.
|
|
48
|
+
|
|
49
|
+
## Registries & lazy loading
|
|
50
|
+
|
|
51
|
+
The core does not eagerly load 40+ renderer modules. Two registries map a logical
|
|
52
|
+
name to a module path + class and load on first use:
|
|
53
|
+
|
|
54
|
+
- **`L4_Orchestration/facade/renderer_registry.js`** — `RENDERER_REGISTRY` maps a
|
|
55
|
+
renderer key (`text`, `header`, `card`, `table`, `zMenu`, `zTerminal`, …) to its
|
|
56
|
+
module path and class. `ensureRenderer(key)` dynamic-imports and caches it.
|
|
57
|
+
- **`L4_Orchestration/facade/manager_registry.js`** — same pattern for managers
|
|
58
|
+
(cache, zVaF, navigation, hooks).
|
|
59
|
+
|
|
60
|
+
This keeps time-to-first-paint cheap: only the renderers a page actually uses are
|
|
61
|
+
fetched. (A `critical`/`deferred` preload tiering is sketched as a TODO in the
|
|
62
|
+
registry for further first-paint tuning.)
|
|
63
|
+
|
|
64
|
+
## Module map (high level)
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
L1_Foundation/
|
|
68
|
+
bootstrap/ bootstrap.js, cdn_loader.js, prism_loader.js, module_registry.js
|
|
69
|
+
config/ client_config.js, config.js (parse #zui-config)
|
|
70
|
+
connection/ websocket_connection.js (open/reconnect lifecycle)
|
|
71
|
+
constants/ bifrost_constants.js (TIMEOUTS, EVENT_TYPES,
|
|
72
|
+
PROTOCOL_EVENTS, CSS_CLASSES…)
|
|
73
|
+
logger/
|
|
74
|
+
|
|
75
|
+
L2_Handling/
|
|
76
|
+
message/ message_handler.js (parse → decode opcodes → dispatch)
|
|
77
|
+
display/
|
|
78
|
+
outputs/ text, typography, header, card, code, list, table, alert, icon,
|
|
79
|
+
image, dl …
|
|
80
|
+
inputs/ button, form, input, input_request
|
|
81
|
+
feedback/ progressbar, spinner
|
|
82
|
+
composite/ dashboard, swiper, terminal, wizard_conditional
|
|
83
|
+
navigation/ menu, navigation
|
|
84
|
+
primitives/ low-level DOM builders shared by renderers (links, typography,
|
|
85
|
+
tables, media, lists, semantic elements)
|
|
86
|
+
orchestration/ zdisplay_orchestrator.js
|
|
87
|
+
cache/ navigation/ zvaf/ hooks/
|
|
88
|
+
|
|
89
|
+
L3_Abstraction/ orchestrator/ renderer/ session/
|
|
90
|
+
|
|
91
|
+
L4_Orchestration/
|
|
92
|
+
facade/ facade.js, renderer_registry.js, manager_registry.js
|
|
93
|
+
rendering/ rendering facade
|
|
94
|
+
client/ lifecycle/
|
|
95
|
+
|
|
96
|
+
zSys/
|
|
97
|
+
dom/ dom_utils.js, style_utils.js, encoding_utils.js (escapeHtml/safeHref SSOT)
|
|
98
|
+
theme/ ztheme_utils.js
|
|
99
|
+
accessibility/ emoji_accessibility.js
|
|
100
|
+
errors/ validation/
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Lifecycle (one connection)
|
|
104
|
+
|
|
105
|
+
1. Page instantiates `BifrostClient` → bootstrap connects, gets `connection_info`.
|
|
106
|
+
2. `BifrostCore` connects its own socket and sends the first `execute_walker`.
|
|
107
|
+
3. Server streams `render_chunk` messages (opcode-encoded display trees).
|
|
108
|
+
4. `message_handler` decodes opcodes → display events and dispatches to renderers
|
|
109
|
+
via hooks (`onRenderChunk`, `onDisplay`, `onMenu`, …).
|
|
110
|
+
5. Renderers build DOM into the `zVaF` mount; navigation/menus drive subsequent
|
|
111
|
+
walker calls. See [PROTOCOL.md](PROTOCOL.md).
|
package/docs/PROTOCOL.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Wire Protocol (client side)
|
|
2
|
+
|
|
3
|
+
This describes the protocol **as the browser sees it**. The authoritative,
|
|
4
|
+
server-side encoding (how display trees become opcodes, auth, chunking) lives in
|
|
5
|
+
the zOS/zGuard repos and is deliberately not duplicated here. The client only
|
|
6
|
+
needs to: connect, decode, and dispatch.
|
|
7
|
+
|
|
8
|
+
All messages are JSON. The discriminator is the `event` field (a few legacy paths
|
|
9
|
+
also look at `display_event` / `type`).
|
|
10
|
+
|
|
11
|
+
## 1. Handshake — `connection_info`
|
|
12
|
+
|
|
13
|
+
After the socket opens, the server sends exactly one `connection_info`:
|
|
14
|
+
|
|
15
|
+
```jsonc
|
|
16
|
+
{
|
|
17
|
+
"event": "connection_info",
|
|
18
|
+
"data": {
|
|
19
|
+
"server_version": "…",
|
|
20
|
+
"bifrost_core_url": "https://cdn.jsdelivr.net/gh/ZoloAi/zbifrost-client@v1.7.52/bifrost_core.js",
|
|
21
|
+
"nav_html": "<nav>…</nav>", // pre-built, RBAC-filtered navbar
|
|
22
|
+
"session": { "authenticated": false, "username": null, "role": null, … },
|
|
23
|
+
"features": [ … ],
|
|
24
|
+
"available_models": [ … ]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- The **bootstrap** uses `bifrost_core_url` to `import()` the core (origin-pinned —
|
|
30
|
+
see [SECURITY.md](SECURITY.md#core-import-origin-pinning)).
|
|
31
|
+
- `nav_html` is consumed as-is; the client does not build navigation itself.
|
|
32
|
+
- `session` is display state only (navbar, RBAC-driven visibility). It is **not**
|
|
33
|
+
proof of identity — the server re-validates every request.
|
|
34
|
+
|
|
35
|
+
## 2. Render stream — `render_chunk` + opcodes
|
|
36
|
+
|
|
37
|
+
The server streams the page as a sequence of `render_chunk` messages. To keep the
|
|
38
|
+
zOS display vocabulary off the wire, each render node's event name is encoded to a
|
|
39
|
+
short **opcode** on `node.e`. The client decodes it back before dispatching.
|
|
40
|
+
|
|
41
|
+
```jsonc
|
|
42
|
+
// on the wire (encoded) // after _decodeRenderNode()
|
|
43
|
+
{ "e": "tx", "content": "Hi" } → { "event": "text", "content": "Hi" }
|
|
44
|
+
{ "e": "hd", "label": "Title" } → { "event": "header", "label": "Title" }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The decode table `_ZRENDER_OPS` in `L2_Handling/message/message_handler.js` is a
|
|
48
|
+
**hand-maintained mirror** of the server SSOT
|
|
49
|
+
(`render_opcodes.py` → `EVENT_TO_OP`, 35 entries). The decoder:
|
|
50
|
+
|
|
51
|
+
- recurses into arrays and child nodes,
|
|
52
|
+
- decodes any node carrying a known opcode (`node.e`),
|
|
53
|
+
- **warns once** (`_warnUnknownOpcode`) on an unknown opcode instead of silently
|
|
54
|
+
dropping it — this surfaces client/server drift loudly. If you see
|
|
55
|
+
`[zRender] Unknown render opcode "…"`, the client mirror is behind the server.
|
|
56
|
+
|
|
57
|
+
> Opcodes are an obfuscation/compactness layer, **not** a security boundary. They
|
|
58
|
+
> carry no routes, field names, or wizard flow — only the display-event vocabulary.
|
|
59
|
+
|
|
60
|
+
## 3. Control & display events — `PROTOCOL_EVENTS`
|
|
61
|
+
|
|
62
|
+
Top-level `message.event` values are the protocol vocabulary. They are the SSOT in
|
|
63
|
+
`L1_Foundation/constants/bifrost_constants.js` as **`PROTOCOL_EVENTS`** /
|
|
64
|
+
**`PROTOCOL_REASONS`** — `message_handler` dispatch references these constants, not
|
|
65
|
+
raw string literals.
|
|
66
|
+
|
|
67
|
+
| Group | Events |
|
|
68
|
+
|-------|--------|
|
|
69
|
+
| Transport / connection | `render_chunk`, `connection_info`, `navigate_back`, `error` |
|
|
70
|
+
| Display / output | `display`, `output`, `zTable`, `zDash`, `zMenu`, `zDialog`, `swiper_init` |
|
|
71
|
+
| Progress / spinner | `progress_bar`, `progress_update`, `progress_complete`, `spinner_start`, `spinner_stop` |
|
|
72
|
+
| Input req/res | `request_input`, `input_request`, `input_response` |
|
|
73
|
+
| Execution / wizard / RBAC | `execute_walker`, `execute_zfunc_response`, `execute_code_response`, `zfunc_exec`, `wizard_gate_result`, `rbac_denied` |
|
|
74
|
+
| Logging | `app_log` |
|
|
75
|
+
|
|
76
|
+
`navigate_back` carries a `reason` (`PROTOCOL_REASONS`):
|
|
77
|
+
`bounce_back_block_completed` (post-login/logout bounce) and `rbac_denied`
|
|
78
|
+
(access-denied redirect).
|
|
79
|
+
|
|
80
|
+
> `EVENT_TYPES` in the same file is a **separate** map — browser-native DOM event
|
|
81
|
+
> names (`click`, `change`, …) used with `addEventListener`. Do not confuse it with
|
|
82
|
+
> the wire `PROTOCOL_EVENTS`.
|
|
83
|
+
|
|
84
|
+
## 4. Dispatch flow
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
WebSocket message
|
|
88
|
+
→ JSON.parse
|
|
89
|
+
→ message.event === render_chunk ? decode opcodes → hooks.call('onRenderChunk')
|
|
90
|
+
→ connection_info ? hooks.call('onConnectionInfo' / 'onConnected')
|
|
91
|
+
→ error ? show alert + hooks.call('onError')
|
|
92
|
+
→ navigate_back ? history.back() / client-side route by reason
|
|
93
|
+
→ zDash / zMenu / rbac_denied ? dedicated hooks
|
|
94
|
+
→ _requestId correlated ? resolve the pending request promise
|
|
95
|
+
→ display / progress / spinner / input / swiper / app_log / zfunc_exec → hooks
|
|
96
|
+
→ else broadcast
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Renderers subscribe to these hooks and build DOM. See [RENDERERS.md](RENDERERS.md).
|
|
100
|
+
|
|
101
|
+
## 5. Outgoing
|
|
102
|
+
|
|
103
|
+
The client sends `execute_walker` (and friends) back to the server. When a session
|
|
104
|
+
cookie is readable it is attached to walker requests as a best-effort sync hint
|
|
105
|
+
(see [SECURITY.md](SECURITY.md#session-cookie-handling)); the server remains the
|
|
106
|
+
authority.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Renderers
|
|
2
|
+
|
|
3
|
+
A renderer turns one decoded display event into DOM. Renderers are **pure**: no
|
|
4
|
+
WebSocket, no app state, no side effects beyond producing/returning elements. They
|
|
5
|
+
consume Layer-2 *primitives* and `zSys` utilities rather than hand-rolling DOM or
|
|
6
|
+
escaping.
|
|
7
|
+
|
|
8
|
+
## The model
|
|
9
|
+
|
|
10
|
+
- Each renderer is an ES-module class under `L2_Handling/display/<group>/`.
|
|
11
|
+
- It exposes render method(s) that accept a decoded event object (`eventData`) and
|
|
12
|
+
return an `HTMLElement` (or append into a container).
|
|
13
|
+
- It is registered in `L4_Orchestration/facade/renderer_registry.js` so it is
|
|
14
|
+
lazy-loaded on first use.
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
// L2_Handling/display/outputs/typography_renderer.js
|
|
18
|
+
export class TypographyRenderer {
|
|
19
|
+
constructor(logger) { this.logger = logger; }
|
|
20
|
+
renderText(eventData) {
|
|
21
|
+
// build & return an HTMLElement from eventData
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## The registry
|
|
27
|
+
|
|
28
|
+
`RENDERER_REGISTRY` maps a logical renderer key to its module path + class:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
export const RENDERER_REGISTRY = {
|
|
32
|
+
text: { path: 'L2_Handling/display/outputs/text_renderer.js', className: 'TextRenderer', isDefault: true, passClient: false },
|
|
33
|
+
header: { path: 'L2_Handling/display/outputs/header_renderer.js', className: 'HeaderRenderer', isDefault: true, passClient: false },
|
|
34
|
+
table: { path: 'L2_Handling/display/outputs/table_renderer.js', className: 'TableRenderer', isDefault: true, passClient: false },
|
|
35
|
+
zMenu: { path: 'L2_Handling/display/navigation/menu_renderer.js', className: 'MenuRenderer', isDefault: true, passClient: true },
|
|
36
|
+
// …
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `className` — the exported class name to instantiate.
|
|
41
|
+
- `passClient` — `true` if the renderer needs a reference to the live client (e.g.
|
|
42
|
+
navigation/menu renderers that trigger walker calls); `false` for pure output
|
|
43
|
+
renderers.
|
|
44
|
+
- `ensureRenderer(key)` dynamic-imports and caches the instance on first use.
|
|
45
|
+
|
|
46
|
+
The renderer **key** corresponds to the decoded `event` name from the wire
|
|
47
|
+
(`text`, `header`, `zTable`, `zMenu`, …) — see [PROTOCOL.md](PROTOCOL.md).
|
|
48
|
+
|
|
49
|
+
## Renderer set (by group)
|
|
50
|
+
|
|
51
|
+
| Group | Renderers |
|
|
52
|
+
|-------|-----------|
|
|
53
|
+
| `outputs/` | text, typography, header, code, card, list, dl, table, alert, icon, image |
|
|
54
|
+
| `inputs/` | button, form, input, input_request |
|
|
55
|
+
| `feedback/` | progressbar, spinner |
|
|
56
|
+
| `composite/` | dashboard, swiper, terminal, wizard_conditional |
|
|
57
|
+
| `navigation/` | menu, navigation |
|
|
58
|
+
| `specialized/` | input_request |
|
|
59
|
+
|
|
60
|
+
Shared low-level DOM builders live in `L2_Handling/display/primitives/`
|
|
61
|
+
(`link_primitives`, `typography_primitives`, `table_primitives`, `media_primitives`,
|
|
62
|
+
`lists_primitives`, `semantic_element_primitive`, …). Prefer composing these over
|
|
63
|
+
writing raw element code in a renderer.
|
|
64
|
+
|
|
65
|
+
## HTML escaping is centralized (SSOT)
|
|
66
|
+
|
|
67
|
+
Renderers must **never** hand-roll escape chains (`.replace(/&/g, …)`) or build
|
|
68
|
+
attribute strings by hand. Use the single source of truth in
|
|
69
|
+
`zSys/dom/encoding_utils.js`:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { escapeHtml, safeHref } from '../../../zSys/dom/encoding_utils.js';
|
|
73
|
+
|
|
74
|
+
// element text or attribute value (escapes & < > " ')
|
|
75
|
+
el.innerHTML = `<span title="${escapeHtml(label)}">${escapeHtml(text)}</span>`;
|
|
76
|
+
|
|
77
|
+
// href/src values — blocks javascript:/data:/vbscript: then attr-escapes
|
|
78
|
+
a.innerHTML = `<a href="${safeHref(url)}">${escapeHtml(label)}</a>`;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- `escapeHtml(value)` — escapes the five HTML-significant characters; safe for both
|
|
82
|
+
text and quoted-attribute contexts; deterministic, no DOM dependency.
|
|
83
|
+
- `safeHref(url)` — sanitizes a URL for an `href`/`src` attribute: blocks dangerous
|
|
84
|
+
schemes (including whitespace/control-char obfuscation), returns `#` for blocked
|
|
85
|
+
or empty input, otherwise attribute-escapes. Run resolved/zPath URLs through it.
|
|
86
|
+
|
|
87
|
+
Rationale and the broader trust boundary are in [SECURITY.md](SECURITY.md).
|
|
88
|
+
|
|
89
|
+
## Adding a renderer
|
|
90
|
+
|
|
91
|
+
1. Create `L2_Handling/display/<group>/<name>_renderer.js` exporting a class with a
|
|
92
|
+
`render…(eventData)` method that returns an `HTMLElement`.
|
|
93
|
+
2. Build DOM via `display/primitives/*` and `zSys/dom/*`; escape every dynamic value
|
|
94
|
+
through `escapeHtml` / `safeHref`.
|
|
95
|
+
3. Add an entry to `RENDERER_REGISTRY` keyed by the decoded event name; set
|
|
96
|
+
`passClient: true` only if you need the live client.
|
|
97
|
+
4. If the event is a **new** wire event, add it to `PROTOCOL_EVENTS` in
|
|
98
|
+
`bifrost_constants.js` and (if it is a render-node op) confirm the server added
|
|
99
|
+
the opcode — the client mirror `_ZRENDER_OPS` and `render_opcodes.py` must agree
|
|
100
|
+
(an unknown opcode logs a drift warning; see [PROTOCOL.md](PROTOCOL.md#2-render-stream--render_chunk--opcodes)).
|
|
101
|
+
5. Syntax-check (ESM): `node --input-type=module --check < path/to/renderer.js`.
|
package/docs/SECURITY.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Security & Trust Boundary
|
|
2
|
+
|
|
3
|
+
This client is a **thin renderer**. Keeping it thin is the security model: the less
|
|
4
|
+
it knows and decides, the smaller its attack surface. This document records the
|
|
5
|
+
boundary and the client-side hardening.
|
|
6
|
+
|
|
7
|
+
## The boundary: what stays server-side
|
|
8
|
+
|
|
9
|
+
The client holds **none** of the following — they live in the zOS/zGuard server:
|
|
10
|
+
|
|
11
|
+
- routing / route resolution
|
|
12
|
+
- `.zolo` parsing or any application grammar
|
|
13
|
+
- RBAC decisions (the client only *displays* RBAC-filtered output the server sends)
|
|
14
|
+
- business logic, schemas, secrets, tokens, signing keys
|
|
15
|
+
- the render-event vocabulary (sent as opaque opcodes; see
|
|
16
|
+
[PROTOCOL.md](PROTOCOL.md#2-render-stream--render_chunk--opcodes))
|
|
17
|
+
|
|
18
|
+
The client's job is: open a socket, decode JSON events, build DOM. Anything that
|
|
19
|
+
would leak mechanism or grant trust is the server's responsibility. Render opcodes
|
|
20
|
+
are an obfuscation/compactness layer, **not** an authorization boundary.
|
|
21
|
+
|
|
22
|
+
## DOM-XSS: centralized escaping (SSOT)
|
|
23
|
+
|
|
24
|
+
Renderers use `innerHTML` for speed, so any dynamic value interpolated into markup
|
|
25
|
+
must be escaped through the single source of truth in
|
|
26
|
+
`zSys/dom/encoding_utils.js`:
|
|
27
|
+
|
|
28
|
+
- **`escapeHtml(value)`** — escapes `& < > " '`. Safe for text and quoted-attribute
|
|
29
|
+
contexts. No `.replace()` chains anywhere else in the codebase.
|
|
30
|
+
- **`safeHref(url)`** — blocks `javascript:` / `data:` / `vbscript:` schemes
|
|
31
|
+
(including whitespace/control-char obfuscation), returns `#` for blocked/empty,
|
|
32
|
+
otherwise attribute-escapes. All `href`/`src` values go through it.
|
|
33
|
+
|
|
34
|
+
Notably, markdown link assembly in `text_renderer.js` escapes the link label and
|
|
35
|
+
runs the resolved href through `safeHref`, so a `[label](javascript:…)` payload can
|
|
36
|
+
never produce an executable link. See [RENDERERS.md](RENDERERS.md#html-escaping-is-centralized-ssot).
|
|
37
|
+
|
|
38
|
+
> The client escapes its own output, but it cannot vouch for what a zApp echoes. zApps
|
|
39
|
+
> that display untrusted user input are responsible for not defeating these guards.
|
|
40
|
+
|
|
41
|
+
## Core-import origin pinning
|
|
42
|
+
|
|
43
|
+
The bootstrap dynamically `import()`s `bifrost_core.js` from
|
|
44
|
+
`connection_info.bifrost_core_url`. Because `connection_info` arrives over the
|
|
45
|
+
socket, a spoofed payload could otherwise point the import at attacker-hosted code.
|
|
46
|
+
`bifrost_client._loadCore` therefore:
|
|
47
|
+
|
|
48
|
+
1. resolves the URL with `new URL(rawUrl, location.origin)` (rejects malformed),
|
|
49
|
+
2. allows the import **only** from an allowlisted origin:
|
|
50
|
+
- the page origin (`window.location.origin`),
|
|
51
|
+
- the official jsDelivr CDN (`https://cdn.jsdelivr.net`, where the canonical
|
|
52
|
+
client ships),
|
|
53
|
+
- any origin you opt into via `opts.coreOriginAllowlist`.
|
|
54
|
+
|
|
55
|
+
Anything else is refused with a logged error and the core is not loaded.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// permit a custom/self-hosted CDN origin:
|
|
59
|
+
new BifrostClient(null, {
|
|
60
|
+
autoConnect: true,
|
|
61
|
+
coreOriginAllowlist: ['https://cdn.example.com'],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> Loading from `https://cdn.jsdelivr.net` is allowed by default because that is the
|
|
66
|
+
> canonical distribution channel and the version is still chosen by *your* server.
|
|
67
|
+
> Self-hosting the core? Serve it from the page origin (no config needed) or add its
|
|
68
|
+
> origin to `coreOriginAllowlist`.
|
|
69
|
+
|
|
70
|
+
## Session-cookie handling
|
|
71
|
+
|
|
72
|
+
`message_handler._getSessionIdFromCookie` reads `session`/`sessionid` from
|
|
73
|
+
`document.cookie` and attaches it to `execute_walker` requests as a best-effort
|
|
74
|
+
session-sync hint for the WebSocket/HTTP bridge.
|
|
75
|
+
|
|
76
|
+
- This only works when the cookie is **not** `HttpOnly`.
|
|
77
|
+
- It is **not** proof of identity — the server re-validates every request.
|
|
78
|
+
- `HttpOnly` deployments (recommended) simply get `null` here and rely on the
|
|
79
|
+
browser attaching the cookie to the WS handshake — the safer path.
|
|
80
|
+
|
|
81
|
+
## Opcode-mirror drift
|
|
82
|
+
|
|
83
|
+
`_ZRENDER_OPS` mirrors the server `render_opcodes.py`. An unknown opcode is **not**
|
|
84
|
+
silently dropped — `_warnUnknownOpcode` logs once so a server change the client
|
|
85
|
+
hasn't mirrored is visible. Treat `[zRender] Unknown render opcode "…"` as "update
|
|
86
|
+
the client mirror / bump the client version."
|
|
87
|
+
|
|
88
|
+
## Reporting
|
|
89
|
+
|
|
90
|
+
This is the public, open-source renderer. Report client-side issues against the
|
|
91
|
+
`zbifrost-client` repo. Server-side concerns (auth, RBAC, `execute_code`, the
|
|
92
|
+
sealed network runtime) belong to the zOS/zGuard projects.
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zolomedia/bifrost-client",
|
|
3
|
+
"version": "1.7.74",
|
|
4
|
+
"description": "Browser client for zBifrost — the WebSocket bridge that turns JSON events from a zOS server into live DOM. Thin bootstrap + server-controlled core.",
|
|
5
|
+
"homepage": "https://github.com/ZoloAi/zbifrost-client#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ZoloAi/zbifrost-client.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"zos",
|
|
13
|
+
"zbifrost",
|
|
14
|
+
"websocket",
|
|
15
|
+
"ssr",
|
|
16
|
+
"declarative-ui"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"release:patch": "npm version patch && npm publish"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism.js language definition for zconfig
|
|
3
|
+
* zConfig.*.zolo - System configuration files
|
|
4
|
+
*
|
|
5
|
+
* Extends base 'zolo' language with file-type-specific patterns.
|
|
6
|
+
*
|
|
7
|
+
* Generated by zlsp/zlsp/generators/generate_prism_zolo.py
|
|
8
|
+
* DO NOT EDIT MANUALLY - regenerate with: python3 -m zlsp.generators.generate_prism_zolo
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
Prism.languages.zconfig = Prism.languages.extend('zolo', {
|
|
12
|
+
'zconfig-special-root': {
|
|
13
|
+
pattern: /((?:^|\n)[ \t]*)(?:zMachine|Z[A-Z0-9_]+)(?=\s*:)/m,
|
|
14
|
+
alias: 'function',
|
|
15
|
+
lookbehind: true,
|
|
16
|
+
},
|
|
17
|
+
'root-key': {
|
|
18
|
+
pattern: /(^|\n)([A-Z][a-zA-Z0-9_]*)(?=\s*(?:\([^)]+\))?[*!^~]?:)/m,
|
|
19
|
+
alias: 'class-name',
|
|
20
|
+
lookbehind: true,
|
|
21
|
+
},
|
|
22
|
+
'zmachine-locked-section': {
|
|
23
|
+
pattern: /(?<=\n)[ \t]{1}(?:storage|python_runtime|launch_commands|network|display|memory|machine_identity|user_paths|gpu|cpu)(?=\s*:)/m,
|
|
24
|
+
alias: 'constant',
|
|
25
|
+
lookbehind: true,
|
|
26
|
+
},
|
|
27
|
+
'zmachine-editable-section': {
|
|
28
|
+
pattern: /(?<=\n)[ \t]{1}(?:user_preferences|time_date_formatting|custom)(?=\s*:)/m,
|
|
29
|
+
alias: 'variable',
|
|
30
|
+
lookbehind: true,
|
|
31
|
+
},
|
|
32
|
+
'zconfig-property': {
|
|
33
|
+
pattern: /(?<=\n)[ \t]{2,}[a-zA-Z][a-zA-Z0-9_]*(?=\s*:)/m,
|
|
34
|
+
alias: 'variable',
|
|
35
|
+
lookbehind: true,
|
|
36
|
+
},
|
|
37
|
+
'property': null
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Support both ```zconfig and ```zConfig code fences
|
|
41
|
+
Prism.languages.zConfig = Prism.languages.zconfig;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism.js language definition for zenv
|
|
3
|
+
* zEnv.*.zolo - Environment configuration files
|
|
4
|
+
*
|
|
5
|
+
* Extends base 'zolo' language with file-type-specific patterns.
|
|
6
|
+
*
|
|
7
|
+
* Generated by zlsp/zlsp/generators/generate_prism_zolo.py
|
|
8
|
+
* DO NOT EDIT MANUALLY - regenerate with: python3 -m zlsp.generators.generate_prism_zolo
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
Prism.languages.zenv = Prism.languages.extend('zolo', {
|
|
12
|
+
'zenv-config-root': {
|
|
13
|
+
pattern: /((?:^|\n)[ \t]*)(?:LOG_LEVEL|DEBUG|DEPLOYMENT)(?=\s*:)/m,
|
|
14
|
+
alias: 'keyword',
|
|
15
|
+
lookbehind: true,
|
|
16
|
+
},
|
|
17
|
+
'zenv-z-uppercase-root': {
|
|
18
|
+
pattern: /((?:^|\n)[ \t]*)Z[A-Z0-9_]+(?=\s*:)/m,
|
|
19
|
+
alias: 'function',
|
|
20
|
+
lookbehind: true,
|
|
21
|
+
},
|
|
22
|
+
'root-key': {
|
|
23
|
+
pattern: /(^|\n)([A-Z][a-zA-Z0-9_]*)(?=\s*(?:\([^)]+\))?[*!^~]?:)/m,
|
|
24
|
+
alias: 'class-name',
|
|
25
|
+
lookbehind: true,
|
|
26
|
+
},
|
|
27
|
+
'znavbar-nested': {
|
|
28
|
+
pattern: /(?<=\n)[ \t]{1}[a-zA-Z][a-zA-Z0-9_]*(?=\s*:)/m,
|
|
29
|
+
alias: 'type',
|
|
30
|
+
lookbehind: true,
|
|
31
|
+
},
|
|
32
|
+
'zsub-key': {
|
|
33
|
+
pattern: /(?<=\n)[ \t]{2,}zSub(?=\s*:)/m,
|
|
34
|
+
alias: 'keyword',
|
|
35
|
+
lookbehind: true,
|
|
36
|
+
},
|
|
37
|
+
'zrbac-key': {
|
|
38
|
+
pattern: /\bzRBAC(?=\s*:)/,
|
|
39
|
+
alias: 'constant',
|
|
40
|
+
},
|
|
41
|
+
'property': {
|
|
42
|
+
pattern: /\b[a-zA-Z][a-zA-Z0-9_]*(?=\s*(?:\([^)]+\))?[*!]?:)/,
|
|
43
|
+
},
|
|
44
|
+
'prefix-modifier': {
|
|
45
|
+
pattern: /[\^~](?=[a-zA-Z][a-zA-Z0-9_]*\s*:)/,
|
|
46
|
+
alias: 'variable',
|
|
47
|
+
},
|
|
48
|
+
'suffix-modifier': {
|
|
49
|
+
pattern: /[*!](?=:)/,
|
|
50
|
+
alias: 'operator',
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Insert value patterns BEFORE string-unquoted for priority
|
|
55
|
+
Prism.languages.insertBefore('zenv', 'string-unquoted', {
|
|
56
|
+
'zpath-value': {
|
|
57
|
+
pattern: /[@~]\.[a-zA-Z0-9_./]+/,
|
|
58
|
+
alias: 'string',
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
Prism.languages.insertBefore('zenv', 'string-unquoted', {
|
|
62
|
+
'env-constant-value': {
|
|
63
|
+
pattern: /\b(?:DEBUG|PROD|INFO|WARNING|ERROR|CRITICAL|SESSION|Development|Production)\b/,
|
|
64
|
+
alias: 'number',
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Support both ```zenv and ```zEnv code fences
|
|
69
|
+
Prism.languages.zEnv = Prism.languages.zenv;
|