API Reference
Full signatures for the @today/webview-bridge client.
createWebViewBridge
function createWebViewBridge(options?: WebViewBridgeOptions): WebViewBridgeResolves the active native channel (window.webkit.messageHandlers.todayWebViewBridge
on iOS, window.todayWebViewBridge on Android) and returns a client bound to it.
If neither channel exists, the returned client is fully functional but inert —
see safe degradation.
interface WebViewBridgeOptions {
/**
* Override the global lookup name. Defaults to "todayWebViewBridge".
* Intended for staged channel migrations.
*/
channelName?: string
/**
* Inject a channel directly, bypassing the window lookup.
* Intended for tests and custom transports.
*/
channel?: NativeChannel
}isAvailable
isAvailable(): booleanReturns true only when a Today native host channel is present. Use it to gate
host-only features. It performs no I/O and is safe to call synchronously during
render.
isAvailable()andplatformare environment hints, not a security boundary — they read page-visible globals that page JS can spoof. Rely on them for rendering and feature-gating, never for a trust decision. See Not a security boundary.
platform
readonly platform: Platform // 'ios' | 'android' | 'macos' | 'windows' | 'web'The detected platform, resolved synchronously at createWebViewBridge()
time — not a method, not a Promise. Safe to read during render. 'web' means no
native host (the same condition as isAvailable() === false). See the
Environment detection contract for how it is
resolved and what the host must inject to split iOS from macOS.
if (bridge.platform === 'ios' || bridge.platform === 'macos') {
// Apple-specific layout
}track
track(event: string, properties?: TrackProperties): voidReports an analytics event. Fire-and-forget: returns void, never throws,
and must not be awaited. The host enriches the event with base fields and
forwards it to PostHog.
type TrackProperties = Record<string, string | number | boolean>properties accepts scalar values only. Nested array / object / null
values are dropped by the host. The public (event, properties) arguments map
onto the wire fields ename / parameters, which stay stable for the analytics
pipeline. See the track contract.
getHeaders
getHeaders(): Promise<Record<string, string>>Returns the headers the web must attach to its own fetch / XHR requests —
Authorization plus any server-driven extra headers. Never rejects: resolves
to {} when no host is present and on any transport fault. Callers (and the
embed boot path) rely on this not throwing.
const headers = await bridge.getHeaders()
// { Authorization: "Bearer …", "X-Today-Extra": "…", … }The result depends on the current page URL and login state. Outside a trusted feed domain, or when logged out,
Authorizationmay be absent. Never assume it is present. See theheaderscontract.
refreshToken
refreshToken(): Promise<RefreshTokenResult>Forces the host to mint a fresh access token. Call it when one of your own
requests returns 401. Concurrent calls are coalesced by the host into a single
real refresh, so it is safe to call from multiple in-flight requests at once.
interface RefreshTokenResult {
authorization: string // ready-to-use header value, e.g. "Bearer …"
}The result carries only authorization (a ready-to-use header value) — never the
raw token. The SDK normalises the host reply to exactly this field and ignores
any extras, keeping the secret surface minimal.
Rejects with one of three BridgeError types — distinguish them, because
only one means the user is signed out:
| Rejection | Cause | Force re-auth? |
|---|---|---|
BridgeUnavailableError | No native host | No — no host |
BridgeError | Host rejected (terminal auth failure) | Yes |
BridgeProtocolError | Host replied with a malformed / empty result | No — host bug |
A BridgeProtocolError is a protocol/serialization bug on the native side, not
a signed-out user — do not force re-authentication on it. Always wrap the call:
try {
const { authorization } = await bridge.refreshToken()
retryWith(authorization)
} catch (e) {
if (e instanceof BridgeProtocolError) {
// malformed reply — host bug, not a real auth failure; do not re-auth
}
// BridgeUnavailableError → no host; plain BridgeError → terminal auth failure
}See the refreshToken contract and
Error handling.
establishSession
establishSession(options?: { refresh?: boolean }): Promise<boolean>Asks the host to mint the same-origin embed_bearer session cookie itself.
The bearer is written into the WebView cookie store natively and is never
returned to JS — the call resolves a plain boolean, not a token. Pass
{ refresh: true } to force a fresh server token before the mint (the 401
path).
Never throws. Resolves false with no host, an unsupported (older) host, or
a failed mint — so the caller falls back to the header bootstrap:
// Preferred bootstrap: native mint, header exchange as the fallback.
if (!(await bridge.establishSession())) {
await bootstrapFromHeaders() // getHeaders() → POST /api/auth/embed-session
}
// After a 401: force a refresh, then re-mint.
if (!(await bridge.establishSession({ refresh: true }))) {
await refreshFromHeaders() // refreshToken() → POST /api/auth/embed-session
}This is the hardened path: because the cookie is HttpOnly, the bearer never
enters page JS at all. See the
establishSession contract and
the Security model.
fetch
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>A convenience wrapper around the platform fetch that:
- calls
getHeaders()and merges the result into the request headers (your explicitinit.headerswin over injected ones on this initial request); - issues the request;
- if the response is
401and the request is replayable, callsrefreshToken()and retries once with the refreshedAuthorization(which takes precedence over the caller header on the retry); - returns the final
Response.
A request is not replayable — and so the 401 is returned without a retry —
when input is a Request object, or when init.body is a ReadableStream.
Both are consumed by the first fetch and cannot be re-sent, so the SDK returns
the 401 rather than throwing an opaque "body already used" error. For a
retryable authenticated request, pass a URL string with a string / Blob /
ArrayBuffer body.
When no host is present it is just fetch — no headers, no retry. This is the
recommended entry point for authenticated requests; reach for the lower-level
methods only when you need to control merging or retry yourself.
const res = await bridge.fetch('/feed/api/cards', { method: 'GET' })Errors
All rejections are instances of BridgeError. The unavailable-host case is the
BridgeUnavailableError subclass; a malformed host reply is the
BridgeProtocolError subclass (a host bug, not a signed-out user). See
Error handling for the full taxonomy.