Overview
The transport-agnostic specification every platform implements against.
This section is the canonical specification of the Today WebView Bridge. The Web SDK is its reference consumer and the Native Reference is non-normative guidance — but this section is the source of truth. Where any document disagrees with this one, this one wins.
The bridge is one channel carrying request/response messages. A host implements the contract by exposing that channel and answering each capability as specified.
Conformance language
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, and MAY are used in the sense of RFC 2119. A host that satisfies every MUST in this section is a conforming host.
The model
┌─────────────────────────┐ postMessage(message) ┌─────────────────────────────┐
│ Web content │ ─────────────────────────────────> │ Native host │
│ (@today/webview-bridge)│ │ (iOS / macOS / Android / │
│ │ <───────────────────────────────── │ Windows) │
└─────────────────────────┘ Promise resolve / reject └────────────────────────────┘- The web sends a message: a JSON object with a required
typediscriminant. - The host returns a result (resolve) or an error (reject), delivered as a
settled
Promiseon the web side. - The channel is named
todayWebViewBridge.
What the contract defines
- Message envelope — the request shape,
typedispatch, the reply shape, and the transport requirements both platforms MUST meet. - Capabilities — the normative request and
result of
track,headers, andrefreshToken. - Environment detection — how
platformis resolved synchronously, and the tag the host injects to split iOS from macOS. - Error handling — when the host rejects, how errors are encoded on the wire, and how the web maps them.
- Versioning — how capabilities are added, how unknown messages behave, and the channel-name migration story.
Invariants
These hold across every capability and platform:
- Single channel. All messages travel over
todayWebViewBridge. There is no second handler. typeis required. Every message carries a stringtype. Dispatch is ontypealone.- Unknown
typeresolves toundefined. A host that does not recognise atypeMUST resolve (never reject) withundefined. This is what makes capability rollout additive — see Versioning. - Every reply is a settled Promise. Resolve on success, reject on failure. The web never polls.
Authorizationis never guaranteed. Any capability that could return it MAY omit it depending on URL trust domain and login state.
Security model
The preferred path keeps the bearer out of page JS entirely:
establishSession has the host
mint the same-origin embed_bearer cookie itself, at the device boundary, and
return only { ok: boolean }. The cookie is HttpOnly, so on a conforming host
no main-frame script — trusted or not — can read the bearer.
The fallback path, for hosts that don't yet implement establishSession, hands
the bearer to main-frame page JS: getHeaders returns Authorization so the
page can exchange it for the same cookie via /api/auth/embed-session. That path
assumes all main-frame script is trusted first-party — the TCK widget bundles
are first-party, served by the BFF, under a strict CSP.
The gates this contract defines — main-frame-only, HTTPS trusted surface, the
cross-origin sub-frame denial — keep the bearer away from untrusted frames on
both paths. On the fallback (getHeaders) path they do not defend against a
malicious or compromised main-frame script, or XSS in first-party code: such code
runs with the page's own authority and could call getHeaders / refreshToken
directly. establishSession closes that gap, because the bearer is never handed
to JS in the first place.
The hardening paths, then, are:
- Mint the cookie natively (preferred, implemented) —
establishSessionsets the same-origin auth cookie at the device boundary without ever exposing the bearer to JS. This is the default the embed boot now takes, falling back to thegetHeadersbootstrap only on older hosts. - Sandbox widgets in cross-origin iframes — the main-frame gate already denies the bearer to a cross-origin sub-frame, so an untrusted widget gets no token on either path.