Skip to main content
Complete reference for every public type, method, and property in the GBGBridge Android SDK. Artifact: com.gbg:gbgbridge-sdk:0.1.0-alpha01 (Maven Central) Packages: com.gbg.gbgbridge.core, com.gbg.gbgbridge.models, com.gbg.gbgbridge.capabilities, com.gbg.gbgbridge.webview Platforms: Android API 24+ (Android 7.0) Kotlin: 2.x

Table of contents


Primary entry points

These are the types you’ll interact with directly to wire the bridge into your app: a host that routes messages, and a configurator that prepares a WebView for bridge traffic.
Differs from iOS: there is no BridgeWebView composable or makeWebView factory on Android. Integration is a plain WebView(context) (or AndroidView { WebView(it) } in Compose) plus host.attach(webView) plus webView.loadUrl(journeyUrl). See Embedding the WebView for the full pattern.

BridgeHost

The central coordinator that manages message routing between a WebView and your native code.
Why it exists: BridgeHost is the main object you interact with. It decodes incoming messages from the WebView, routes requests to registered handlers, tracks pending requests, and sends responses and events back to the web journey. Threading: Main-thread-only. Every public state-mutating method asserts the main thread at runtime and throws IllegalStateException if called from a background thread; the @MainThread annotation also enforces this statically via lint. Inbound JavaScript messages arrive on the WebView render thread and are posted to the main looper before any handler or delegate runs.
Differs from iOS: iOS enforces main-thread isolation through @MainActor and the Swift concurrency system. Android relies on a documented main-thread contract enforced with runtime assertions and lint β€” the compiler will not stop you, but the SDK will fail loudly rather than corrupt state silently.

Constructors

BridgeHost offers two constructors. Most apps should use the hostVersion convenience constructor, which wires up typed capability slots automatically. Use the primary constructor when you need to declare capabilities explicitly or supply a dynamic provider.
BridgeHost(hostVersion:) β€” Recommended
Creates a new bridge host with typed capability slots and an empty static capability map. Capability support is declared by setting handlers on the documentCapture and selfieCapture slots. A CapabilityQueryHandler is registered automatically and builds its response dynamically from typed slots and custom capabilities, including permissionState metadata. Example:

BridgeHost(configuration:, capabilitiesProvider:) β€” Configuration-based
Creates a new bridge host with explicit configuration. Capabilities are declared statically in the BridgeConfiguration map, or dynamically via capabilitiesProvider. A built-in CapabilityQueryHandler is registered automatically for "capability.query" requests.
Differs from iOS: capabilitiesProvider is an Android-only addition. On iOS, dynamic capability state is expressed by mutating the published capabilities map; on Android the merged capabilities property is read-only, and dynamic state belongs in the provider. Also unlike iOS β€” where init(configuration:) does not wire typed slots into the query handler β€” both Android constructors merge configured typed slots into capability.query responses automatically.
Example:
Example β€” dynamic provider:
The static configuration.capabilities map is defensively snapshotted at host construction. Mutating a MutableMap you passed in does not change query responses β€” use capabilitiesProvider for dynamic state. If your provider lambda can throw, wrap direct reads of host.capabilities; a provider throw propagates to the caller on direct reads (the inbound capability.query path is protected separately by the SDK’s handler catch).

Companion constants

These constants describe the protocol identity and the host’s internal buffer limits. The buffer caps surface through public behaviour β€” eviction from receivedMessages, leak warnings in lastError, and the response dedupe window.

Properties

These properties expose the host’s configuration and observable state. The list-valued properties return immutable snapshots β€” each read copies the underlying buffer.
configuration
The configuration passed at construction. Read-only.
delegate
An optional delegate that receives notifications about inbound messages, outbound messages, unhandled requests, and errors. Set this to observe bridge activity without registering handlers. Read and assigned on the main thread.
Differs from iOS: the delegate is backed by a WeakReference β€” the host does not keep your delegate alive. The common pattern (the delegate is your Activity, Fragment, or ViewModel, which the platform holds strongly) is unaffected, but an inline assignment like host.delegate = SomeImpl() with no other strong reference will be garbage-collected, after which callbacks silently stop firing. Always hold your own strong reference to the delegate.

