Today WebView Bridge
Contract

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 type discriminant.
  • The host returns a result (resolve) or an error (reject), delivered as a settled Promise on the web side.
  • The channel is named todayWebViewBridge.

What the contract defines

  • Message envelope — the request shape, type dispatch, the reply shape, and the transport requirements both platforms MUST meet.
  • Capabilities — the normative request and result of track, headers, and refreshToken.
  • Environment detection — how platform is 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:

  1. Single channel. All messages travel over todayWebViewBridge. There is no second handler.
  2. type is required. Every message carries a string type. Dispatch is on type alone.
  3. Unknown type resolves to undefined. A host that does not recognise a type MUST resolve (never reject) with undefined. This is what makes capability rollout additive — see Versioning.
  4. Every reply is a settled Promise. Resolve on success, reject on failure. The web never polls.
  5. Authorization is 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)establishSession sets 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 the getHeaders bootstrap 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.

On this page