@exodus/react-native-bundle-loader 0.2.0-exodus.1

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/package.json ADDED
@@ -0,0 +1,149 @@
1
+ {
2
+ "name": "@exodus/react-native-bundle-loader",
3
+ "version": "0.2.0-exodus.1",
4
+ "description": "Loads a remote React Native JS bundle.",
5
+ "main": "lib/commonjs/index",
6
+ "module": "lib/module/index",
7
+ "types": "lib/typescript/index.d.ts",
8
+ "react-native": "src/index",
9
+ "source": "src/index",
10
+ "files": [
11
+ "src",
12
+ "lib",
13
+ "android/build.gradle",
14
+ "android/gradle.properties",
15
+ "android/src/main",
16
+ "ios/BundleLoader.h",
17
+ "ios/BundleLoader.m",
18
+ "ios/BundleLoader.xcodeproj/project.pbxproj",
19
+ "react-native-bundle-loader.podspec",
20
+ "SECURITY.md",
21
+ "!**/*.map",
22
+ "!**/*.snap",
23
+ "!**/*.iml",
24
+ "!**/*.tgz",
25
+ "!**/*.test.*",
26
+ "!**/*.spec.*",
27
+ "!**/__tests__",
28
+ "!**/__snapshots__",
29
+ "!**/__fixtures__",
30
+ "!**/__mocks__",
31
+ "!**/.DS_Store",
32
+ "!**/.gitkeep",
33
+ "!**/xcuserdata",
34
+ "!**/.project",
35
+ "!**/.settings",
36
+ "!**/.idea",
37
+ "!**/.vscode",
38
+ "!android/src/test",
39
+ "!android/src/androidTest"
40
+ ],
41
+ "scripts": {
42
+ "test": "jest",
43
+ "typescript": "tsc --noEmit",
44
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
45
+ "prepare": "bob build",
46
+ "verify-pack": "rm -rf _pack && mkdir _pack && yarn prepare && npm pack --pack-destination _pack && tar -tzf _pack/*.tgz | LC_ALL=C sort | sed 's|^package/||' | diff -u .npm-tarball-allowlist -",
47
+ "preflight": "yarn lint && yarn typescript && yarn test --ci --runInBand && yarn verify-pack"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.18"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "keywords": [
56
+ "react-native",
57
+ "ios",
58
+ "android",
59
+ "bundle",
60
+ "remote",
61
+ "ota",
62
+ "integrity"
63
+ ],
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/ExodusForks/react-native-bundle-loader.git"
67
+ },
68
+ "author": "Exodus Movement, Inc. <hello@exodus.com>",
69
+ "contributors": [
70
+ "Jusbrasil <dev@jusbrasil.com.br> (original author)"
71
+ ],
72
+ "license": "MIT",
73
+ "bugs": {
74
+ "url": "https://github.com/ExodusForks/react-native-bundle-loader/issues"
75
+ },
76
+ "homepage": "https://github.com/ExodusForks/react-native-bundle-loader#readme",
77
+ "dependencies": {
78
+ "@exodus/bytes": "1.15.0",
79
+ "@exodus/crypto": "1.0.0-rc.34"
80
+ },
81
+ "devDependencies": {
82
+ "@react-native-community/bob": "0.16.2",
83
+ "@react-native-community/eslint-config": "2.0.0",
84
+ "@types/jest": "26.0.14",
85
+ "@types/react": "16.9.52",
86
+ "@types/react-native": "0.62.13",
87
+ "eslint": "7.11.0",
88
+ "eslint-config-prettier": "6.13.0",
89
+ "eslint-plugin-prettier": "3.1.4",
90
+ "jest": "26.5.3",
91
+ "prettier": "2.1.2",
92
+ "react": "16.11.0",
93
+ "react-native": "0.62.2",
94
+ "typescript": "3.9.7"
95
+ },
96
+ "peerDependencies": {
97
+ "react": ">=16.8.0",
98
+ "react-native": ">=0.62.0"
99
+ },
100
+ "jest": {
101
+ "preset": "react-native",
102
+ "modulePathIgnorePatterns": [
103
+ "<rootDir>/lib/"
104
+ ]
105
+ },
106
+ "eslintConfig": {
107
+ "extends": [
108
+ "@react-native-community",
109
+ "prettier"
110
+ ],
111
+ "rules": {
112
+ "prettier/prettier": [
113
+ "error",
114
+ {
115
+ "quoteProps": "consistent",
116
+ "singleQuote": true,
117
+ "tabWidth": 2,
118
+ "trailingComma": "es5",
119
+ "useTabs": false
120
+ }
121
+ ]
122
+ }
123
+ },
124
+ "eslintIgnore": [
125
+ "node_modules/",
126
+ "lib/"
127
+ ],
128
+ "prettier": {
129
+ "quoteProps": "consistent",
130
+ "singleQuote": true,
131
+ "tabWidth": 2,
132
+ "trailingComma": "es5",
133
+ "useTabs": false
134
+ },
135
+ "@react-native-community/bob": {
136
+ "source": "src",
137
+ "output": "lib",
138
+ "targets": [
139
+ "commonjs",
140
+ "module",
141
+ [
142
+ "typescript",
143
+ {
144
+ "project": "tsconfig.build.json"
145
+ }
146
+ ]
147
+ ]
148
+ }
149
+ }
@@ -0,0 +1,19 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "react-native-bundle-loader"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => "13.0" }
14
+ s.source = { :git => "https://github.com/ExodusForks/react-native-bundle-loader.git", :tag => "v#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm}"
17
+
18
+ s.dependency "React-Core"
19
+ end
package/src/index.tsx ADDED
@@ -0,0 +1,171 @@
1
+ /** @format */
2
+ import React, { useCallback, useState } from 'react';
3
+ import {
4
+ Alert,
5
+ Modal,
6
+ NativeModules,
7
+ NativeSyntheticEvent,
8
+ StyleSheet,
9
+ Text,
10
+ TextInput,
11
+ TextInputChangeEventData,
12
+ TouchableOpacity,
13
+ View,
14
+ } from 'react-native';
15
+ import { hash } from '@exodus/crypto/hash';
16
+ import { fromHex } from '@exodus/bytes/hex.js';
17
+ import { toBase64 } from '@exodus/bytes/base64.js';
18
+
19
+ export type RunningMode = 'LOCAL' | 'REMOTE';
20
+
21
+ type NativeBundleLoader = {
22
+ load(url: string): void;
23
+ loadFromBase64?(base64: string): Promise<void>;
24
+ runningMode(): Promise<RunningMode>;
25
+ };
26
+
27
+ function getNative(): NativeBundleLoader {
28
+ const m = (NativeModules as { BundleLoader?: NativeBundleLoader })
29
+ .BundleLoader;
30
+ if (!m) {
31
+ throw new Error(
32
+ "@exodus/react-native-bundle-loader: native module 'BundleLoader' is not linked. " +
33
+ 'On iOS, run `pod install` and rebuild the host app. ' +
34
+ 'On Android, ensure BundleLoaderPackage is registered.'
35
+ );
36
+ }
37
+ return m;
38
+ }
39
+
40
+ function assertSafeUrl(url: string): void {
41
+ if (typeof url !== 'string' || url.length === 0) {
42
+ throw new Error('Bundle URL must be a non-empty string');
43
+ }
44
+ if (!/^https:\/\//.test(url)) {
45
+ throw new Error('Bundle URL must use the https scheme');
46
+ }
47
+ }
48
+
49
+ function timingSafeEqualBytes(a: Uint8Array, b: Uint8Array): boolean {
50
+ if (a.length !== b.length) return false;
51
+ let diff = 0;
52
+ /* eslint-disable no-bitwise */
53
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
54
+ /* eslint-enable no-bitwise */
55
+ return diff === 0;
56
+ }
57
+
58
+ export async function loadVerified(
59
+ url: string,
60
+ expectedSha256Hex: string
61
+ ): Promise<void> {
62
+ assertSafeUrl(url);
63
+ if (
64
+ typeof expectedSha256Hex !== 'string' ||
65
+ expectedSha256Hex.length !== 64
66
+ ) {
67
+ throw new Error('Expected sha256 must be a 64-character hex string');
68
+ }
69
+ const expected = fromHex(expectedSha256Hex);
70
+
71
+ const response = await fetch(url);
72
+ if (!response.ok) {
73
+ throw new Error(`Bundle fetch failed: HTTP ${response.status}`);
74
+ }
75
+ const buffer = await response.arrayBuffer();
76
+ const bytes = new Uint8Array(buffer);
77
+ const actual = await hash('sha256', bytes, 'uint8');
78
+
79
+ if (!timingSafeEqualBytes(actual, expected)) {
80
+ throw new Error('Bundle hash mismatch — refusing to load');
81
+ }
82
+
83
+ const native = getNative();
84
+ if (typeof native.loadFromBase64 !== 'function') {
85
+ throw new Error(
86
+ 'Native loadFromBase64 not available. Rebuild the host app after upgrading.'
87
+ );
88
+ }
89
+ await native.loadFromBase64(toBase64(bytes));
90
+ }
91
+
92
+ function loadUnverified(url: string): void {
93
+ assertSafeUrl(url);
94
+ getNative().load(url);
95
+ }
96
+
97
+ async function runningMode(): Promise<RunningMode> {
98
+ return getNative().runningMode();
99
+ }
100
+
101
+ const BundleLoader = {
102
+ load: loadUnverified,
103
+ loadVerified,
104
+ runningMode,
105
+ };
106
+
107
+ export default BundleLoader;
108
+
109
+ const styles = StyleSheet.create({
110
+ container: { marginTop: 48, padding: 16, flex: 1 },
111
+ input: {
112
+ height: 48,
113
+ marginTop: 8,
114
+ paddingHorizontal: 8,
115
+ borderColor: 'gray',
116
+ borderRadius: 4,
117
+ borderWidth: 1,
118
+ },
119
+ button: {
120
+ backgroundColor: '#007AFF',
121
+ marginTop: 16,
122
+ height: 48,
123
+ justifyContent: 'center',
124
+ },
125
+ buttonText: {
126
+ color: 'white',
127
+ alignSelf: 'center',
128
+ fontSize: 18,
129
+ alignContent: 'center',
130
+ },
131
+ });
132
+
133
+ export function BundlePrompt() {
134
+ const [url, setUrl] = useState<string>('');
135
+
136
+ const reload = useCallback(() => {
137
+ if (!url) {
138
+ Alert.alert('Oops…', 'You need to provide a URL');
139
+ return;
140
+ }
141
+ try {
142
+ loadUnverified(url);
143
+ } catch (e) {
144
+ Alert.alert('Invalid URL', (e as Error).message);
145
+ }
146
+ }, [url]);
147
+
148
+ return (
149
+ <Modal>
150
+ <View style={styles.container}>
151
+ <TextInput
152
+ keyboardType="url"
153
+ onChange={(e: NativeSyntheticEvent<TextInputChangeEventData>) =>
154
+ setUrl(e.nativeEvent.text.trim())
155
+ }
156
+ style={styles.input}
157
+ clearButtonMode="always"
158
+ autoFocus
159
+ placeholder="https://…"
160
+ />
161
+ <TouchableOpacity
162
+ style={styles.button}
163
+ onPress={reload}
164
+ accessibilityLabel="Reload entire app"
165
+ >
166
+ <Text style={styles.buttonText}>Reload</Text>
167
+ </TouchableOpacity>
168
+ </View>
169
+ </Modal>
170
+ );
171
+ }