capabilities
A read-only merged snapshot of the host’s capability map, computed on each read. The merge combines, from lowest to highest precedence (later entries win on key collision):
  1. Runtime registerCustomCapability registrations.
  2. The static configuration map or the dynamic capabilitiesProvider β€” explicit configuration is authoritative over custom registrations.
  3. Typed slots (documentCapture, selfieCapture) with a non-null handler. An unused slot never shadows integrator-supplied capability info for the same ID.
Differs from iOS: iOS exposes a mutable @Published var capabilities: [String: Bool] that hosts write directly. The Android property is read-only and richer (BridgeCapabilityInfo values, not booleans); dynamic state belongs in capabilitiesProvider.

receivedMessages
An ordered snapshot of all messages received from the web journey (requests, responses, and events), capped at MAX_RECEIVED_MESSAGES (200) with oldest entries evicted first. Each read returns an immutable copy. Useful for debugging or building a message log UI. Read on the main thread.
pendingRequests
A snapshot of requests from the web journey that have no registered handler. These are waiting for a manual response via the lookup overload of respond. When a response is sent for a pending request, it is removed from this list. If the list grows beyond PENDING_REQUEST_LEAK_THRESHOLD (50), lastError is set with a leak warning. Read on the main thread.
lastError
The most recent error message, if any. Set when message decoding fails, outbound encoding fails, a handler throws before responding, an origin-gated message is rejected, or the pending-request buffer exceeds its leak threshold. Call clearError() to reset.
Differs from iOS: the setter is private β€” host apps cannot write lastError themselves; only the SDK records errors there. Also, sending while no WebView is attached does not set lastError on Android (see sendEvent), whereas iOS records "WebView not attached".

documentCapture
The typed slot for document capture (capability ID camera.document). Set its handler to declare support. Routes requests for the "camera.document.capture" action.
selfieCapture
The typed slot for selfie capture (capability ID camera.selfie). Set its handler to declare support. Routes requests for the "camera.selfie.capture" action.

Methods

All methods are main-thread-only and return Unit unless noted. After dispose(), the state-mutating methods throw IllegalStateException β€” see dispose() for the full post-dispose contract.
attach(webView:, client:)
Associates a WebView with this host. Internally calls BridgeWebViewConfigurator.configure(...) (enabling JavaScript and DOM storage, and installing the bootstrap-injecting WebViewClient), then installs the SDK’s JavaScript interface on the WebView under the name GBGBridge. If the host is already attached, it detaches first. Example:
attach() calls BridgeWebViewConfigurator.configure() for you β€” do not call configure() separately before attach(), as it would be re-run and clobbered. If you need a custom WebChromeClient, set it after attach().

detach()
Disconnects the host from its WebView. Cancels any in-flight typed-slot capture (so the web side receives a cancelled response), removes the JavaScript interface, and resets the per-attach-session state: the response dedupe set, pendingRequests, and receivedMessages are all cleared. Idempotent β€” safe to call when not attached.
Differs from iOS: iOS preserves pendingRequests and receivedMessages across detach. Android clears both, because a Compose host driving a list off these snapshots would otherwise show ghost entries from a previous attach-session after re-attaching to a different WebView.

dispose()
Terminal teardown: detaches from the WebView and cancels the typed-slot coroutine scopes so future capture launches refuse rather than hang. Idempotent on repeat calls. Call once from the owner’s terminal lifecycle hook (Activity.onDestroy, ViewModel.onCleared). After dispose():
  • attach, register, unregister, registerCustomCapability, both respond overloads, and sendEvent throw IllegalStateException.
  • detach(), dispose(), clearError(), all property getters (which return empty/null), and the delegate setter remain safe to call.
  • The host must not be re-attached β€” hosts that just want to swap WebViews should use detach()/attach(), which keep the slot scopes live.
Differs from iOS: there is no iOS equivalent β€” ARC handles terminal cleanup there. On Android, dispose explicitly cancels the coroutine scopes that pin handler closures (and through them, Activity context).
detach() failures propagate out of dispose(). The rare path is removeJavascriptInterface throwing on a WebView already in shutdown. Wrap the call when wiring it into onDestroy:

register(handler:)
Registers a capability handler. When a request arrives with an action matching the handler’s action property, the handler’s handle(request, responder) method is called on the main thread. If a handler is already registered for the same action, it is replaced β€” including the SDK’s own auto-registered handlers (the CapabilityQueryHandler for "capability.query", and the typed-slot handlers for the capture actions). Example:

