@xorgate/react-native 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake.d.ts","sourceRoot":"","sources":["../src/wake.ts"],"names":[],"mappings":"AAEA,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,CAOhE"}
package/dist/wake.js ADDED
@@ -0,0 +1,38 @@
1
+ import { AppState } from "react-native";
2
+ /**
3
+ * The foreground edge. This is the wake source a phone actually needs: iOS
4
+ * suspends the app's sockets within seconds of it leaving the screen, so both
5
+ * the MQTT connection and the peer connection are usually dead by the time the
6
+ * user comes back — and the live plane would otherwise sit out a backoff of up
7
+ * to 30 s in front of them. A browser tab has the same problem in miniature;
8
+ * `visibilitychange` is the web's version of this listener.
9
+ *
10
+ * Only the `active` edge counts. "inactive" is the app switcher or a system
11
+ * sheet over the app, where nothing has been torn down yet.
12
+ */
13
+ export function appStateWake(onWake) {
14
+ const subscription = AppState.addEventListener("change", (state) => {
15
+ if (state === "active")
16
+ onWake();
17
+ });
18
+ return () => subscription.remove();
19
+ }
20
+ /**
21
+ * Fan one `onWake` out over several sources. Used to add "the network came
22
+ * back" to the foreground edge; see the README for the expo-network and
23
+ * NetInfo recipes, which are four lines each and stay in the consumer so this
24
+ * package needs neither as a dependency.
25
+ *
26
+ * A wake is a hint, never a command: the feeds and the video session ignore
27
+ * one while they are connected or mid-connect, so firing twice costs nothing.
28
+ */
29
+ export function composeWake(...sources) {
30
+ return (onWake) => {
31
+ const unsubscribes = sources.map((source) => source(onWake));
32
+ return () => {
33
+ for (const unsubscribe of unsubscribes)
34
+ unsubscribe();
35
+ };
36
+ };
37
+ }
38
+ //# sourceMappingURL=wake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wake.js","sourceRoot":"","sources":["../src/wake.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAKxC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,MAAkB;IAC7C,MAAM,YAAY,GAAG,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;QACjE,IAAI,KAAK,KAAK,QAAQ;YAAE,MAAM,EAAE,CAAC;IACnC,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAG,OAAqB;IAClD,OAAO,CAAC,MAAM,EAAE,EAAE;QAChB,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAC7D,OAAO,GAAG,EAAE;YACV,KAAK,MAAM,WAAW,IAAI,YAAY;gBAAE,WAAW,EAAE,CAAC;QACxD,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,45 @@
1
+ type Bytes = ArrayBuffer | ArrayBufferView;
2
+ export interface HmacKey {
3
+ type: "secret";
4
+ algorithm: {
5
+ name: "HMAC";
6
+ hash: {
7
+ name: "SHA-256";
8
+ };
9
+ };
10
+ extractable: boolean;
11
+ usages: string[];
12
+ raw: Uint8Array;
13
+ }
14
+ export declare const subtle: {
15
+ digest(algorithm: {
16
+ name: string;
17
+ } | string, data: Bytes): Promise<ArrayBuffer>;
18
+ importKey(format: string, keyData: Bytes, algorithm: {
19
+ name: string;
20
+ hash?: {
21
+ name: string;
22
+ } | string;
23
+ }, extractable: boolean, usages: string[]): Promise<HmacKey>;
24
+ sign(_algorithm: {
25
+ name: string;
26
+ }, key: HmacKey, data: Bytes): Promise<ArrayBuffer>;
27
+ };
28
+ declare const webcrypto: {
29
+ subtle: {
30
+ digest(algorithm: {
31
+ name: string;
32
+ } | string, data: Bytes): Promise<ArrayBuffer>;
33
+ importKey(format: string, keyData: Bytes, algorithm: {
34
+ name: string;
35
+ hash?: {
36
+ name: string;
37
+ } | string;
38
+ }, extractable: boolean, usages: string[]): Promise<HmacKey>;
39
+ sign(_algorithm: {
40
+ name: string;
41
+ }, key: HmacKey, data: Bytes): Promise<ArrayBuffer>;
42
+ };
43
+ };
44
+ export default webcrypto;
45
+ //# sourceMappingURL=webcrypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webcrypto.d.ts","sourceRoot":"","sources":["../src/webcrypto.ts"],"names":[],"mappings":"AAuBA,KAAK,KAAK,GAAG,WAAW,GAAG,eAAe,CAAC;AAc3C,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE;YAAE,IAAI,EAAE,SAAS,CAAA;SAAE,CAAA;KAAE,CAAC;IACvD,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,EAAE,UAAU,CAAC;CACjB;AAED,eAAO,MAAM,MAAM;sBACO;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;sBAW3E,MAAM,WACL,KAAK,aACH;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,MAAM,CAAA;KAAE,eAChD,OAAO,UACZ,MAAM,EAAE,GACf,OAAO,CAAC,OAAO,CAAC;qBAgBI;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,OAAO,OAAO,QAAQ,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;CAK1F,CAAC;AAEF,QAAA,MAAM,SAAS;;0BAvCW;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;0BAW3E,MAAM,WACL,KAAK,aACH;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE;gBAAE,IAAI,EAAE,MAAM,CAAA;aAAE,GAAG,MAAM,CAAA;SAAE,eAChD,OAAO,UACZ,MAAM,EAAE,GACf,OAAO,CAAC,OAAO,CAAC;yBAgBI;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,OAAO,OAAO,QAAQ,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;;CAO/D,CAAC;AAC7B,eAAe,SAAS,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `@xorgate/react-native/webcrypto` — the WebCrypto subset the KVS signaling
3
+ * client's SigV4 signer needs, for Metro to alias `isomorphic-webcrypto` at.
4
+ * See README "Metro setup"; it is one line of config and is not imported by
5
+ * anything in this package.
6
+ *
7
+ * Why an alias is needed at all: `amazon-kinesis-video-streams-webrtc` signs
8
+ * its WebSocket URL through `isomorphic-webcrypto`, whose React Native build
9
+ * evaluates to `undefined` on Hermes — its export is assembled behind an
10
+ * msrcrypto PRNG promise, and it also wants `expo-random`, removed from the
11
+ * Expo SDK after 51. The symptom is "Cannot read property 'subtle' of
12
+ * undefined" the first time a viewer connects.
13
+ *
14
+ * Why this is a complete answer rather than a stopgap: the signer makes
15
+ * exactly three calls — `digest(SHA-256)`, `importKey("raw", HMAC)` and
16
+ * `sign(HMAC)` — and all three are deterministic hashing over
17
+ * `@aws-crypto/sha256-js`, which is already in the tree because
18
+ * `presignIotWssUrl` signs with it. The entropy msrcrypto was providing is
19
+ * not used on this path. Anything outside those three calls throws by name
20
+ * instead of returning a wrong answer quietly.
21
+ */
22
+ import { Sha256 } from "@aws-crypto/sha256-js";
23
+ function toU8(data) {
24
+ if (data instanceof ArrayBuffer)
25
+ return new Uint8Array(data);
26
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
27
+ }
28
+ function toArrayBuffer(bytes) {
29
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
30
+ }
31
+ export const subtle = {
32
+ async digest(algorithm, data) {
33
+ const name = typeof algorithm === "string" ? algorithm : algorithm.name;
34
+ if (name.toUpperCase() !== "SHA-256") {
35
+ throw new Error(`@xorgate/react-native/webcrypto: unsupported digest ${name}`);
36
+ }
37
+ const hash = new Sha256();
38
+ hash.update(toU8(data));
39
+ return toArrayBuffer(await hash.digest());
40
+ },
41
+ async importKey(format, keyData, algorithm, extractable, usages) {
42
+ if (format !== "raw" || algorithm.name.toUpperCase() !== "HMAC") {
43
+ throw new Error(`@xorgate/react-native/webcrypto: unsupported importKey ${format}/${algorithm.name}`);
44
+ }
45
+ return {
46
+ type: "secret",
47
+ algorithm: { name: "HMAC", hash: { name: "SHA-256" } },
48
+ extractable,
49
+ usages,
50
+ // Copied: the caller may reuse its buffer for the next signing step.
51
+ raw: toU8(keyData).slice(),
52
+ };
53
+ },
54
+ async sign(_algorithm, key, data) {
55
+ const hash = new Sha256(key.raw);
56
+ hash.update(toU8(data));
57
+ return toArrayBuffer(await hash.digest());
58
+ },
59
+ };
60
+ const webcrypto = { subtle };
61
+ export default webcrypto;
62
+ //# sourceMappingURL=webcrypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webcrypto.js","sourceRoot":"","sources":["../src/webcrypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAI/C,SAAS,IAAI,CAAC,IAAW;IACvB,IAAI,IAAI,YAAY,WAAW;QAAE,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;IAC7D,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,aAAa,CAAC,KAAiB;IACtC,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,CACvB,KAAK,CAAC,UAAU,EAChB,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CACrB,CAAC;AACnB,CAAC;AAUD,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,CAAC,MAAM,CAAC,SAAoC,EAAE,IAAW;QAC5D,MAAM,IAAI,GAAG,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC;QACxE,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,uDAAuD,IAAI,EAAE,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACxB,OAAO,aAAa,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,SAAS,CACb,MAAc,EACd,OAAc,EACd,SAA6D,EAC7D,WAAoB,EACpB,MAAgB;QAEhB,IAAI,MAAM,KAAK,KAAK,IAAI,SAAS,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;YAChE,MAAM,IAAI,KAAK,CACb,0DAA0D,MAAM,IAAI,SAAS,CAAC,IAAI,EAAE,CACrF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE;YACtD,WAAW;YACX,MAAM;YACN,qEAAqE;YACrE,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE;SAC3B,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAA4B,EAAE,GAAY,EAAE,IAAW;QAChE,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACxB,OAAO,aAAa,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;CACF,CAAC;AAEF,MAAM,SAAS,GAAG,EAAE,MAAM,EAAE,CAAC;AAC7B,eAAe,SAAS,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { WebRtcPlatform } from "@xorgate/react";
2
+ /**
3
+ * The KVS viewer's platform on React Native.
4
+ *
5
+ * Only two of the five members differ from the browser: the peer connection
6
+ * (react-native-webrtc's, which is a real native PeerConnection with hardware
7
+ * H.264 decode behind it) and the id entropy (Hermes has no `crypto` global).
8
+ * The three AWS bodies — `getViewerEndpoints`, `getIceServers` and the lazy
9
+ * `createSignaling` facade — are `@xorgate/react`'s own: the AWS SDK v3
10
+ * clients and `amazon-kinesis-video-streams-webrtc` both run on Hermes once
11
+ * Metro resolves them (see README "Metro setup"), so copying them here would
12
+ * only mean two implementations of the hardened signaling path. The one
13
+ * addition is the missing-`crypto` guard below, in front of the first of
14
+ * them.
15
+ *
16
+ * `iceTransportPolicy: "all"` matters on a phone: a viewer on a carrier NAT
17
+ * frequently has no working host or srflx path to the device and lands on the
18
+ * TURN relay AWS hands out with the ICE server list.
19
+ */
20
+ export declare function createNativeWebRtcPlatform(randomId: () => string): WebRtcPlatform;
21
+ //# sourceMappingURL=webrtc-platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webrtc-platform.d.ts","sourceRoot":"","sources":["../src/webrtc-platform.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGzE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,MAAM,GAAG,cAAc,CAiBjF"}
@@ -0,0 +1,61 @@
1
+ import { createWebRtcPlatform } from "@xorgate/react";
2
+ import { RTCPeerConnection } from "react-native-webrtc";
3
+ /**
4
+ * The KVS viewer's platform on React Native.
5
+ *
6
+ * Only two of the five members differ from the browser: the peer connection
7
+ * (react-native-webrtc's, which is a real native PeerConnection with hardware
8
+ * H.264 decode behind it) and the id entropy (Hermes has no `crypto` global).
9
+ * The three AWS bodies — `getViewerEndpoints`, `getIceServers` and the lazy
10
+ * `createSignaling` facade — are `@xorgate/react`'s own: the AWS SDK v3
11
+ * clients and `amazon-kinesis-video-streams-webrtc` both run on Hermes once
12
+ * Metro resolves them (see README "Metro setup"), so copying them here would
13
+ * only mean two implementations of the hardened signaling path. The one
14
+ * addition is the missing-`crypto` guard below, in front of the first of
15
+ * them.
16
+ *
17
+ * `iceTransportPolicy: "all"` matters on a phone: a viewer on a carrier NAT
18
+ * frequently has no working host or srflx path to the device and lands on the
19
+ * TURN relay AWS hands out with the ICE server list.
20
+ */
21
+ export function createNativeWebRtcPlatform(randomId) {
22
+ const platform = createWebRtcPlatform({
23
+ randomId,
24
+ createPeerConnection: (iceServers) => new RTCPeerConnection({
25
+ iceServers: iceServers,
26
+ iceTransportPolicy: "all",
27
+ }),
28
+ });
29
+ return {
30
+ ...platform,
31
+ async getViewerEndpoints(channelRef, region, credentials) {
32
+ assertCryptoGlobal();
33
+ return platform.getViewerEndpoints(channelRef, region, credentials);
34
+ },
35
+ };
36
+ }
37
+ /**
38
+ * The AWS SDK needs a `crypto` global, and Hermes has none.
39
+ *
40
+ * `@smithy/core`'s React Native serde bundle ends its UUID v4 path in
41
+ * `const _getRandomValues = (array) => crypto.getRandomValues(array)` — a BARE
42
+ * `crypto` — and that is the fallback `bindV4` selects precisely when
43
+ * `typeof crypto === "undefined"`, so the guard hands control to the one line
44
+ * that cannot survive the case it guards against. Without the polyfill the
45
+ * first signaling call throws `Property 'crypto' doesn't exist`, which the
46
+ * viewer session reads as a network failure and retries forever.
47
+ *
48
+ * Checked here, at the first AWS call, rather than at platform construction:
49
+ * throwing while the provider mounts would take the whole app down over a
50
+ * feature the user may not have opened. This way the message lands in the
51
+ * session's `error` and says what to do.
52
+ */
53
+ function assertCryptoGlobal() {
54
+ const c = globalThis.crypto;
55
+ if (c && typeof c.getRandomValues === "function")
56
+ return;
57
+ throw new Error("Live video needs a `crypto` global, which React Native does not have: the AWS SDK " +
58
+ 'calls crypto.getRandomValues(). Add `import "@xorgate/react-native/polyfills"` to the ' +
59
+ "top of your entry file. (Live telemetry does not need it.)");
60
+ }
61
+ //# sourceMappingURL=webrtc-platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webrtc-platform.js","sourceRoot":"","sources":["../src/webrtc-platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,0BAA0B,CAAC,QAAsB;IAC/D,MAAM,QAAQ,GAAG,oBAAoB,CAAC;QACpC,QAAQ;QACR,oBAAoB,EAAE,CAAC,UAAU,EAAsB,EAAE,CACvD,IAAI,iBAAiB,CAAC;YACpB,UAAU,EAAE,UAAmB;YAC/B,kBAAkB,EAAE,KAAK;SAC1B,CAAkC;KACtC,CAAC,CAAC;IAEH,OAAO;QACL,GAAG,QAAQ;QACX,KAAK,CAAC,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW;YACtD,kBAAkB,EAAE,CAAC;YACrB,OAAO,QAAQ,CAAC,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;QACtE,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,kBAAkB;IACzB,MAAM,CAAC,GAAI,UAAyD,CAAC,MAAM,CAAC;IAC5E,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,eAAe,KAAK,UAAU;QAAE,OAAO;IACzD,MAAM,IAAI,KAAK,CACb,oFAAoF;QAClF,wFAAwF;QACxF,4DAA4D,CAC/D,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@xorgate/react-native",
3
+ "version": "0.1.0",
4
+ "description": "React Native adapters for @xorgate/react: live telemetry over MQTT, live video over WebRTC (react-native-webrtc / RTCView), and the recorded-media replay player over expo-video.",
5
+ "license": "MIT",
6
+ "author": "Epye Labs",
7
+ "homepage": "https://docs.xorgate.io/docs/frontend-sdk",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/epyelabs/xorgate-react-native.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/epyelabs/xorgate-react-native/issues"
14
+ },
15
+ "keywords": [
16
+ "xorgate",
17
+ "react-native",
18
+ "expo",
19
+ "hooks",
20
+ "iot",
21
+ "telemetry",
22
+ "webrtc",
23
+ "video",
24
+ "replay"
25
+ ],
26
+ "type": "module",
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "sideEffects": [
31
+ "./dist/polyfills.js"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "./polyfills": {
43
+ "types": "./dist/polyfills.d.ts",
44
+ "import": "./dist/polyfills.js",
45
+ "default": "./dist/polyfills.js"
46
+ },
47
+ "./package.json": "./package.json",
48
+ "./webcrypto": {
49
+ "types": "./dist/webcrypto.d.ts",
50
+ "import": "./dist/webcrypto.js",
51
+ "default": "./dist/webcrypto.js"
52
+ }
53
+ },
54
+ "module": "./dist/index.js",
55
+ "types": "./dist/index.d.ts",
56
+ "files": [
57
+ "dist",
58
+ "README.md",
59
+ "CHANGELOG.md",
60
+ "LICENSE"
61
+ ],
62
+ "scripts": {
63
+ "build": "npm run clean && tsc -p tsconfig.build.json",
64
+ "ci": "npm run typecheck && npm test && npm run build && npm run verify:tarball",
65
+ "clean": "rm -rf dist .tmp",
66
+ "prepack": "npm run build",
67
+ "test": "vitest run",
68
+ "typecheck": "tsc -p tsconfig.json --noEmit",
69
+ "verify:tarball": "node scripts/verify-tarball.mjs"
70
+ },
71
+ "peerDependencies": {
72
+ "expo-crypto": ">=13.0.0",
73
+ "react": "^18.2.0 || ^19.0.0",
74
+ "react-native": ">=0.76.0",
75
+ "react-native-webrtc": ">=124.0.0"
76
+ },
77
+ "dependencies": {
78
+ "@aws-crypto/sha256-js": "^5.2.0",
79
+ "@xorgate/react": "^0.2.0-next.1"
80
+ },
81
+ "devDependencies": {
82
+ "@testing-library/react": "^16.3.3",
83
+ "@types/node": "^22.10.2",
84
+ "@types/react": "^19.0.0",
85
+ "@types/react-dom": "^19.3.0",
86
+ "expo-crypto": "~57.0.3",
87
+ "jsdom": "^25.0.1",
88
+ "react": "^19.2.6",
89
+ "react-dom": "^19.3.0",
90
+ "react-native": "^0.86.3",
91
+ "react-native-webrtc": "^124.0.8",
92
+ "typescript": "^5.7.2",
93
+ "vitest": "^3.0.0"
94
+ }
95
+ }
package/readme.md ADDED
@@ -0,0 +1,312 @@
1
+ # @xorgate/react-native
2
+
3
+ React Native adapters for [`@xorgate/react`](https://www.npmjs.com/package/@xorgate/react).
4
+ Live telemetry over MQTT-over-WebSocket and live video over WebRTC, rendered by
5
+ `react-native-webrtc`'s `RTCView` — a real native video view with hardware
6
+ H.264 decode behind it, not a web player in a shell.
7
+
8
+ Everything pure or platform-neutral — the KVS session hardening, the telemetry
9
+ feed, the timeline, lanes, the replay clock, the telemetry math, the data hooks
10
+ — lives in `@xorgate/react` and is **re-exported from here**, so your app
11
+ imports from one package. What this package adds is the runtime underneath
12
+ (`nativePlatform`, `XorgateNativeProvider`) and the two things that cannot be
13
+ platform-neutral because they are pixels.
14
+
15
+ **0.1.0 ships telemetry and live video.** Replay over `expo-video` is 0.2.0.
16
+
17
+ **Full reference: [docs.xorgate.io/docs/frontend-sdk](https://docs.xorgate.io/docs/frontend-sdk)**
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npx expo install @xorgate/react-native react-native-webrtc @config-plugins/react-native-webrtc expo-crypto
23
+ ```
24
+
25
+ Peers: `react`, `react-native`, `react-native-webrtc`, `expo-crypto`.
26
+ `@xorgate/react` is a dependency, not a peer — one resolved copy is what makes
27
+ `instanceof XorgateError` hold across the boundary.
28
+
29
+ `expo-crypto` is required, not optional: Hermes ships **no `crypto` global at
30
+ all** — not `randomUUID`, not `getRandomValues`, not `subtle` — and every MQTT
31
+ and signaling client id needs entropy, so there is nothing else to default to.
32
+
33
+ Add the config plugin to `app.json` / `app.config.ts`:
34
+
35
+ ```json
36
+ { "plugins": ["@config-plugins/react-native-webrtc"] }
37
+ ```
38
+
39
+ **A development build is required.** `react-native-webrtc` is a native module,
40
+ so Expo Go cannot run it.
41
+
42
+ The plugin writes both an `NSCameraUsageDescription` and an
43
+ `NSMicrophoneUsageDescription`, and on Android adds `CAMERA`, `RECORD_AUDIO`,
44
+ `MODIFY_AUDIO_SETTINGS` and Bluetooth permissions. A **viewer** app uses none
45
+ of them, but Apple's upload check (ITMS-90683) still wants a microphone string
46
+ from any binary that links the microphone APIs, which WebRTC does. Set an
47
+ honest one rather than shipping the plugin's placeholder, and strip the Android
48
+ permissions you do not use:
49
+
50
+ ```ts
51
+ plugins: [["@config-plugins/react-native-webrtc", {
52
+ cameraPermission: "…",
53
+ microphonePermission: "This app views live video only and does not record audio.",
54
+ }]],
55
+ android: { blockedPermissions: ["android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS"] },
56
+ ```
57
+
58
+ ## Metro setup
59
+
60
+ `@xorgate/react`'s AWS / KVS / mqtt tree bundles under Metro with four
61
+ resolver rules. They are mechanical and package-agnostic — they read each
62
+ package's own `package.json` — so paste this into `metro.config.js` once:
63
+
64
+ ```js
65
+ const fs = require("node:fs")
66
+ const path = require("node:path")
67
+ const { getDefaultConfig } = require("expo/metro-config")
68
+
69
+ const config = getDefaultConfig(__dirname)
70
+
71
+ const AWS_SCOPES = ["@aws-sdk/", "@smithy/"]
72
+ const esEntry = new Map()
73
+
74
+ /**
75
+ * (1) `@aws-sdk/*` and `@smithy/*` packages that ship NO `exports` map fall
76
+ * through to `main` = `dist-cjs`, which requires `node:https` / `node:stream`.
77
+ * Their `react-native` and `browser` fields are sub-path maps, not entry
78
+ * points, so Metro cannot use them: redirect the bare request to the `module`
79
+ * entry (`dist-es`). Packages that DO have `exports` (`@smithy/core`,
80
+ * `@aws-sdk/core`, `@aws-sdk/nested-clients`) resolve correctly and must be
81
+ * left alone — `@smithy/core` actively refuses `./dist-es/index.js`.
82
+ */
83
+ function awsEsEntry(name) {
84
+ if (!AWS_SCOPES.some((s) => name.startsWith(s))) return null
85
+ if (name.split("/").length !== 2) return null // sub-path imports are fine
86
+ if (esEntry.has(name)) return esEntry.get(name)
87
+ let result = null
88
+ try {
89
+ const dir = path.dirname(require.resolve(`${name}/package.json`, { paths: [__dirname] }))
90
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"))
91
+ if (!pkg.exports && typeof pkg.module === "string") result = path.join(dir, pkg.module)
92
+ } catch {
93
+ result = null
94
+ }
95
+ esEntry.set(name, result)
96
+ return result
97
+ }
98
+
99
+ const previous = config.resolver.resolveRequest
100
+
101
+ config.resolver.resolveRequest = (context, moduleName, platform) => {
102
+ const es = awsEsEntry(moduleName)
103
+ if (es) return { type: "sourceFile", filePath: es }
104
+
105
+ // (2) `./runtimeConfig` from inside a `dist-es` directory -> the
106
+ // `runtimeConfig.native.js` sibling, which is what the package's
107
+ // `react-native` sub-path map would have chosen. Without it the AWS clients
108
+ // come up with the Node runtime config instead of `FetchHttpHandler`.
109
+ if (moduleName === "./runtimeConfig" && context.originModulePath.includes(`${path.sep}dist-es${path.sep}`)) {
110
+ const native = path.join(path.dirname(context.originModulePath), "runtimeConfig.native.js")
111
+ if (fs.existsSync(native)) return { type: "sourceFile", filePath: native }
112
+ }
113
+
114
+ // (3) The KVS signaling client does `new (WebSocket || require("ws"))(url)`.
115
+ // React Native has a global `WebSocket`, so the Node implementation only has
116
+ // to RESOLVE; it never runs.
117
+ if (moduleName === "ws") {
118
+ return { type: "sourceFile", filePath: require.resolve("ws/browser.js", { paths: [__dirname] }) }
119
+ }
120
+
121
+ // (4) `isomorphic-webcrypto` -> this package's WebCrypto subset. The real
122
+ // package's React Native build evaluates to `undefined` on Hermes (its
123
+ // export is assembled behind an msrcrypto PRNG promise, and it wants
124
+ // `expo-random`, gone from the Expo SDK after 51), so the KVS signaling
125
+ // client dies with "Cannot read property 'subtle' of undefined" the first
126
+ // time a viewer connects.
127
+ if (moduleName === "isomorphic-webcrypto") {
128
+ return {
129
+ type: "sourceFile",
130
+ filePath: require.resolve("@xorgate/react-native/webcrypto", { paths: [__dirname] }),
131
+ }
132
+ }
133
+
134
+ return (previous ?? context.resolveRequest)(context, moduleName, platform)
135
+ }
136
+
137
+ module.exports = config
138
+ ```
139
+
140
+ `@xorgate/react-native/webcrypto` covers exactly the three calls the SigV4
141
+ signer makes — `digest(SHA-256)`, `importKey("raw", HMAC)`, `sign(HMAC)` —
142
+ over `@aws-crypto/sha256-js`, which is already in the tree. All three are
143
+ deterministic hashing, so the entropy msrcrypto was adding is not used on this
144
+ path; anything outside those three throws by name rather than answering wrong.
145
+
146
+ Nothing else was needed on Expo SDK 57 / Hermes: no `buffer`, `process`,
147
+ `events`, `url`, stream or URL polyfill, measured rather than assumed.
148
+
149
+ A global `resolverMainFields: ["module", …]` is **not** a shortcut for rule 1:
150
+ it breaks `react-native-safe-area-context`'s codegen spec.
151
+
152
+ ## Polyfill
153
+
154
+ Add one line at the very top of your entry file, before anything that reaches
155
+ the SDK:
156
+
157
+ ```ts
158
+ import "@xorgate/react-native/polyfills"
159
+ ```
160
+
161
+ It installs `getRandomValues` and `randomUUID` from `expo-crypto` onto
162
+ `globalThis.crypto`, additively (an existing `crypto` keeps everything it
163
+ already has).
164
+
165
+ **Live video does not work without it.** Not because of this package —
166
+ `nativePlatform()` covers the SDK's own entropy through the `randomId` slot,
167
+ and live telemetry runs with no polyfill at all — but because of the AWS SDK
168
+ underneath the KVS viewer. `@smithy/core`'s React Native serde bundle ends its
169
+ UUID v4 path in
170
+
171
+ ```js
172
+ const _getRandomValues = (array) => crypto.getRandomValues(array)
173
+ ```
174
+
175
+ with a **bare** `crypto`, and that is the fallback `bindV4` selects precisely
176
+ when `typeof crypto === "undefined"` — so the guard hands control to the one
177
+ line that cannot survive the case it guards against. On Hermes the first
178
+ `GetSignalingChannelEndpoint` call then throws `Property 'crypto' doesn't
179
+ exist`, the session treats it as a network error, and the viewer retries
180
+ forever on a backoff. (Measured on Expo SDK 57 / Hermes, `@smithy/core` 3.34.1,
181
+ 2026-09-15. The same bug was fixed for the Node build in
182
+ smithy-typescript#2022; the browser and React Native builds were not changed —
183
+ smithy-typescript#2276.)
184
+
185
+ ## Use
186
+
187
+ ```tsx
188
+ import { XorgateNativeProvider } from "@xorgate/react-native"
189
+
190
+ <XorgateNativeProvider
191
+ key={organizationId ?? "none"} // remounting is how you switch tenants
192
+ auth={{ getLiveCredentials }}
193
+ organizationId={organizationId}
194
+ >
195
+ {children}
196
+ </XorgateNativeProvider>
197
+ ```
198
+
199
+ Same props as `@xorgate/react`'s `XorgateProvider`, plus the native slots
200
+ (`randomId`, `wake`, `mqttConnect`, and a `platform` escape hatch).
201
+
202
+ ### Live telemetry
203
+
204
+ Unchanged from the web package — `mqtt.js` 5 runs on React Native's global
205
+ `WebSocket` as it is:
206
+
207
+ ```tsx
208
+ const { latest, history, status, receivedAt } = useLiveTelemetry(deviceId)
209
+ ```
210
+
211
+ Read freshness off `receivedAt`, not `status`. A phone that loses signal keeps
212
+ a socket that reads `connected` until the OS surfaces the close, which can take
213
+ 20 s or more; the age of the last frame is the honest signal, and the web app
214
+ treats it the same way.
215
+
216
+ ### Live video
217
+
218
+ ```tsx
219
+ import { LiveVideo, useVideoChannels } from "@xorgate/react-native"
220
+
221
+ const { data } = useVideoChannels(deviceId)
222
+ const cam0 = data?.find((c) => c.streamKey === "cam0") ?? null
223
+
224
+ <LiveVideo channel={cam0} style={{ aspectRatio: 16 / 9 }} />
225
+ ```
226
+
227
+ `useVideoChannels` is a REST read, so it needs an auth mode that HAS a REST
228
+ credential. An app on `auth={{ getLiveCredentials }}` — the vended-direct mode,
229
+ where your own backend mints the credential and your users have no xorgate
230
+ identity — gets `INVALID_CONFIG` from it by design, and should fetch the
231
+ channel list from its own API and hand the `VideoChannel` to `LiveVideo`
232
+ directly. The live and replay planes are the parts that mode reaches.
233
+
234
+ `LiveVideo` is the view and nothing else: no card, no spinner, no status text,
235
+ no aspect ratio of its own. Take `onStatusChange`, `onStats`,
236
+ `onVideoDimensions` and `onDisplayedStreamChange` for chrome of your own, or
237
+ drop to the hook:
238
+
239
+ ```tsx
240
+ const { streamURL, status, stats, error } = useLiveVideo(cam0)
241
+ ```
242
+
243
+ One behaviour `LiveVideo` adds over a bare `RTCView`: **the reconnect hold**.
244
+ Every reconnect produces a new `MediaStream` with a new `streamURL`, and an
245
+ `RTCView` pointed at a stream that has not decoded a frame is black — so a
246
+ short blip would blank the picture, where a browser's `<video>` simply keeps
247
+ its last frame. `LiveVideo` mounts the new stream underneath at zero opacity
248
+ and promotes it only once the renderer reports its dimensions, falling back to
249
+ the swap after 3 s so a dead stream cannot pin a stale frame. Render `RTCView`
250
+ yourself and you own that.
251
+
252
+ ### Waking the live plane
253
+
254
+ The foreground edge (`AppState` → `active`) is wired for you, and on a phone it
255
+ matters more than anything else in this package: iOS suspends the app's sockets
256
+ within seconds of it leaving the screen, so both the MQTT connection and the
257
+ peer connection are usually dead when the user comes back. Without the nudge
258
+ the live plane sits out a backoff of up to 30 s in front of them.
259
+
260
+ Add "the network came back" — four lines, and it stays in your app so this
261
+ package depends on neither network library:
262
+
263
+ ```ts
264
+ import * as Network from "expo-network"
265
+
266
+ const networkWake = (onWake: () => void) => {
267
+ const sub = Network.addNetworkStateListener((s) => {
268
+ if (s.isInternetReachable ?? s.isConnected) onWake()
269
+ })
270
+ return () => sub.remove()
271
+ }
272
+
273
+ <XorgateNativeProvider wake={[networkWake]} …>
274
+ ```
275
+
276
+ With `@react-native-community/netinfo`:
277
+
278
+ ```ts
279
+ const networkWake = (onWake: () => void) =>
280
+ NetInfo.addEventListener((s) => { if (s.isConnected) onWake() })
281
+ ```
282
+
283
+ A wake is a hint, never a command: a feed that is connected or mid-connect
284
+ ignores one, so firing twice costs nothing.
285
+
286
+ ## What is not here
287
+
288
+ `@xorgate/react`'s browser-only exports — its `useLiveVideo`, `LiveVideo`,
289
+ `useReplayPlayer`, `ReplayVideo` and the `browser*` platform helpers — are
290
+ deliberately not re-exported: they need `HTMLVideoElement`, `window` or Media
291
+ Source Extensions. `useReplayPlayerCore`, the timeline, the lanes and the clock
292
+ **are** re-exported; the `expo-video` engine that binds them to a player
293
+ arrives in 0.2.0.
294
+
295
+ ## Scripts
296
+
297
+ `npm run ci` = `typecheck` → `test` → `build` → `verify:tarball` (pack, install
298
+ into an empty project, import it as a consumer with stub peers, check the
299
+ export surface, scan for secret material).
300
+
301
+ The suite runs under jsdom against doubles for `react-native` and
302
+ `react-native-webrtc`, because neither has a Node build. It proves the wiring —
303
+ which listener is attached, which prop the view gets, what happens across a
304
+ reconnect — and nothing about pixels or hardware decode, which are measured by
305
+ hand on a simulator and a phone.
306
+
307
+ Publishing is manual (`npm publish --tag next` for pre-releases, `npm publish`
308
+ for a final); CI has no publish job on purpose.
309
+
310
+ ## License
311
+
312
+ MIT © Epye Labs