@renbostudios/edge-ota 0.3.1 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +313 -11
  2. package/dist/index.js +47 -3
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,26 +1,328 @@
1
1
  # @renbostudios/edge-ota CLI
2
2
 
3
- Zero-SDK, self-hostable OTA updates for Expo.
3
+ Zero-SDK, self-hostable OTA update CLI for Expo React Native. Push signed JavaScript bundles to any EdgeOTA server.
4
4
 
5
- See the [main README](../../README.md) for full documentation.
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @renbostudios/edge-ota
9
+ ```
10
+
11
+ Requires Node.js 18+ and an Expo project with `expo-updates` installed.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # 1. Log in to your EdgeOTA account
17
+ edge-ota login
18
+
19
+ # 2. Register your project and generate signing keys
20
+ edge-ota init
21
+
22
+ # 3. Rebuild your native app (REQUIRED — see below)
23
+ npx expo prebuild --clean
24
+ npx expo run:ios
25
+ npx expo run:android
26
+
27
+ # 4. Push an OTA update
28
+ edge-ota push
29
+ ```
30
+
31
+ ---
6
32
 
7
33
  ## Commands
8
34
 
35
+ ### `edge-ota login`
36
+
37
+ Authenticate with your EdgeOTA account. Credentials are stored globally at `~/.config/edge-ota/config.json`.
38
+
39
+ ```bash
40
+ edge-ota login
41
+ edge-ota login --server https://my-server.com # custom server
9
42
  ```
10
- edge-ota login # Log in globally
11
- edge-ota logout # Log out
12
- edge-ota init # Set up a new project
13
- edge-ota push # Publish an OTA update
14
- edge-ota status # View recent deployments
15
- edge-ota keygen # Generate a new key pair
43
+
44
+ You'll be prompted for your email and password. The session token is saved globally — no per-project `.env` files needed.
45
+
46
+ ### `edge-ota logout`
47
+
48
+ Remove stored credentials.
49
+
50
+ ```bash
51
+ edge-ota logout
16
52
  ```
17
53
 
18
- ## Install
54
+ ### `edge-ota init`
55
+
56
+ Register your project on the EdgeOTA server, generate an ECDSA P-256 signing key pair, and configure `app.json` with the correct update URL.
19
57
 