unregister(action:)
Removes the handler registered for the given action, along with any custom capability registered under the same name. Subsequent requests for that action are added to pendingRequests instead.
"capability.query" is the action of the SDK’s auto-registered CapabilityQueryHandler. Unregistering it makes capability queries fall through to the unhandled-request path; re-register a replacement (CapabilityQueryHandler is public for exactly this purpose) if you want the action to keep working.

registerCustomCapability(action:, version:, handler:)
Registers a lightweight custom capability backed by a lambda. The capability automatically appears in capability.query responses with supported = true and the supplied version (defaulting to "1.0"). The lambda is invoked on the main thread when a matching request arrives. Exceptions thrown from the lambda are caught by the SDK and routed to delegate.onError, with a best-effort error response dispatched to the web side. If a typed slot with a handler uses the same capability ID, the typed slot’s info wins in query responses.
Differs from iOS: the handler lambda is synchronous, not async. For asynchronous work, retain the responder, complete the work, hop back to the main thread, then call responder.respond(...) β€” see BridgeResponder.
Example:

sendEvent(action:, data:)
Sends a one-way event message to the web journey. Events are fire-and-forget β€” no response is expected. Each event is stamped with an auto-generated correlation ID of the form android-event-{uuid} (iOS uses ios-event-{uuid}). Example:
Error cases: if outbound JSON encoding fails, lastError is set, delegate.onError fires, and the message is dropped.
Differs from iOS: if no WebView is attached, the message fires delegate.onMessageSent (recording intent for tracing) and is then silently dropped at transport β€” lastError is not set. iOS records lastError = "WebView not attached" in the same situation.

respond(to:, status:, data:, error:)
Sends a response to a pending request, looking up its action from pendingRequests. The matching pending request is removed before dispatch; the response’s action is automatically set to the original request’s action. If no pending request matches the correlation ID, the call silently no-ops. Example:
Retry asymmetry: the pending entry is consumed before sending. If the send then fails (encode error, or no WebView attached), a retry through this lookup overload silently no-ops because the entry is gone. To retry, use the explicit-action overload below β€” you can recover the original action from the request you captured via delegate.onUnhandledRequest.

respond(to:, action:, status:, data:, error:)
Sends a response with an explicit action, without requiring a matching pending request. Use this from contexts that have already consumed the request, or when retrying after a failed send. This overload deduplicates by correlation ID: a second call with the same ID silently no-ops (the dedupe window holds the most recent MAX_RESPONDED_TRACKED = 200 IDs and is reset on detach()). If the send fails β€” encode error or no WebView attached β€” the dedupe entry is rolled back so a retry with the same correlation ID works once the underlying problem is resolved.
clearError()
Resets lastError to null. Safe to call after dispose().

Request dispatch behaviour

When a request arrives with no registered handler, it is appended to pendingRequests and delegate.onUnhandledRequest fires; respond later via the lookup respond overload. When a registered handler throws, the SDK catches the exception and routes it to delegate.onError. If the handler had not yet responded, lastError is also set and an ERROR response with code HANDLER_FAILURE (recoverable = false) is dispatched to the web side; a handler that responded successfully and then threw is reported via onError only β€” the request itself still succeeded. See Messaging for the full message flow and Capability handling for handler patterns.

BridgeWebViewConfigurator

A utility object that configures a WebView with the bridge infrastructure. BridgeHost.attach() calls it for you β€” you only interact with it directly when subclassing its WebViewClient for custom navigation policy.
Why it exists: Separates WebView setup (JavaScript settings, bootstrap injection, client installation) from the host’s message routing.

configure(webView:, bootstrapScript:, client:)

Applies the SDK’s required WebView defaults and installs a WebViewClient that injects the bootstrap script on each page load:
  1. Sets javaScriptEnabled = true and domStorageEnabled = true.
  2. Installs a BootstrapInjectingWebViewClient (the supplied client, or a default built from bootstrapScript).
  3. Installs a plain WebChromeClient.
The default bootstrap script is:
configure() unconditionally overwrites any existing webViewClient and webChromeClient on the WebView (the API-26+ getters that would allow reading an existing client are unavailable at the SDK’s minSdk = 24 floor). Since BridgeHost.attach() calls configure() internally, set a custom WebChromeClient only after attach(), and supply custom WebViewClient behaviour by subclassing BootstrapInjectingWebViewClient and passing it to attach(webView, client = ...).

BootstrapInjectingWebViewClient

