Message contract between room.html running inside a native embedding host (WebView2 on Windows, WKWebView on macOS, CEF, or an equivalent) and the host process. The host owns consent and OS-level input injection; the page owns WebRTC and signaling. When the page detects a host with the remote-control-target capability, it never grants control on its own — every allow/deny decision comes from the host, and every data-channel event is forwarded back to the host for the host's own event injection.
Deployed as:/js/room-native-bridge.js, loaded from room.html before conference.js. Detection: window.chrome.webview is present AND a hello arrives naming a supported capability set.
Overview
Every message crossing the bridge is a JSON object with two fixed top-level fields:
bridge: the string "codeb-native-v1". Any message missing this label or naming a different version is ignored by both sides.
type: the message kind. Kinds are enumerated in this document.
Transport:
Page → host:window.chrome.webview.postMessage(obj) on Windows WebView2. On other hosts the analogous native APIs are:
webkit.messageHandlers.codebNative.postMessage(obj) for WKWebView,
window.cefQuery({request: JSON.stringify(obj)}) for CEF.
The bridge script exposes CodebNativeBridge.postToHost(obj) that picks the right one at runtime.
Host → page:WebView2.CoreWebView2.PostWebMessageAsJson(jsonString) on Windows; equivalent on other hosts. The bridge script listens on window.chrome.webview.message and delivers to registered handlers via CodebNativeBridge.on(type, fn).
Threading: all handlers run on the page's main thread; the host must serialise its own dispatch.
Detection
On page load, room-native-bridge.js checks for window.chrome?.webview (or the platform equivalent) and, if present, listens for the host's hello message. The host is expected to send this before the page joins the room, ideally within 200 ms of DOM ready. When the hello arrives, the bridge fires a codeb-native-ready event on window with the negotiated capability set, and conference.js checks for it before wiring the in-page consent dialog.
If no hello arrives within 1000 ms, the page fires a codeb-native-absent window event and assumes a normal browser (in-page fallback, no host forwarding). Late arrivals after that deadline are rejected — a hello that lands after the page has committed to the plain-browser path would leave the two sides out of sync and is treated as an attempted downgrade.
Transport isolation. Exactly one inbound transport is installed per host, chosen by feature detection: WebView2 → window.chrome.webview.addEventListener('message'); WKWebView → the global __codebNativeInbound(json) (installed by the bridge as non-writable, non-configurable); CEF and Electron → the host installs its own inbound function via CodebNativeBridge.setInboundTransport(). There is no window.postMessage fallback: a page script cannot impersonate the host by posting a message to itself.
P→HPage → host messages
Sent by the page whenever a signaling event that the host cares about occurs. The host does not need to acknowledge; the page does not wait for a reply.
Sent once, right after welcome is received. Lets the host bind its UI to a specific peer and tenant.
R20 (2026-09-29, Wishlist W11):pageVersion is this build's tag (matches join.client without any host prefix). Not intended for compatibility gating — there is only ever one current version of the page — only so the host can display it in About / support info.
R20 (2026-09-29, Wishlist W2):peers is a snapshot of the room roster at welcome time, with the same shape as the per-peer peer-joined event fired for later joiners; self is not included. Follow-up peer-joined / peer-left messages keep the host's list in sync.
control-request
{ "bridge":"codeb-native-v1", "type":"control-request",
"requestId":"req-1a2b3c", "from":"<peerId>", "name":"Alice Walker",
"verified":true, "verifiedIdentity":"alice@example.com",
"attest":"eyJhbGci...", "attestExpiresUtc":"2026-09-28T15:24:00Z",
"purpose":"You asked me for help with the printer setup",
"requestedFeatures":["pointer","keyboard","wheel"],
"accessCode":"k7m2x9" }
The page forwards the WS control-request unchanged (server-stamped fields are preserved so the host can verify the attestation locally). The page also keeps a private requestId → pending map with a 60 s TTL; a later control-decision is routed by requestId, never by "whoever asked last".
accessCode (R17+). Optional TeamViewer-style one-touch code the helper's user typed into the tile form. Up to 32 chars from [A-Za-z0-9 -]. The page forwards the field to the host only when the host advertised the access-code capability in its hello caps; older hosts never see it. When the host recognises the code (its own salted-hash compare) it may skip the consent dialog and answer with control-decision {allow:true} directly. Never logged, never persisted, never in the URL.
The page refuses a NEW control-request while a session is already active. It does not surface such a request to the host; it answers the helper directly with a WS control-revoke (reason: "policy").
Sent immediately after the page has sent its WS control-grant in response to a host control-decision. The host needs the sessionId for later control-revoke and screen-info messages (both are ignored when the id does not match). Nothing else tells the host what session id the page picked, since hb events are not forwarded.
Every data-channel event of the active session is mirrored to the host. from comes from the RTCPeerConnection the event arrived on — never from the payload — so the host cannot be tricked by a spoofed event.from.
Sent when the helper hits Cancel on its own outstanding request before the target has responded. The host closes its consent dialog immediately; there is no control-decision to send. Fires only when the pending request was already known to the page (matched a live entry in the target-side pending map).
Sent after the page sees a control-revoke (WS or DC end), a data-channel close, or a PeerConnection close. The page has already released its local UI state; the host must do the same.
Fired when getDisplayMedia resolves or the underlying track ends. Lets the host draw the "you are sharing your screen" banner and stop-button. surface is the real value from the shared MediaStreamTrack.getSettings().displaySurface (monitor, window, or browser). width / height are the track's physical pixel dimensions so a host on a multi-monitor setup can identify which monitor is being shared.
R20 (2026-09-29):share-info is emitted with the same shape as share-started whenever the shared track's dimensions or surface change (e.g. the user resizes a shared window, drags a browser tab share to a bigger monitor). Fires from the track's resize event and, as a fallback, from a 3-second poll while the share is active. Consumers should treat share-info as an update to the most-recent share-started; there is one share-started per session and any number of share-info updates until share-stopped.
R20 (2026-09-29, Wishlist W2): Fired for every peer whose peer-joined / peer-left arrives on the WS, so the host can maintain its own participants list without waiting for a control-request to arrive. client mirrors what the peer sent as join.client, so RDPLauncher can distinguish another AloahaRDPSystemTray from a browser. reason on peer-left is "left" by default (the server does not carry a richer reason today); a future change may propagate moderator-kick / duplicate-connection / server-shutdown.
The initial roster is also delivered inside the ready message as a peers array of the same {peerId, name, client} shape, so a host that starts the WebView after a peer is already in the room sees them immediately.
If the departing peer was the target of an active control session, the page also sends control-ended with reason helper-disconnected or target-disconnected as appropriate.
R20 (2026-09-29, Wishlist W3): Sent on every room-lock state change (either because the host itself called lock-room, or because another peer clicked the page's Lock button and the server broadcast the change). Also sent once, right after ready, with the current state so the host UI can render Lock / Unlock in the correct position from the first frame. by is the display name of the peer that flipped the lock (empty for the initial state broadcast).
Fired when the page's Stop-share button is pressed (mirrors the host's own Ctrl+Alt+F12). Lets the host clear its own state without racing the page.
H→PHost → page messages
Sent by the host to drive the page. The page validates every message: unknown bridge, missing required fields, or a message that arrives before hello is ignored.
First message. caps currently recognised: remote-control-target (the host owns consent and injection), share-indicator (the host draws the sharing banner), screen-info (the host provides multi-monitor metadata), cursor-shape (R15+, the host emits cursor-shape H→P messages), access-code (R17+, the host accepts accessCode inside a bridge control-request and grants directly on a match).
R17 edition field. Self-declared edition of the native host. Two whitelisted values: "free" (default) and "commercial". Unknown, missing, or malformed values normalise to "free", so older RDPLauncher builds keep working. The page exposes it verbatim via CodebNativeBridge.hostCaps().edition and copies it into the WS join as a separate hostEdition field so the server can propagate it in welcome.peers[] / peer-joined.peer for display badges and telemetry.
Not a licence. The value is set by the program on the user's own computer, so anyone can claim "commercial". Do not gate paid features on it. A future spec will add a separately signed licenseToken that the server verifies with Aloaha's public key before trusting the edition; until then, edition is display-only.
Consent verdict. On allow:true the page sends the WS control-grant (with the fields above) and opens the codeb-control-v1 RTCDataChannel. On allow:false the page sends a WS control-revoke with reason user-stopped and the request ends.
Host asks the page to end the session. The page sends a WS control-revoke, sends end on the DC, and closes the DC. The host is expected to have already released any pressed keys and mouse buttons.
Host tells the page the current shape of the mouse pointer on the target's desktop, so the helper's own pointer over the shared video takes the same shape. Sent while a control session is active; only on change (repeats are suppressed by the host).
kind is one of the standard names default, text, pointer, wait, progress, crosshair, move, not-allowed, help, ew-resize, ns-resize, nwse-resize, nesw-resize, none — or the literal "custom" for a program-specific cursor. none hides the cursor (useful when the target hid its own).
For custom: the png field (base64 with padding, transparency preserved) is sent only the FIRST time an id appears in a control session; repeats carry just the id. hx/hy is the hot spot; w/h is at most 128 (the browser limit for CSS cursors). fallback is a standard kind the browser uses if the PNG cannot be loaded. Ids reset at every new session.
Requires the cursor-shape capability in the hello caps. Page-side validation: id matches ^c\d{1,6}$, 1 ≤ w,h ≤ 128, 0 ≤ hx < w, 0 ≤ hy < h, png is base64 ≤ 64 KiB. Anything else is dropped without touching the DC or the video's cursor: style.
Host announces the current shared surface and monitor topology. The page forwards this as a screen-info event on the codeb-control-v1 DC. Sent immediately after DC open and again whenever the surface changes.
Host asks the page to stop sharing its screen (Ctrl+Alt+F12 or a Stop button in the host UI). The page ends the local MediaStreamTrack. If a control session was active, it also revokes.
R20 (2026-09-29, Wishlist W3): Host asks the page to flip the room-wide lock. Same effect as clicking the page's own Lock button: sends a WS lock or unlock frame; the server broadcasts room-locked / room-unlocked back to every peer in the room (including us), and the page mirrors the new state onto its Lock button UI and re-posts room-locked to the host. A host that flips the lock therefore also gets the acknowledgement on the P→H side; if the server denies the flip or the state is already what the host asked for, the P→H room-locked still carries the current state so the two sides re-converge.
Use. RDPLauncher's tray offers a Lock the room now button in a help session so anyone who knows the room code cannot join and passively watch after the helper has already taken control. A future update may lock automatically once the first control session starts.
JSPage JS API for the host
Some host actions are not a good fit for the P↔H message bridge because the browser demands them to originate from a user gesture on the page's own event loop (notably getDisplayMedia). For those, the page exposes a stable JavaScript surface as window.CodebHost that the host invokes with WebView2's ExecuteScriptAsync (or the WKWebView / CEF equivalent) from inside a synthesized user gesture. Non-writable + non-configurable, same install discipline as window.CodebControl.
R20 (2026-09-29, Wishlist W1): Programmatic entry into the share pipeline. opts.surface is passed to getDisplayMedia as the displaySurface hint (monitor, window, or browser); the host almost always wants monitor because remote control needs a whole-screen share. The rest of the pipeline is identical to a real user click on #btn-share: same replaceTrack, same share-started P→H event, same auto-spotlight on the local tile, same auto-revoke on track.ended. Removes the need for RDPLauncher's DevTools-protocol click workaround (see Wishlist W4 NoCameraShareFixScript is unrelated; that path is covered by the R9 share-with-cam-off fix).
Same effect as the H→P stop-share message but callable in-process. Prefer the bridge message for cross-process usage; use this from an ExecuteScriptAsync when it's simpler.
CodebHost.isSharing()
window.CodebHost.isSharing(); // -> true | false
Reads the same flag the page uses internally so the host UI can render Start / Stop from a synchronous call without waiting for a share-started round-trip.
Security
Origin gating. The bridge script accepts host messages only when the page is loaded from a permitted origin (https://phone.codeb.io, https://phone.aloaha.com, or a host explicitly configured in the client bundle). Any other origin ignores every message.
No decision without hello.control-decision, control-revoke, screen-info, and stop-share are all ignored until a valid hello has been received in the same document.
Attestation is passed through unchanged. The page never edits the attest JWS or the identity fields before handing them to the host. The host verifies locally.
Bridge scope. Only the top document of the meeting page is a bridge peer. Frames and workers are not addressed.
No page-side grant. When a host hello with remote-control-target was received, the in-page consent dialog is disabled. The page never sends a WS control-grant without a preceding host control-decision.
Reference transcript
Windows tray host, dev tenant, single control session, target's perspective.
// Host launches WebView2, navigates to https://phone.codeb.io/room.html?room=hostinghelp&cam=off
// DOMContentLoaded fires; room-native-bridge.js runs
H → P { "bridge":"codeb-native-v1", "type":"hello",
"client":"AloahaRDPSystemTray/5.1.0.0",
"caps":["remote-control-target","share-indicator","screen-info"] }
// bridge fires window CustomEvent("codeb-native-ready"). conference.js
// suppresses its in-page consent dialog and joins the room.
P → H { "bridge":"codeb-native-v1", "type":"ready",
"room":"hostinghelp", "peerId":"dd44ee55ff66",
"protoVersion":"2", "tenant":"phone.codeb.io" }
// user opens getDisplayMedia (host's Share button, or the room UI):
P → H { "bridge":"codeb-native-v1", "type":"share-started",
"surface":"monitor", "width":2560, "height":1440 }
// helper Alice sends control-request on the WS. Page relays to host:
P → H { "bridge":"codeb-native-v1", "type":"control-request",
"requestId":"req-1a2b3c", "from":"aa11bb22cc33", "name":"Alice Walker",
"verified":true, "verifiedIdentity":"alice@example.com",
"attest":"eyJhbGci...", "attestExpiresUtc":"2026-09-28T15:24:00Z",
"purpose":"Printer setup", "requestedFeatures":["pointer","keyboard","wheel"] }
// host verifies attest, shows consent dialog, user clicks Allow:
H → P { "bridge":"codeb-native-v1", "type":"control-decision",
"requestId":"req-1a2b3c", "allow":true,
"grantedFeatures":["pointer","keyboard","wheel"],
"expiresUtc":"2026-09-28T16:20:00Z", "hbIntervalMs":5000 }
// page sends WS control-grant, opens codeb-control-v1 DC.
// host immediately announces multi-monitor topology:
H → P { "bridge":"codeb-native-v1", "type":"screen-info", "sessionId":"s7v8h1kw2q3r4p5m",
"surface":"monitor", "surfaceRect":{"x":0,"y":0,"w":2560,"h":1440},
"virtual":{"x":-1920,"y":0,"w":4480,"h":1440},
"monitors":[{"id":"\\\\.\\DISPLAY1","x":0,"y":0,"w":2560,"h":1440,"scale":1.25,"primary":true}] }
// page forwards as DC screen-info event. Alice starts sending pointer events:
P → H { "bridge":"codeb-native-v1", "type":"control-event",
"sessionId":"s7v8h1kw2q3r4p5m", "from":"aa11bb22cc33",
"event":{"s":"s7v8h1kw2q3r4p5m","seq":1,"t":"pointer","nx":0.4531,"ny":0.1093} }
// (host maps to desktop coords using screen-info and SendInputs...)
// user clicks Stop-share (Ctrl+Alt+F12):
H → P { "bridge":"codeb-native-v1", "type":"control-revoke",
"sessionId":"s7v8h1kw2q3r4p5m", "reason":"emergency" }
H → P { "bridge":"codeb-native-v1", "type":"stop-share" }
// page sends WS control-revoke, DC end, closes DC, ends the local share track:
P → H { "bridge":"codeb-native-v1", "type":"control-ended",
"sessionId":"s7v8h1kw2q3r4p5m", "reason":"emergency" }
P → H { "bridge":"codeb-native-v1", "type":"share-stopped" }
Helper JS API. For hosts that also act as the helper (drive another participant's screen), room.html exposes window.CodebControl with a full send API: sendPointer, sendButton, sendWheel, sendKey, sendClipRead, sendClipWrite. See the Helper JS API section of /remote_control_api.html for the full surface.