API Reference
Full signatures for the @todayai-labs/webview-bridge client.
createWebViewBridge
function createWebViewBridge(options?: WebViewBridgeOptions): WebViewBridgeResolves the active host channel
(window.webkit.messageHandlers.todayWebViewBridge on iOS/macOS or
window.todayWebViewBridge on Android/Linux/Windows) 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 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' | 'linux' | '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
host bridge (the same condition as isAvailable() === false). See the
Environment detection contract for how it is
resolved and what the host must inject to disambiguate shared transports.
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.
emit
emit(type: string, payload?: Record<string, unknown>): voidPosts a custom app-specific event through the same native channel.
Fire-and-forget: returns void, never throws, and must not be awaited.
bridge.emit('todayPage.firstBatchRendered', {
schemaVersion: 1,
requestId: 'refresh-...',
batchId: 'batch-a',
})The SDK sends one flat wire object:
{ ...payload, type }If payload also contains a type field, the first argument wins.
Use emit for additive, app-specific events whose result is not needed by the
web page. The host should resolve undefined after accepting the event. See the
Today Page V2 contract for the native Today tab
lifecycle events built on this helper.
onNativeEvent
onNativeEvent(handler: (event: NativeEventMessage) => void): () => voidSubscribes to native → web lifecycle events delivered through
todayWebViewBridge:nativeEvent. Returns an unsubscribe function.
const unsubscribe = bridge.onNativeEvent((event) => {
if (event.type === 'webView.visibilityChanged') {
// event.visible is the native-visible state
}
})The SDK only validates that event.type is a non-empty string. Unknown event
types are ignored by consumers, not rejected by the transport.
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.