This nested class is the SDK’s WebViewClient. It is public open so security-conscious hosts can subclass it to layer their own navigation policy without losing the bootstrap injection that makes the bridge work.
The client injects the bootstrap via evaluateJavascript in onPageStarted (main frame only β€” sub-frames receive no bootstrap, matching iOS’s forMainFrameOnly: true). Override any callback you need β€” shouldOverrideUrlLoading allowlists, error logging, SSL handling β€” and call super.onPageStarted(...) to preserve the bootstrap. Example β€” restricting navigation to a single origin:
Differs from iOS: iOS injects the bootstrap with WKUserScript(.atDocumentStart), which is guaranteed to run before any page JavaScript. Android’s onPageStarted + evaluateJavascript is best-effort β€” a <script> in the document head that synchronously calls window.GBGBridge.receive could race the injection. Hosts that install their own client must also supply the bootstrap literal themselves (via the subclass constructor), since the default script constant is not public.

Capabilities

The capability types implement the native side of capability negotiation: typed slots that declare and route capture requests, a strongly-typed result hierarchy, and helpers for permission state.

CaptureCapability

A typed capability slot that represents a capture operation (document or selfie). Setting a handler declares support; the SDK handles routing, result encoding, and busy rejection automatically.
Why it exists: Eliminates the need to manually build BridgeCapabilityInfo maps, encode capture results into JsonElement, and keep capability declarations in sync with handler registrations. Setting a handler is the declaration. You typically don’t create CaptureCapability instances directly β€” use the built-in slots on BridgeHost (documentCapture, selfieCapture). All mutable state is main-thread-only, like the rest of the SDK.

Properties

Differs from iOS: the handler is a Kotlin suspend lambda rather than a Swift async closure, and activeRequest is a StateFlow rather than an @Published property β€” the observation idiom changes but the semantics match.

Request dispatch

When a request arrives for the slot’s actionId, the slot decides as follows:
  1. No handler set β†’ responds UNSUPPORTED (error code UNSUPPORTED).
  2. A request is already in flight β†’ responds ERROR with code BUSY (recoverable = true).
  3. Otherwise, activeRequest is set and the handler is launched on the main dispatcher.
  4. Handler throws β†’ HANDLER_FAILURE response (recoverable = false) plus delegate.onError. A CancellationException is translated to a cancelled response instead.
  5. Handler returns a CaptureResult β†’ the result is encoded onto the wire and activeRequest is cleared.

Methods

These methods drive capability handling at runtime β€” building query responses, awaiting capture completion, and signalling completion or cancellation from the UI layer.
buildCapabilityInfo()
Returns a BridgeCapabilityInfo reflecting the slot’s current state: supported mirrors isSupported, version is null when unsupported (else falls back to "1.0"), and permissionState carries the slot’s current wire token. Used internally to build capability.query responses.
awaitCompletion()
Suspends the current handler until complete(...) is called (or returns CaptureResult.Cancelled if cancelIfBusy runs first). Use this in handlers that present capture UI declaratively and complete from a callback. Example:

complete(result:)
Resumes the pending handler with a capture result. Call from your capture UI’s callback. Main-thread-only; silently no-ops when no handler is pending. Example:

cancelIfBusy(reason:)
Cancels the in-flight request, if any: resumes the pending handler with CaptureResult.Cancelled(reason) and clears activeRequest immediately. Main-thread-only; safe to call when idle. Call this from teardown paths (Activity destruction, dialog dismissal) and when the user backs out of your capture UI. See Capture screens for complete capture-UI patterns built on this slot.

CaptureResult

A strongly-typed result returned by capture handlers.
Why it exists: Eliminates manual JsonElement map construction. The SDK converts CaptureResult values to the bridge protocol format automatically.
Differs from iOS: iOS models this as an enum with separate DocumentCaptureResult / SelfieCaptureResult wrapper structs. Kotlin uses a sealed class whose subclasses carry the data directly β€” construct CaptureResult.Document(...) rather than .document(DocumentCaptureResult(...)). Image payloads are ByteArray instead of Data; the Document and Selfie data classes override equals/hashCode to compare byte-array content. The wire format is identical on both platforms.

CaptureResult.Document

A successful document capture, carrying the encoded image bytes. The SDK performs no compression or conversion β€” the bytes are emitted as supplied.
Bridge response data (status success):

CaptureResult.Selfie

A successful selfie capture, carrying preview image bytes and biometric blob bytes (encrypted and unencrypted, integrator-supplied).
Bridge response data (status success):

CaptureResult.Cancelled

