Consent & Security
How the device decides — host-owned consent, activity indicators, cooldowns and one-prompt-at-a-time, connection admission with Origin allowlists and authenticators, and what the server may and may not trust
Consent & Security
Letting a server ask for someone's camera is a big deal. The protocol is built around one rule: the server asks, the device decides. The device host enforces every policy on this page. Nothing the server sends can establish an OS permission or a user grant.
Consent gates
Each sensitive operation passes a gate that your app can't draw, click or hide:
| Capability | Gate |
|---|---|
gallery.pick, file.pick | The system picker. On the web, the host's dialog comes first, because the browser needs a fresh gesture. |
file.save | The host dialog, then the destination picker. No data is sent before a destination is chosen. |
camera.capture | The host's capture UI: live preview, Capture/Record, Cancel. Nothing is captured before the user taps it. |
mic.record | The host dialog, then the recording indicator for the whole recording |
bluetooth.select | The host-owned chooser (the browser's chooser on the web) |
bluetooth.scan | The host dialog, whose grant can be remembered for a limited time, then the indicator for the whole scan |
permission.request | The host dialog, then the OS prompt |
permission.query | None. It never prompts. |
The host dialog is part of the device host, not your UI:
- It names the authenticated server origin and the requested operation, and offers Continue and Cancel.
- Its labels and explanations come from the host. A server can't supply text that impersonates it.
- It sits outside the patch tree, so your app can't restyle, cover or click it.
- It is independent of the element that triggered the action. The request may have come from a lifecycle handler, and the app's button may already be disabled or gone. Continue still works, and it doesn't dispatch an app action.
Protection against click-through
On the web, Continue ignores input that wasn't meant for it:
- It stays disabled for 500 ms after the dialog appears, and again
whenever the page regains visibility or focus. Change this with the
inputProtectionMsoption. - Only an activation that starts on the enabled button counts: a trusted pointer-down, or Enter or Space pressed on it.
So a fast-clicking user, or a hostile server that pops up a request in the middle of a game or a typing session, can't turn a stray click or keystroke into consent. Other app gestures never consume a pending request.
One prompt at a time, and cooldowns
- One prompt per device host. While a consent dialog or picker is open,
any other request that needs a prompt fails at once with
throttled(prompt-in-progress), even across connections. Requests aren't queued invisibly. - Refusals cool down. When the user refuses (for example, taps Cancel in
the host dialog), that capability can't prompt again for a while: 30
seconds by default on the web and Android hosts. Requests during the
cooldown fail with
throttled(cooldown). Cooldowns persist across reconnects and app restarts for authenticatedhttps:/wss:origins. - Grants are keyed by origin and capability. A remembered grant, like the
one for
bluetooth.scan, is stored per normalized server origin and per capability. A photo picker never grants anything else. Grants are persisted only forwss://origins. On plaintextws://connections they last only for that connection.
Indicators
mic.record and bluetooth.scan run under an always-visible indicator
that names the origin and has a Stop button:
- It's host UI outside the patch tree. The app can't hide or restyle it.
- Stop ends a recording normally: your handler gets a successful result
with what was captured. Stop on a scan settles it as
cancelled. - No indicator, no capability. Native hosts offer
mic.recordandbluetooth.scanonly while their indicator can be shown. On iOS this is an overlay window. On Android it's the Compose overlay thatHypenApprenders. When the indicator stops being visible (the app goes to the background, the overlay is removed), running streams stop. - The web host ends a recording when the page becomes hidden
(
stopRecordingWhenHidden). The web grants no background capture. - While the
bluetooth.selectchooser is open, the chooser itself is the visible indicator.
Connection admission
The server decides at the WebSocket upgrade who may connect at all, for UI and device traffic alike. The device plane is on by default, so set this up before you deploy. Admission works the same in every SDK:
- Browsers: with an allowlist configured, an upgrade that carries an
Originmust name an origin on it, or the server answers 403. This defends against cross-site WebSocket hijacking: another site's page can't open a socket to your server with the user's cookies. - Native apps: with an allowlist configured, an upgrade without an
Originis admitted only if yourauthenticatehook returns true. Native clients send app credentials, such as anAuthorizationheader, which you set inRemoteEngineConfigon iOS and Android. - Nothing configured: with neither an allowlist nor an authenticator, every upgrade is admitted, as on any UI-only server, and the server logs one startup warning. The device plane doesn't widen what such a peer can do: it only lets the server ask that peer's own device for input, behind that device's consent UI. It does mean any page can open a socket to your server, so configure admission in production.
- A configured authenticator runs for every upgrade, including browser
upgrades with an allowed
Origin.
Origin authenticates nothing on its own. Treat the allowlist as a browser
defense, and your authenticator as the real gate.
Other connection protections
- Each message is compressed on its own. Servers negotiate
permessage-deflatewith no context takeover in both directions, and clients refuse device traffic on a socket compressed with shared history. Compressing secrets together with attacker-influenced data can leak them through message sizes; compressing every message separately keeps them apart. - Resume credentials. The server sends a fresh random
resumeTokenwith everysessionAck. To resume a session that negotiated a device plane, a client must present the latest token. A session id alone doesn't let anyone take over a live session. - Replay firewall. Device work can't start from a replayed or broadcast dispatch (see Lifetimes).
- Request ids are not secrets. They correlate messages on one authenticated connection. A message on one connection can never settle a request on another.
What the server can trust
- Bytes arrive intact. Before your handler sees an upload, the server checks every item's size and SHA-256 against what the client declared, and checks the item count and content types against the capability's rules.
- Bytes aren't vouched for. A hash detects corruption and mismatch. It doesn't establish that the content is what it claims to be, or that the client is honest. Validate uploads before using them.
- Client reports aren't proof. A permission status, a device id or an advertised capability is information from the client, not proof that the user authorized anything in your app.
- Deletion is your job. When a permission is revoked, the device stops capturing and clears its own buffers. Deleting data your server already derived from it is up to your handlers.
Lifetimes & Navigation
How device work is owned by a module activation, what happens on navigation, deactivation and reconnect, and why the background lifetime is reserved
Limits & Known Limitations
Item counts, item and connection size budgets, message size, deadlines and flow-control defaults for device capabilities, plus what is not implemented or validated yet