20
58
  ```bash
21
- npm install -g @renbostudios/edge-ota
59
+ edge-ota init
60
+ edge-ota init --server https://my-server.com # override server
61
+ ```
62
+
63
+ **What it does:**
64
+
65
+ 1. Fetches your existing projects from the server (if any)
66
+ 2. Lets you select an existing project or create a new one
67
+ 3. Generates an ECDSA P-256 key pair
68
+ 4. Registers the project on the server with your public key
69
+ 5. Saves the private key to `~/.config/edge-ota/keys/<projectId>.key`
70
+ 6. Updates your `app.json` with:
71
+ - `expo.updates.url` — points to your EdgeOTA server
72
+ - `expo.updates.checkAutomatically` — set to `"ON_LOAD"`
73
+ - `expo.updates.fallbackToCacheTimeout` — set to `30000`
74
+ - `expo.updates.requestHeaders` — sets `expo-channel-name` to `"production"`
75
+ - `expo.extra.edgeOtaServer` — the server URL for manual override
76
+ - `expo.extra.edgeOtaPublicKey` — the generated public key
77
+
78
+ **After init, you MUST rebuild your native app.** The CLI will print a prominent warning with instructions. See the [Rebuild Required](#rebuild-required) section below.
79
+
80
+ ### `edge-ota push`
81
+
82
+ Export your Expo bundle and publish an OTA update to the server.
83
+
84
+ ```bash
85
+ edge-ota push # default: all platforms, production channel
86
+ edge-ota push --channel staging # deploy to staging
87
+ edge-ota push --platform ios # iOS only
88
+ edge-ota push --platform android # Android only
89
+ edge-ota push --skip-export # skip expo export, use existing ./dist
90
+ edge-ota push --dry-run # build and sign, but don't upload
91
+ ```
92
+
93
+ **What it does:**
94
+
95
+ 1. Runs `npx expo export` to create the JavaScript bundle (unless `--skip-export`)
96
+ 2. Collects all assets from `./dist`
97
+ 3. For each target platform (ios/android):
98
+ - Locates the platform-specific bundle
99
+ - Signs the payload with your ECDSA private key
100
+ - Uploads the bundle + signature to the server
101
+ 4. The server verifies the signature before storing the update
102
+
103
+ **How it reads your config:**
104
+
105
+ The CLI reads everything from `app.json` — no flags required for standard deploys:
106
+ - **Server URL** — from `expo.updates.url` (or `expo.extra.edgeOtaServer`)
107
+ - **Project ID** — parsed from the updates URL
108
+ - **Runtime version** — from `expo.runtimeVersion` (string or policy object)
109
+ - **Private key** — from `~/.config/edge-ota/keys/<projectId>.key`
110
+
111
+ ### `edge-ota status`
112
+
113
+ List recent deployments for the current project.
114
+
115
+ ```bash
116
+ edge-ota status
117
+ edge-ota status --limit 20 # show more releases
118
+ ```
119
+
120
+ ### `edge-ota keygen`
121
+
122
+ Generate a new ECDSA P-256 key pair and print it to stdout. Does not write any files.
123
+
124
+ ```bash
125
+ edge-ota keygen
126
+ ```
127
+
128
+ Useful for manual key rotation or generating keys for server-side signing.
129
+
130
+ ---
131
+
132
+ ## Rebuild Required
133
+
134
+ After running `edge-ota init`, you **must** rebuild your native app. The `expo-updates` native module reads its configuration (server URL, runtime version) from the **native binary**, not from `app.json` at runtime.
135
+
136
+ ```bash
137
+ # Regenerate native projects with fresh config
138
+ npx expo prebuild --clean
139
+
140
+ # Rebuild
141
+ npx expo run:ios
142
+ npx expo run:android
143
+
144
+ # Or via EAS
145
+ eas build --profile production
146
+ ```
147
+
148
+ **If you skip this step**, the app will fail with:
149
+
150
+ ```
151
+ Failed to check for update
152
+ ```
153
+
154
+ The native module has no valid server URL to reach because it was never embedded in the binary.
155
+
156
+ You must also rebuild after any change to `expo.updates.url` or `expo.runtimeVersion` in `app.json`.
157
+
158
+ ---
159
+
160
+ ## How It Works
161
+
162
+ ```
163
+ Developer Machine EdgeOTA Server User's Phone
164
+ ───────────────── ────────────── ────────────
165
+ edge-ota push ─────────────► POST /api/updates
166
+ (ECDSA verified)
167
+ stores bundle
168
+ ◄──────────── GET /api/updates
169
+ (expo-updates SDK)
170
+ ────────────► multipart manifest
171
+ ◄──────────── GET /api/assets/:hash
172
+ download bundle
173
+ reload app ✓
174
+ ```
175
+
176
+ 1. `edge-ota push` runs `expo export` and uploads the Hermes bundle + ECDSA signature to your server
177
+ 2. The server verifies the ECDSA P-256 signature before storing anything
178
+ 3. When users open the app, `expo-updates` checks your server for a new manifest
179
+ 4. The server responds with a signed multipart manifest (Expo Updates Protocol v1)
180
+ 5. `expo-updates` downloads and applies the update automatically
181
+
182
+ ---
183
+
184
+ ## What `init` Writes to `app.json`
185
+
186
+ After running `edge-ota init`, your `app.json` will contain:
187
+
188
+ ```json
189
+ {
190
+ "expo": {
191
+ "updates": {
192
+ "url": "https://your-server.com/api/projects/<projectId>/updates",
193
+ "checkAutomatically": "ON_LOAD",
194
+ "fallbackToCacheTimeout": 30000,
195
+ "requestHeaders": {
196
+ "expo-channel-name": "production"
197
+ }
198
+ },
199
+ "runtimeVersion": "1.0.0",
200
+ "extra": {
201
+ "edgeOtaServer": "https://your-server.com",
202
+ "edgeOtaPublicKey": "-----BEGIN PUBLIC KEY-----\n..."
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ The `runtimeVersion` field is **not modified** by the CLI — it must already exist in your `app.json`. It supports both static strings and policy objects:
209
+
210
+ ```json
211
+ // Static (recommended)
212
+ "runtimeVersion": "1.0.0"
213
+
214
+ // Policy-based (auto-increments with expo.version)
215
+ "runtimeVersion": { "policy": "appVersion" }
216
+ ```
217
+
218
+ ---
219
+
220
+ ## Global Storage
221
+
222
+ All credentials and keys are stored outside your project directory:
223
+
224
+ | File | Contents |
225
+ |---|---|
226
+ | `~/.config/edge-ota/config.json` | Auth token, email, default server URL |
227
+ | `~/.config/edge-ota/keys/<projectId>.key` | Private ECDSA signing key (per-project) |
228
+ | `~/.config/edge-ota/update-check.json` | Auto-update check timestamp |
229
+
230
+ Nothing sensitive is ever stored in your project repository.
231
+
232
+ ---
233
+
234
+ ## Environment Variables
235
+
236
+ | Variable | Purpose |
237
+ |---|---|
238
+ | `EDGE_OTA_TOKEN` | Override the auth token (useful for CI/CD) |
239
+ | `EDGE_OTA_UPDATING` | Internal — prevents recursive auto-updates |
240
+
241
+ ---
242
+
243
+ ## Auto-Update
244
+
245
+ The CLI checks for newer versions on npm every 15 minutes. If a newer version is found, it auto-installs globally. This check happens silently in the background and never blocks command execution.
246
+
247
+ To disable: set `EDGE_OTA_UPDATING=true` in your environment.
248
+
249
+ ---
250
+
251
+ ## Security
252
+
253
+ - All bundles are signed with ECDSA P-256 before upload
254
+ - The server rejects any bundle with an invalid or missing signature
255
+ - Private keys are stored with `0600` permissions (owner read/write only)
256
+ - Session tokens expire after 24 hours
257
+ - Private keys never leave your machine
258
+
259
+ ---
260
+
261
+ ## Server Setup
262
+
263
+ The EdgeOTA server is a standard Node.js/Express app. You can self-host it or use the managed cloud service.
264
+
265
+ ### Docker
266
+
267
+ ```bash
268
+ docker pull renbostudios/edge-ota-server:latest
269
+ docker run -p 3000:3000 -e DATABASE_URL=... renbostudios/edge-ota-server
270
+ ```
271
+
272
+ ### Docker Compose
273
+
274
+ ```bash
275
+ git clone https://github.com/renbostudios/edge-ota-cli
276
+ cd edge-ota
277
+ docker-compose up -d
278
+ ```
279
+
280
+ ### Manual
281
+
282
+ ```bash
283
+ git clone https://github.com/renbostudios/edge-ota-cli
284
+ cd edge-ota
285
+ pnpm install
286
+ pnpm --filter @renbostudios/edge-ota-server-node dev
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Troubleshooting
292
+
293
+ ### "Failed to check for update" on app launch
294
+
295
+ The `expo-updates` native module can't reach your server because the URL was never embedded in the binary. Fix: rebuild the native app after `edge-ota init`.
296
+
297
+ ```bash
298
+ npx expo prebuild --clean
299
+ npx expo run:ios
300
+ npx expo run:android
22
301
  ```
23
302
 
303
+ ### Updates work on iOS but not Android
304
+
305
+ The Android native project may still have the default Expo Updates URL. Fix: regenerate and rebuild.
306
+
307
+ ```bash
308
+ npx expo prebuild --clean
309
+ npx expo run:android
310
+ ```
311
+
312
+ Verify by checking `android/app/src/main/AndroidManifest.xml` for the `EXPO_UPDATE_URL` meta-data entry — it should point to your EdgeOTA server.
313
+
314
+ ### Updates stopped working after a version bump
315
+
316
+ If `runtimeVersion` uses the `appVersion` policy, every version bump changes the runtime version. Push a new update after bumping, or switch to a static string.
317
+
318
+ ### "No signing key found" error on push
319
+
320
+ Run `edge-ota init` to generate keys, or use `edge-ota keygen` to generate a new pair and register it manually.
321
+
24
322
  ---
25
323
 
26
- by **[Renbo Studios](https://renbostudios.com)** — Mobile App Development Studio
324
+ ## Built by Renbo Studios
325
+
326
+ [renbostudios.com](https://renbostudios.com) — Mobile App Development Studio
327
+
328
+ MIT License
package/dist/index.js CHANGED
@@ -246,12 +246,32 @@ function readAppJson(cwd) {
246
246
  }
247
247
  const expo = data?.expo ?? {};
248
248
  // ── Runtime version ──
249
- const runtimeVersion = expo.runtimeVersion;
249
+ let runtimeVersion = expo.runtimeVersion;
250
250
  if (!runtimeVersion) {
251
251
  console.error(` ${c.red}✗${c.reset} ${c.bold}expo.runtimeVersion${c.reset} is not set in app.json`);
252
252
  console.error(` Add: ${c.dim}"runtimeVersion": "1.0.0"${c.reset} under the expo key.\n`);
253
253
  process.exit(1);
254
254
  }
255
+ // Resolve runtimeVersion object (e.g. policy helper) to string
256
+ if (typeof runtimeVersion === "object" && runtimeVersion !== null) {
257
+ const policy = runtimeVersion.policy;
258
+ if (policy === "appVersion") {
259
+ runtimeVersion = expo.version;
260
+ }
261
+ else if (policy === "sdkVersion") {
262
+ runtimeVersion = expo.sdkVersion;
263
+ }
264
+ else {
265
+ runtimeVersion = expo.version || expo.sdkVersion;
266
+ }
267
+ }
268
+ if (!runtimeVersion || typeof runtimeVersion !== "string") {
269
+ console.error(` ${c.red}✗${c.reset} Could not resolve ${c.bold}expo.runtimeVersion${c.reset} to a valid string in app.json`);
270
+ if (typeof expo.runtimeVersion === "object") {
271
+ console.error(` Since you are using a policy, please ensure either ${c.bold}expo.version${c.reset} or ${c.bold}expo.sdkVersion${c.reset} is defined.\n`);
272
+ }
273
+ process.exit(1);
274
+ }
255
275
  // ── Updates URL ──
256
276
  const updatesUrl = expo?.updates?.url ?? "";
257
277
  if (!updatesUrl) {
@@ -571,6 +591,7 @@ program
571
591
  console.log(` ${c.green}✓${c.reset} Logged in as ${c.bold}${data.email}${c.reset}`);
572
592
  console.log(` ${c.dim}credentials saved to ${GLOBAL_CONFIG_FILE}${c.reset}`);
573
593
  console.log(sep + "\n");
594
+ process.exit(0);
574
595
  }
575
596
  catch (e) {
576
597
  spin.fail(`Connection failed: ${e.message}`);
@@ -593,6 +614,7 @@ program
593
614
  else {
594
615
  console.log(` ${c.dim}Already logged out.${c.reset}\n`);
595
616
  }
617
+ process.exit(0);
596
618
  });
597
619
  // ──────────────────────────────────────────────────────────────────────────────
598
620
  // edge-ota init
@@ -708,8 +730,27 @@ program
708
730
  console.log(` project ${c.dim}${projectId}${c.reset}`);
709
731
  console.log(` server ${c.dim}${serverUrl}${c.reset}`);
710
732
  console.log(` key file ${c.dim}${path.join(GLOBAL_KEYS_DIR, `${projectId}.key`)}${c.reset}`);
733
+ console.log(sep);
734
+ console.log(`\n${c.bold}${c.yellow} ⚠ REBUILD REQUIRED${c.reset}`);
735
+ console.log(sep);
736
+ console.log(` ${c.bold}Your native app MUST be rebuilt for OTA updates${c.reset}`);
737
+ console.log(` ${c.bold}to work.${c.reset} The server URL is baked into the native`);
738
+ console.log(` binary at build time — not read from app.json.`);
739
+ console.log();
740
+ console.log(` Run these commands in order:`);
741
+ console.log();
742
+ console.log(` ${c.cyan}npx expo prebuild --clean${c.reset}`);
743
+ console.log(` ${c.cyan}npx expo run:ios${c.reset}`);
744
+ console.log(` ${c.cyan}npx expo run:android${c.reset}`);
745
+ console.log();
746
+ console.log(` Or via EAS:`);
747
+ console.log(` ${c.cyan}eas build --profile production${c.reset}`);
748
+ console.log();
749
+ console.log(` ${c.red}Skipping this WILL cause "Failed to check for${c.reset}`);
750
+ console.log(` ${c.red}update" errors on app launch.${c.reset}`);
711
751
  console.log(sep + "\n");
712
- console.log(` ${c.dim}Next step:${c.reset} run ${c.cyan}edge-ota push${c.reset} to publish your first update.\n`);
752
+ console.log(` Then run ${c.cyan}edge-ota push${c.reset} to publish your first update.\n`);
753
+ process.exit(0);
713
754
  }
714
755
  });