The capture was cancelled by the user or system. Produces a cancelled response with error code CANCELLED and recoverable = true.

CaptureResult.Failed

The capture failed. Produces an error response carrying the supplied code, message, and recoverability flag in a BridgeErrorPayload.

PermissionState

The current authorisation status for a native capability. The wire representation β€” the string surfaced in capability.query responses β€” uses the same tokens as iOS, exposed via the wireValue property.

CameraDetector

A utility for detecting camera hardware availability and permission status.
Why it exists: Provides a simple way to populate permissionState on capture slots without writing PackageManager boilerplate.

check(context:)

Checks camera hardware availability using PackageManager.FEATURE_CAMERA_ANY and permission status using Context.checkSelfPermission(Manifest.permission.CAMERA). Returns: A CameraDetector.Result with hardware and permission info.

CameraDetector.Result

The nested result type carries the two facts a host needs to seed its capture slots.
Differs from iOS: the Android runtime permission API cannot reliably distinguish β€œnever requested” from β€œpermanently denied” without an Activity and integrator-tracked state, so check() reports only GRANTED vs NOT_DETERMINED. Integrators who have run their own permission flow should set the richer DENIED/RESTRICTED state on the slot directly. iOS reports the full range from AVAuthorizationStatus.
Example:

Configuration

The configuration types describe what the host declares to the web journey: its version, its capability map, and the optional WebView bootstrap and origin allowlist.

BridgeConfiguration

Declares the host application’s version, supported capabilities, bootstrap script, and an optional origin allowlist.
Why it exists: Provides a single configuration object that BridgeHost uses to initialise capability state, configure the WebView bootstrap, and (optionally) gate inbound messages by origin.
Differs from iOS: allowedOrigins is an Android-only addition, and capabilities has a default value (emptyMap()), so configuration-only hosts can construct BridgeConfiguration(hostVersion = "1.0.0") directly.

allowedOrigins

When non-null, inbound postMessage calls are checked against the normalised origin of the WebView’s main-frame URL. On rejection the message is dropped, lastError is set, and delegate.onError fires with a SecurityException. The constructor validates the list up front and throws IllegalArgumentException for:
  • An empty list (which would be a kill-switch with no opt-out β€” pass null to disable enforcement instead).
  • Any malformed entry: missing host, non-http(s) scheme, % in the host, or a raw internationalised domain label (use punycode A-labels).
Entries and the live URL are both normalised to scheme://host[:port] β€” http/https only, case-insensitive, default ports elided, single trailing host dot stripped β€” so "HTTPS://APP.example.com/", "https://app.example.com", and "https://app.example.com:443" are equivalent.
allowedOrigins is not a security boundary. Android’s addJavascriptInterface injects into every frame, but only the main-frame URL can be checked β€” a hostile sub-frame under an allowlisted page can still drive the bridge, and the URL read is subject to time-of-check races. Treat it as defence-in-depth against host misconfiguration, layered on top of navigation-level filtering via shouldOverrideUrlLoading. See Security.

BridgeCapabilityInfo

Metadata about a single capability.
Why it exists: Allows the host to express not just whether a capability is supported, but also its version and permission state. As a Kotlin data class with val properties, instances are immutable β€” derive variants with .copy(...).

Message models

Every value that crosses the bridge is wrapped in a BridgeMessage envelope, which carries a typed payload, a correlation ID, and a status. The types in this section describe that envelope and the shapes you’ll embed inside it. All of them live in com.gbg.gbgbridge.models and are @Serializable via kotlinx.serialization; the wire format is identical to iOS.

BridgeMessage

A structured envelope for all bridge communication.
Differs from iOS: there is no Identifiable conformance or computed id property. When rendering messages in a Compose LazyColumn, key items by correlationId (combined with timestamp if you expect to render multiple messages per correlation ID).

BridgePayload

The payload carried within a BridgeMessage.

BridgeErrorPayload

Structured error information included in error responses.
Why it exists: Provides machine-readable error codes alongside human-readable messages, plus a recoverable flag that tells the web journey whether it should retry or fail.

BridgeRequestOptions

Optional per-request options the web journey can attach to a request payload.

BridgeMessageType

The type of a bridge message.

BridgeResponseStatus

The status of a response message.

JsonElement

