HypenHypen
Device Capabilities

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.

Each sensitive operation passes a gate that your app can't draw, click or hide:

CapabilityGate
gallery.pick, file.pickThe system picker. On the web, the host's dialog comes first, because the browser needs a fresh gesture.
file.saveThe host dialog, then the destination picker. No data is sent before a destination is chosen.
camera.captureThe host's capture UI: live preview, Capture/Record, Cancel. Nothing is captured before the user taps it.
mic.recordThe host dialog, then the recording indicator for the whole recording
bluetooth.selectThe host-owned chooser (the browser's chooser on the web)
bluetooth.scanThe host dialog, whose grant can be remembered for a limited time, then the indicator for the whole scan
permission.requestThe host dialog, then the OS prompt
permission.queryNone. 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 inputProtectionMs option.
  • 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 authenticated https:/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 for wss:// origins. On plaintext ws:// 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.record and bluetooth.scan only while their indicator can be shown. On iOS this is an overlay window. On Android it's the Compose overlay that HypenApp renders. 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.select chooser 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 Origin must 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 Origin is admitted only if your authenticate hook returns true. Native clients send app credentials, such as an Authorization header, which you set in RemoteEngineConfig on 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-deflate with 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 resumeToken with every sessionAck. 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.