715
756
  // ──────────────────────────────────────────────────────────────────────────────
@@ -869,6 +910,7 @@ program
869
910
  console.log(`\n${sep}`);
870
911
  console.log(` ${c.green}✓${c.reset} ${c.bold}done${c.reset} ${c.dim}update will be applied on next OTA sync${c.reset}`);
871
912
  console.log(`${sep}\n`);
913
+ process.exit(0);
872
914
  });
873
915
  // ──────────────────────────────────────────────────────────────────────────────
874
916
  // edge-ota status
@@ -903,7 +945,7 @@ program
903
945
  spin.stop();
904
946
  if (!releases.length) {
905
947
  console.log(` ${c.dim}No releases found.${c.reset}\n`);
906
- return;
948
+ process.exit(0);
907
949
  }
908
950
  const limit = parseInt(options.limit);
909
951
  const rows = releases.slice(0, limit).map((r) => ({
@@ -914,6 +956,7 @@ program
914
956
  Created: r.created_at ? new Date(r.created_at).toLocaleString() : "—",
915
957
  }));
916
958
  console.table(rows);
959
+ process.exit(0);
917
960
  });
918
961
  // ──────────────────────────────────────────────────────────────────────────────
919
962
  // edge-ota keygen
@@ -932,6 +975,7 @@ program
932
975
  console.log(` ${c.bold}public key${c.reset} ${c.dim}paste into dashboard → Settings → General${c.reset}`);
933
976
  console.log(sep);
934
977
  console.log(keys.publicKey);
978
+ process.exit(0);
935
979
  });
936
980
  await checkAndAutoUpdate();
937
981
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@renbostudios/edge-ota",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "preferGlobal": true,
5
5
  "description": "Zero-SDK, self-hostable OTA update CLI for Expo — push signed JS bundles to any EdgeOTA server",
6
6
  "license": "MIT",
7
7
  "author": "EdgeOTA Contributors",
8
- "homepage": "https://github.com/renbostudios/edge-ota",
8
+ "homepage": "https://github.com/renbostudios/edge-ota-cli",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/renbostudios/edge-ota.git"
11
+ "url": "https://github.com/renbostudios/edge-ota-cli.git"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/renbostudios/edge-ota/issues"
14
+ "url": "https://github.com/renbostudios/edge-ota-cli/issues"
15
15
  },
16
16
  "keywords": [
17
17
  "expo",