@qontinui/ui-bridge-native 0.8.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -2
- package/dist/core/index.js +13 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +13 -3
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/sha256.d.ts +29 -7
- package/dist/core/sha256.d.ts.map +1 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +996 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +986 -34
- package/dist/index.mjs.map +1 -1
- package/dist/observation/observation.d.ts +15 -1
- package/dist/observation/observation.d.ts.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +25 -3
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +967 -32
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +968 -33
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts +120 -2
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +934 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +916 -17
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/pairing.d.ts +295 -0
- package/dist/server/pairing.d.ts.map +1 -0
- package/dist/server/types.d.ts +20 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/transport/DeviceAnnouncer.d.ts +0 -2
- package/dist/transport/DeviceAnnouncer.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,8 +55,8 @@ function SubmitButton({ onPress }) {
|
|
|
55
55
|
```python
|
|
56
56
|
from ui_bridge import UIBridgeClient
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
client = UIBridgeClient("http://
|
|
58
|
+
# Over a cable or an emulator, after `adb forward tcp:8087 tcp:8087`. No token needed.
|
|
59
|
+
client = UIBridgeClient("http://localhost:8087")
|
|
60
60
|
|
|
61
61
|
# Find and interact with elements
|
|
62
62
|
client.press("submit-button")
|
|
@@ -225,6 +225,8 @@ table:
|
|
|
225
225
|
| GET | `/ui-bridge/_help` | The same list plus per-handler descriptions, actions and tips |
|
|
226
226
|
|
|
227
227
|
```bash
|
|
228
|
+
# Over `adb forward`, which arrives as loopback. On a device IP this route answers
|
|
229
|
+
# 401 PAIRING_REQUIRED; see "Device Pairing".
|
|
228
230
|
curl -s http://localhost:8087/ui-bridge/_routes | jq '.data.routes'
|
|
229
231
|
```
|
|
230
232
|
|
|
@@ -244,6 +246,50 @@ A wrong verb on a path the bridge does publish answers **405** with an `Allow`
|
|
|
244
246
|
header naming the verbs it accepts (and the same verbs in the JSON envelope's
|
|
245
247
|
`error`); a path it does not publish answers **404**.
|
|
246
248
|
|
|
249
|
+
### Device Pairing
|
|
250
|
+
|
|
251
|
+
The server admits callers as follows:
|
|
252
|
+
|
|
253
|
+
| Caller | Answer |
|
|
254
|
+
| --- | --- |
|
|
255
|
+
| Loopback peer with a loopback `Host` and nothing a web page sends (`Origin`, `Referer`, a `Sec-Fetch-Site` other than `none`) | Admitted, no token |
|
|
256
|
+
| Any other caller with a valid `X-UI-Bridge-Auth` signature | Admitted |
|
|
257
|
+
| Anyone, `GET /ui-bridge/health` (trimmed) | Answered |
|
|
258
|
+
| Anyone but a web page, `POST /ui-bridge/pair` | Answered |
|
|
259
|
+
| A web page, `POST /ui-bridge/pair` | `403 PAIRING_WEB_CALLER_REFUSED` |
|
|
260
|
+
| Signature outside the 300 s window, or older than this server will accept | `401 PAIRING_TIMESTAMP_REFUSED` with `data.serverTime`: re-sync and sign again, no new pairing needed |
|
|
261
|
+
| Signed body over `SIGNED_BODY_MAX_CHARS` (262,144 characters) | `413 PAIRING_BODY_TOO_LARGE` |
|
|
262
|
+
| Signed method over 16 characters, or target over `SIGNED_TARGET_MAX_CHARS` (8,192) | `414 PAIRING_TARGET_TOO_LONG` |
|
|
263
|
+
| Everyone else | `401 PAIRING_REQUIRED` |
|
|
264
|
+
| Not loopback, on a server with no `pairingStore` | `401 PAIRING_UNCONFIGURED` |
|
|
265
|
+
| WebSocket upgrade from anywhere but a trusted loopback caller, signed or not | `403 PAIRING_WS_LOOPBACK_ONLY` |
|
|
266
|
+
|
|
267
|
+
`adb forward` was measured arriving as `::ffff:127.0.0.1` on an Android 11 emulator on 2026-10-31, which counts as loopback. Physical phones are unmeasured.
|
|
268
|
+
|
|
269
|
+
A `curl` over a cable keeps working with nothing new. A `curl` to a device IP can reach only health, because a hand `curl` cannot sign.
|
|
270
|
+
|
|
271
|
+
To accept LAN callers, pass a `pairingStore`. The app supplies persistence and secure randomness, since the SDK's only generator is `Math.random`:
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
const pairingStore: PairingStore = {
|
|
275
|
+
get: () => readToken(), // throw until loaded; return null only when there is no token
|
|
276
|
+
set: (token) => writeToken(token), // pairing answers only after this settles
|
|
277
|
+
clear: () => deleteToken(),
|
|
278
|
+
randomBytes: (n) => Crypto.getRandomBytes(n),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
<UIBridgeNativeProvider
|
|
282
|
+
serverAdapter={adapter}
|
|
283
|
+
deviceId={deviceId}
|
|
284
|
+
pairingStore={pairingStore}
|
|
285
|
+
// ...
|
|
286
|
+
/>;
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`useUIBridgeNative()` gives Settings `armPairingCode()`, `disarmPairingCode()`, `getPairingState()` and `revokePairing()`. A code lasts about 2 minutes, works once, and is disarmed by 5 wrong attempts from anyone. `getPairingState().consumedBy` names the peer that used it. A correct code is not used up when the store fails, so the user can try it again. A revoke replaces the token. No signature verifies while it runs, and one checked against the old token is not served after it.
|
|
290
|
+
|
|
291
|
+
A sample on a trusted network can opt out with `allowUnauthenticatedNetwork`. Anything that can reach the port can then drive the app over HTTP, with two exceptions. A web page is refused with `403 PAIRING_WEB_CALLER_REFUSED`: anything carrying `Origin`, `Referer` or a `Sec-Fetch-Site` other than `none`, and any `Host` other than an IP address, `localhost` or a `.local` name, which is how DNS rebinding shows up. A WebSocket upgrade stays loopback-only.
|
|
292
|
+
|
|
247
293
|
### Custom Server Adapter
|
|
248
294
|
|
|
249
295
|
The HTTP server requires a platform-specific adapter. See [documentation](./docs/server-adapters.md) for examples with:
|
|
@@ -251,6 +297,8 @@ The HTTP server requires a platform-specific adapter. See [documentation](./docs
|
|
|
251
297
|
- `react-native-http-bridge`
|
|
252
298
|
- `@aspect/react-native-http-server`
|
|
253
299
|
|
|
300
|
+
An adapter must also pass what the gate reads. Fill each `HTTPRequest` with `remoteAddress` from the accepted socket, never from a header, and with `rawTarget` and `rawBody` exactly as received, because a signature covers those bytes. Without `remoteAddress` a caller is treated as not loopback. A WebSocket adapter must call `onWebSocketUpgrade(req)` before sending the 101 and answer `decision.response` when `decision.admit` is false. Messages are refused on an adapter that never calls `onWebSocketUpgrade`.
|
|
301
|
+
|
|
254
302
|
## Debug Inspector
|
|
255
303
|
|
|
256
304
|
Built-in visual inspector for development:
|
package/dist/core/index.js
CHANGED
|
@@ -572,8 +572,7 @@ function utf8Bytes(input) {
|
|
|
572
572
|
return Uint8Array.from(out);
|
|
573
573
|
}
|
|
574
574
|
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
575
|
-
function
|
|
576
|
-
const bytes = utf8Bytes(input);
|
|
575
|
+
function sha256Bytes(bytes) {
|
|
577
576
|
const blockBytes = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
578
577
|
const buffer = new Uint8Array(blockBytes);
|
|
579
578
|
buffer.set(bytes);
|
|
@@ -617,7 +616,18 @@ function sha256Hex(input) {
|
|
|
617
616
|
h[6] = h[6] + g | 0;
|
|
618
617
|
h[7] = h[7] + hh | 0;
|
|
619
618
|
}
|
|
620
|
-
|
|
619
|
+
const digest = new Uint8Array(32);
|
|
620
|
+
const digestView = new DataView(digest.buffer);
|
|
621
|
+
h.forEach((word, i) => digestView.setUint32(i * 4, word >>> 0));
|
|
622
|
+
return digest;
|
|
623
|
+
}
|
|
624
|
+
function bytesToHex(bytes) {
|
|
625
|
+
let out = "";
|
|
626
|
+
for (let i = 0; i < bytes.length; i++) out += bytes[i].toString(16).padStart(2, "0");
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
function sha256Hex(input) {
|
|
630
|
+
return bytesToHex(sha256Bytes(utf8Bytes(input)));
|
|
621
631
|
}
|
|
622
632
|
|
|
623
633
|
// src/core/vision-fields.ts
|