Arbitrary JSON values in payloads are represented with kotlinx.serialization.json.JsonElement β€” the standard kotlinx.serialization JSON tree type β€” wherever the iOS SDK uses its custom JSONValue enum.
Differs from iOS: there is no SDK-defined JSON type. JsonElement (with its subtypes JsonPrimitive, JsonObject, JsonArray, and JsonNull) comes from the kotlinx-serialization-json dependency, so you get the full ecosystem of builders and accessors for free.
Example β€” constructing a payload:
Example β€” reading request parameters:

Interfaces

Where the iOS SDK defines protocols, the Android SDK defines Kotlin interfaces with the same roles: a handler contract for capabilities, a responder for sending results, and a delegate for observing host activity.

BridgeCapabilityHandler

The interface for objects that handle specific bridge request actions.
Why it exists: Defines the contract for capability implementations. Each handler is responsible for a single action and receives a responder to send its result back to the web journey.

action

The action identifier this handler responds to (e.g., "camera.document.capture", "nfc.read"). Must be unique per BridgeHost β€” registering a handler with the same action replaces the previous one.

handle(request:, responder:)

Called on the main thread when a request arrives with a matching action. Perform your native operation and call responder.respond(...) when done.
Differs from iOS: handle is synchronous, not async. For asynchronous work (camera capture, network calls, pickers), retain the responder, kick off the work, and when it completes hop back to the main thread and call responder.respond(...). See BridgeResponder for the threading contract and hop patterns.
Example:

BridgeResponder

A callback interface used to send a response from a capability handler back to the web journey.
Why it exists: Decouples the handler from BridgeHost internals. Handlers don’t need to know about WebViews or JavaScript evaluation β€” they simply call respond(). Implementations are provided by the SDK; you never implement this interface yourself for the built-in dispatch path. Threading: Main-thread-only, like the rest of the SDK. Handlers that defer to an asynchronous worker (an executor, OkHttp callback, CameraX callback) must hop back to the main thread before responding:
  • Coroutines: withContext(Dispatchers.Main) { responder.respond(...) }
  • Handler: Handler(Looper.getMainLooper()).post { responder.respond(...) }
  • Activity: runOnUiThread { responder.respond(...) }
Differs from iOS: the iOS responder is safe to call from any thread (it dispatches to the main actor internally). The Android responder throws IllegalStateException when called off the main thread β€” the hop is your responsibility. Call respond exactly once per request; subsequent calls silently no-op.

BridgeHostDelegate

An observer interface for monitoring bridge activity. All methods have default no-op implementations, so you only override what you need.
Why it exists: Provides a way to observe all bridge messages and react to unhandled requests and errors without registering a handler. Useful for logging, analytics, or building custom request handling.
Differs from iOS: Kotlin uses distinct method names rather than Swift argument labels β€” onMessage corresponds to bridgeHost(_:didReceive:) and onUnhandledRequest to bridgeHost(_:unhandledRequest:). onMessageSent and onError are Android-only additions. Remember that BridgeHost.delegate is WeakReference-backed: keep your own strong reference to the delegate.

onMessage(host:, message:)

Called for every message received from the web journey, regardless of type. One-way journey events (e.g. journey.completed) are observed here β€” events do not go through the request dispatch path.

onMessageSent(host:, message:)

Called for every outbound envelope, before transport. This records intent, not delivery β€” it also fires for messages that are subsequently dropped because no WebView is attached, or that fail to encode. Android-only.

onUnhandledRequest(host:, request:)

Called when a request arrives with no registered handler. The request is also added to host.pendingRequests; respond later via the lookup respond overload.

onError(host:, error:)

The Throwable-level error channel, Android-only. Fires for inbound decode failures, handler exceptions, outbound encode failures, and origin-gate rejections (SecurityException). Pairs with the string-valued lastError property for hosts that want full exception objects.

Built-in handlers

The SDK auto-registers one handler on every host so capability negotiation works out of the box.

CapabilityQueryHandler

A built-in handler that responds to "capability.query" requests with the host’s declared capabilities.
Why it exists: Capability negotiation is fundamental to the bridge protocol. This handler is automatically registered by BridgeHost so the web journey can always discover available capabilities. Action: "capability.query"
Differs from iOS: there is a single constructor taking a rich BridgeCapabilityInfo provider β€” no separate β€œsimple” boolean-map initialiser. The class is public (iOS keeps its equivalent internal) so hosts that call unregister("capability.query") can register a replacement instance with their own provider.

Response format

When the web journey sends a capability.query request, the response data contains:
constraints is never emitted, on either platform.