@renbostudios/edge-ota 0.4.2 → 0.4.3

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 +159 -355
  2. package/dist/index.js +72 -32
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,307 +1,200 @@
1
1
  <p align="center">
2
- <img src="https://ota.renbo.site/og.png" alt="Edge OTA" width="100%">
2
+ <a href="https://ota.renbo.site">
3
+ <img src="https://raw.githubusercontent.com/renbostudios/edge-ota/main/assets/banner-hero.png" alt="EdgeOTA — Zero-SDK OTA Engine for Expo" width="100%">
4
+ </a>
3
5
  </p>
4
6
 
5
- <h1 align="center">
6
- <code>edge-ota</code>
7
- </h1>
7
+ <h1 align="center">@renbostudios/edge-ota</h1>
8
8
 
9
9
  <p align="center">
10
- <strong>Zero-SDK, self-hostable OTA update CLI for Expo React Native</strong><br>
11
- Push signed JavaScript bundles to any EdgeOTA server.
10
+ <strong>Zero-SDK, serverless, self-hostable OTA update CLI for Expo & React Native</strong><br>
11
+ Push cryptographically signed JS bundles to Cloudflare Edge or your own VPS with zero egress fees.
12
12
  </p>
13
13
 
14
14
  <p align="center">
15
- <a href="https://www.npmjs.com/package/@renbostudios/edge-ota"><img src="https://img.shields.io/npm/v/@renbostudios/edge-ota?style=flat-square&color=81C784" alt="npm version"></a>
16
- <a href="https://github.com/renbostudios/edge-ota-cli/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@renbostudios/edge-ota?style=flat-square&color=FFAA00" alt="license"></a>
17
- <a href="https://www.npmjs.com/package/@renbostudios/edge-ota"><img src="https://img.shields.io/npm/dm/@renbostudios/edge-ota?style=flat-square&color=81C784" alt="downloads"></a>
15
+ <a href="https://www.npmjs.com/package/@renbostudios/edge-ota"><img src="https://img.shields.io/npm/v/@renbostudios/edge-ota?style=flat-square&color=000000&labelColor=161616" alt="npm version"></a>
16
+ <a href="https://www.npmjs.com/package/@renbostudios/edge-ota"><img src="https://img.shields.io/npm/dm/@renbostudios/edge-ota?style=flat-square&color=000000&labelColor=161616" alt="npm downloads"></a>
17
+ <a href="https://github.com/renbostudios/edge-ota/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-000000?style=flat-square&labelColor=161616" alt="license"></a>
18
+ <a href="https://docs.expo.dev/technical-specs/expo-updates-0/"><img src="https://img.shields.io/badge/Protocol-Expo_v1-000000?style=flat-square&labelColor=161616&logo=expo&logoColor=white" alt="Expo Protocol v1"></a>
19
+ <a href="https://developers.cloudflare.com/r2/"><img src="https://img.shields.io/badge/Storage-Cloudflare_R2_(Zero_Egress)-000000?style=flat-square&labelColor=161616&logo=cloudflare&logoColor=white" alt="Cloudflare R2"></a>
20
+ <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-Strict_ESM-000000?style=flat-square&labelColor=161616&logo=typescript&logoColor=white" alt="TypeScript"></a>
18
21
  </p>
19
22
 
20
23
  <p align="center">
21
- <a href="https://github.com/renbostudios/edge-ota-cli">GitHub</a> · <a href="https://ota.renbo.site">Dashboard</a> · <a href="https://ota.renbo.site/docs">Docs</a> · <a href="https://pd1mvpk7lt.ufs.sh/f/6uDGWrRxa3ipmhByNjc2TlHph0oA4dsWRFUuqZMan18Gwb7g">Video Tutorial</a>
24
+ <a href="https://ota.renbo.site"><strong>Cloud Console</strong></a> ·
25
+ <a href="https://ota.renbo.site/docs"><strong>Documentation</strong></a> ·
26
+ <a href="#video-walkthrough"><strong>Video Walkthrough</strong></a> ·
27
+ <a href="https://www.npmjs.com/package/@renbostudios/edge-ota"><strong>npm Package</strong></a> ·
28
+ <a href="https://github.com/renbostudios/edge-ota/issues"><strong>Report Issue</strong></a>
22
29
  </p>
23
30
 
24
31
  ---
25
32
 
26
- <br>
27
-
28
- ```
29
- ╔══════════════════════════════════════════════════════════════════╗
30
- ║ ║
31
- ║ ┌─┐┬┌─┬┌┬┐┌─┐┌─┐┬ ┬┌─┐┌┐┌┌┬┐┌─┐ ║
32
- ║ └─┐││ ┤│││├┤ │ ┬├─┤├─┤│││ │ ┤ └─┐ ║
33
- ║ └─┘┴└─┴┴ ┴└ └─┘┴ ┴┴ ┴┘└┘─┴┘└─┘└─┘ ║
34
- ║ ║
35
- ║ Push signed OTA bundles to the edge. ║
36
- ║ Zero SDK. Zero egress. Full control. ║
37
- ║ ║
38
- ╚══════════════════════════════════════════════════════════════════╝
39
- ```
40
-
41
- <br>
42
-
43
- ## ⚡ Quick Start
44
-
45
- ```
46
- ┌──────────────────────────────────────────────────────────────┐
47
- │ STEP 01 │
48
- │ Install the CLI globally │
49
- └──────────────────────────────────────────────────────────────┘
50
- ```
51
-
52
- ```bash
53
- npm install -g @renbostudios/edge-ota
54
- ```
55
-
56
- ```
57
- ┌──────────────────────────────────────────────────────────────┐
58
- │ STEP 02 │
59
- │ Log in to your EdgeOTA account │
60
- └──────────────────────────────────────────────────────────────┘
61
- ```
62
-
63
- ```bash
64
- edge-ota login
65
- ```
66
-
67
- ```
68
- ┌──────────────────────────────────────────────────────────────┐
69
- │ STEP 03 │
70
- │ Register your project & generate signing keys │
71
- └──────────────────────────────────────────────────────────────┘
72
- ```
73
-
74
- ```bash
75
- edge-ota init
76
- ```
33
+ ## Highlights
77
34
 
78
- ```
79
- ┌──────────────────────────────────────────────────────────────┐
80
- │ STEP 04 │
81
- │ ⚠ REBUILD YOUR NATIVE APP (required!) │
82
- └──────────────────────────────────────────────────────────────┘
83
- ```
84
-
85
- ```bash
86
- npx expo prebuild --clean
87
- npx expo run:ios
88
- npx expo run:android
89
- ```
35
+ - **Zero-SDK Footprint**: Powered natively by the official `expo-updates` library already inside your Expo app. No proprietary runtime agents, binary bloat, or invasive hooks.
36
+ - **Zero Egress Bandwidth Fees**: Store and distribute bundles via Cloudflare R2 and Workers. High-concurrency global cache invalidation without per-gigabyte bandwidth penalties.
37
+ - **Hardware-Grade Code Signing**: Every Hermes bundle is signed locally using ECDSA P-256 (`prime256v1`). Your private key is stored securely in `~/.config/edge-ota/keys/` and never leaves your machine.
38
+ - **Multi-Runtime Matrix Updates**: Target multiple native app versions simultaneously in a single command (`--runtime 1.0.0,1.0.1,1.0.2`).
39
+ - **100% Protocol v1 Compliant**: Produces standard RFC multipart/mixed manifests and Structured Field Values (`SFV`) signature headers expected by Expo clients.
40
+ - **True Open-Core & Self-Hostable**: Deploy on your own Cloudflare account (`apps/worker-oss`) or Docker VPS (`apps/server-node`), or connect directly to the hosted cloud at [ota.renbo.site](https://ota.renbo.site).
90
41
 
91
- ```
92
- ┌──────────────────────────────────────────────────────────────┐
93
- │ STEP 05 │
94
- │ Push an OTA update 🚀 │
95
- └──────────────────────────────────────────────────────────────┘
96
- ```
42
+ ---
97
43
 
98
- ```bash
99
- edge-ota push
100
- ```
44
+ ## Video Walkthrough
101
45
 
102
- > **📹 Prefer video?** Watch the full setup tutorial below:
46
+ Watch the 45-second terminal walkthrough demonstrating `edge-ota init`, project registration, and publishing an instant OTA hotfix:
103
47
 
104
48
  <p align="center">
105
- <video src="https://pd1mvpk7lt.ufs.sh/f/6uDGWrRxa3ipmhByNjc2TlHph0oA4dsWRFUuqZMan18Gwb7g" controls width="100%" style="max-width: 640px; border: 1px solid #333; border-radius: 4px; background: #000;">
106
- Your browser does not support the video tag.
49
+ <video src="https://github.com/user-attachments/assets/476fd099-6834-45a5-b11b-130b3b8fab02" autoplay loop muted playsinline controls width="100%" style="max-width: 720px; border-radius: 8px; border: 1px solid #333333;">
107
50
  </video>
108
51
  </p>
109
52
 
110
- <br>
53
+ ### Live Terminal Flow
111
54
 
112
- ---
55
+ <p align="center">
56
+ <img src="https://raw.githubusercontent.com/renbostudios/edge-ota/main/assets/demo.gif" alt="EdgeOTA Live Terminal Demo" width="100%" style="max-width: 720px; border-radius: 8px; border: 1px solid #27272a;">
57
+ </p>
113
58
 
114
- <br>
59
+ ---
115
60
 
116
- ## 📦 Commands
61
+ ## 60-Second Quick Start
117
62
 
118
- ### `edge-ota login`
63
+ ### 1. Install the CLI
119
64
 
120
- Authenticate with your EdgeOTA account. Credentials are stored globally — no per-project `.env` files needed.
65
+ Install the official EdgeOTA CLI globally:
121
66
 
122
67
  ```bash
123
- edge-ota login # default server
124
- edge-ota login --server https://my-server.com # custom server
125
- ```
126
-
127
- ```
128
- ┌──────────────────────────────────────────────┐
129
- │ 📁 Stored at: │
130
- │ ~/.config/edge-ota/config.json │
131
- │ │
132
- │ Contains: auth token, email, server URL │
133
- └──────────────────────────────────────────────┘
68
+ npm install -g @renbostudios/edge-ota
134
69
  ```
135
70
 
136
- ---
137
-
138
- ### `edge-ota logout`
71
+ ### 2. Authenticate
139
72
 
140
- Remove stored credentials from this machine.
73
+ Log in to your account (or specify your self-hosted server via `--server`):
141
74
 
142
75
  ```bash
143
- edge-ota logout
76
+ edge-ota login
144
77
  ```
145
78
 
146
- ---
147
-
148
- ### `edge-ota init`
79
+ ### 3. Initialize Your Project
149
80
 
150
- The all-in-one setup command. Registers your project, generates ECDSA P-256 signing keys, and configures `app.json`.
81
+ From the root of your Expo React Native project, run:
151
82
 
152
83
  ```bash
153
- edge-ota init # interactive setup
154
- edge-ota init --server https://my-server.com # override server
84
+ edge-ota init
155
85
  ```
156
86
 
157
- **What happens under the hood:**
158
-
159
- ```
160
- ① Fetches your existing projects from the server
161
- ② Lets you select an existing project or create a new one
162
- ③ Generates an ECDSA P-256 key pair
163
- ④ Registers the project with your public key
164
- ⑤ Saves private key → ~/.config/edge-ota/keys/<projectId>.key
165
- ⑥ Updates app.json with server URL, public key, and headers
166
- ```
87
+ `edge-ota init` automatically:
88
+ 1. Generates a local cryptographic **ECDSA P-256** key pair.
89
+ 2. Registers your project and securely records the public key with the backend.
90
+ 3. Automatically patches `app.json` with the manifest endpoint and security metadata.
167
91
 
168
- > ⚠ **After `init`, you MUST rebuild your native app.** See [Rebuild Required](#-rebuild-required) below.
92
+ ### 4. Rebuild Native Binary (Required Once)
169
93
 
170
- ---
171
-
172
- ### `edge-ota push`
173
-
174
- Export your Expo bundle and publish an OTA update.
94
+ The `expo-updates` native client embeds the update server URL into your native binary at compile time:
175
95
 
176
96
  ```bash
177
- edge-ota push # all platforms, production channel
178
- edge-ota push --channel staging # deploy to staging
179
- edge-ota push --runtime 1.0.0 # target specific runtime version
180
- edge-ota push --runtime 1.0.5,1.0.6,1.0.7 # target multiple runtime versions (matrix deployment)
181
- edge-ota push --platform ios # iOS only
182
- edge-ota push --platform android # Android only
183
- edge-ota push --skip-export # use existing ./dist
184
- edge-ota push --dry-run # build & sign, don't upload
185
- ```
186
-
187
- **What happens under the hood:**
97
+ # Clean prebuild and compile locally
98
+ npx expo prebuild --clean
99
+ npx expo run:ios
100
+ npx expo run:android
188
101
 
102
+ # Or build in the cloud with EAS
103
+ eas build --profile production
189
104
  ```
190
- ① Runs `npx expo export` to create the JS bundle
191
- ② Collects all assets from ./dist
192
- ③ For each platform (ios/android):
193
- → Locates the platform-specific bundle
194
- → Signs the payload with your ECDSA private key
195
- → Uploads bundle + signature to the server
196
- ④ Server verifies the signature before storing
197
- ```
198
-
199
- **How it reads your config:**
200
105
 
201
- | Source | What it reads |
202
- |---|---|
203
- | `expo.updates.url` | Server URL + Project ID |
204
- | `expo.runtimeVersion` | Runtime version (string or policy) |
205
- | `~/.config/edge-ota/keys/<id>.key` | ECDSA private key |
206
- | `expo.extra.edgeOtaServer` | Server URL override |
106
+ > [!IMPORTANT]
107
+ > Whenever `expo.updates.url` or `runtimeVersion` changes, you must run `prebuild` or generate a new native build so native code knows where to fetch manifests.
207
108
 
208
- ---
109
+ ### 5. Publish an OTA Update
209
110
 
210
- ### `edge-ota status`
211
-
212
- List recent deployments for the current project.
111
+ Push your updated JavaScript code and assets live:
213
112
 
214
113
  ```bash
215
- edge-ota status
216
- edge-ota status --limit 20 # show more releases
114
+ edge-ota push --channel production
217
115
  ```
218
116
 
219
- ---
117
+ Your app silently downloads the update in the background and applies it seamlessly on the next cold launch.
220
118
 
221
- ### `edge-ota keygen`
119
+ ---
222
120
 
223
- Generate a new ECDSA P-256 key pair and print it to stdout. Does not write any files.
121
+ ## CLI Command Reference
224
122
 
225
123
  ```bash
226
- edge-ota keygen
124
+ edge-ota <command> [options]
227
125
  ```
228
126
 
229
- Useful for manual key rotation or generating keys for server-side signing.
127
+ | Command | Description | Common Flags |
128
+ | :--- | :--- | :--- |
129
+ | `edge-ota login` | Authenticate and save credentials globally | `--server <url>`, `--email <email>` |
130
+ | `edge-ota logout` | Clear stored authentication session | — |
131
+ | `edge-ota init` | Register project, generate keys, & configure `app.json` | `--server <url>` |
132
+ | `edge-ota push` | Export Hermes bundle, sign locally, and publish | `--channel <name>`, `--runtime <ver>`, `--platform <p>`, `--dry-run` |
133
+ | `edge-ota status` | Inspect recent releases, channels, and deployments | `--limit <n>`, `--channel <name>` |
134
+ | `edge-ota keygen` | Generate a standalone ECDSA P-256 key pair to stdout | — |
230
135
 
231
- <br>
136
+ ### Advanced Workflows
232
137
 
233
- ---
138
+ #### Multi-Runtime Matrix Deployment
139
+ Deploy a single JS patch simultaneously across multiple released versions of your native app:
234
140
 
235
- <br>
236
-
237
- ## 🔄 Rebuild Required
141
+ ```bash
142
+ edge-ota push --runtime 1.0.0,1.0.1,1.0.2 --channel production
143
+ ```
238
144
 
239
- After running `edge-ota init`, you **must** rebuild your native app. The `expo-updates` native module reads its config from the **native binary**, not from `app.json` at runtime.
145
+ #### Platform Targeting
146
+ Deploy an update exclusively to iOS or Android devices:
240
147
 
241
148
  ```bash
242
- npx expo prebuild --clean
243
- npx expo run:ios
244
- npx expo run:android
245
-
246
- # or via EAS
247
- eas build --profile production
149
+ edge-ota push --platform ios --channel production
248
150
  ```
249
151
 
250
- ```
251
- ╔══════════════════════════════════════════════════════════════════╗
252
- ║ ⚠ SKIP THIS STEP AND YOU'LL SEE: ║
253
- ║ ║
254
- ║ "Failed to check for update" ║
255
- ║ ║
256
- ║ The native module has no valid server URL to reach because ║
257
- ║ it was never embedded in the binary. ║
258
- ╚══════════════════════════════════════════════════════════════════╝
259
- ```
152
+ #### Dry Run & CI/CD Pipelines
153
+ Export and sign the bundle without uploading to test your pipeline:
260
154
 
261
- You must also rebuild after any change to `expo.updates.url` or `expo.runtimeVersion` in `app.json`.
155
+ ```bash
156
+ # Validate export and ECDSA signature locally
157
+ edge-ota push --dry-run
262
158
 
263
- <br>
159
+ # Upload pre-exported ./dist bundle from previous CI build step
160
+ edge-ota push --skip-export --channel production
161
+ ```
264
162
 
265
163
  ---
266
164
 
267
- <br>
268
-
269
- ## 🔁 How It Works
165
+ ## Architecture & Data Flow
270
166
 
271
167
  ```
272
- Developer Machine EdgeOTA Server User's Phone
273
- ───────────────── ────────────── ────────────
274
-
275
- edge-ota push ──────────────► POST /api/updates
276
- (ECDSA verified ✓)
277
- (stores bundle)
278
- ◄──────────── GET /api/updates
279
- (expo-updates SDK)
280
- ────────────► signed manifest
281
- ◄──────────── GET /api/assets/:hash
282
- (downloads bundle)
283
- (reloads app ✓)
284
- ```
168
+ Developer Workstation EdgeOTA Backend User Device
169
+ ───────────────────── ─────────────── ───────────
285
170
 
286
- 1. `edge-ota push` runs `expo export` and uploads the Hermes bundle + ECDSA signature
287
- 2. The server **verifies** the ECDSA P-256 signature before storing anything
288
- 3. When users open the app, `expo-updates` checks for a new manifest
289
- 4. The server responds with a signed multipart manifest (Expo Updates Protocol v1)
290
- 5. `expo-updates` downloads and applies the update automatically
171
+ edge-ota push ────────────────► POST /api/updates
172
+ (ECDSA verified)
173
+ (stores bundle & assets in R2)
174
+ ◄─────────────── GET /api/updates
175
+ (expo-updates client)
176
+ ────────────────► Signed Multipart Manifest
177
+ ◄─────────────── GET /api/assets/:hash
178
+ (downloads bundle)
179
+ (app reloads silently)
180
+ ```
291
181
 
292
- <br>
182
+ 1. **Local Compilation & Signing**: `edge-ota push` runs `expo export` to produce Hermes bytecode bundles, hashes static assets with SHA-256, and signs the bundle manifest locally with your private ECDSA P-256 key.
183
+ 2. **Edge Verification**: The EdgeOTA API validates the cryptographic signature against the registered public key and confirms all asset hashes before accepting the release.
184
+ 3. **Multipart Manifest Protocol**: When client devices check for updates, the EdgeOTA backend generates an official Expo Updates Protocol v1 `multipart/mixed` response with `expo-signature` headers.
185
+ 4. **Silent Edge Delivery**: The client downloads diffed assets directly from Cloudflare R2 edge locations with zero latency and applies the update on next launch.
293
186
 
294
187
  ---
295
188
 
296
- <br>
189
+ ## Configuration (`app.json`)
297
190
 
298
- ## ⚙ What `init` Writes to `app.json`
191
+ `edge-ota init` populates your `app.json` with standard Expo configuration:
299
192
 
300
193
  ```json
301
194
  {
302
195
  "expo": {
303
196
  "updates": {
304
- "url": "https://your-server.com/api/projects/<projectId>/updates",
197
+ "url": "https://ota.renbo.site/api/projects/<projectId>/updates",
305
198
  "checkAutomatically": "ON_LOAD",
306
199
  "fallbackToCacheTimeout": 30000,
307
200
  "requestHeaders": {
@@ -310,213 +203,124 @@ You must also rebuild after any change to `expo.updates.url` or `expo.runtimeVer
310
203
  },
311
204
  "runtimeVersion": "1.0.0",
312
205
  "extra": {
313
- "edgeOtaServer": "https://your-server.com",
314
- "edgeOtaPublicKey": "-----BEGIN PUBLIC KEY-----\n..."
206
+ "edgeOtaServer": "https://ota.renbo.site",
207
+ "edgeOtaPublicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...\n-----END PUBLIC KEY-----"
315
208
  }
316
209
  }
317
210
  }
318
211
  ```
319
212
 
320
- > `runtimeVersion` is **not modified** by the CLI — it must already exist in your `app.json`. Supports both static strings and policy objects:
321
-
322
- ```json
323
- "runtimeVersion": "1.0.0"
324
- ```
325
- ```json
326
- "runtimeVersion": { "policy": "appVersion" }
327
- ```
328
-
329
- <br>
330
-
331
213
  ---
332
214
 
333
- <br>
334
-
335
- ## 🗂 Global Storage
336
-
337
- All credentials and keys are stored **outside** your project directory:
338
-
339
- ```
340
- ~/.config/edge-ota/
341
- ├── config.json ← auth token, email, server URL
342
- ├── keys/
343
- │ └── <projectId>.key ← ECDSA private key (per-project)
344
- └── update-check.json ← auto-update check timestamp
345
- ```
346
-
347
- ```
348
- ┌──────────────────────────────────────────────────────────────┐
349
- │ 🔒 Nothing sensitive is ever stored in your project repo. │
350
- └──────────────────────────────────────────────────────────────┘
351
- ```
352
-
353
- <br>
354
-
355
- ---
356
-
357
- <br>
358
-
359
- ## 🧪 Environment Variables
360
-
361
- | Variable | Purpose |
362
- |---|---|
363
- | `EDGE_OTA_TOKEN` | Override the auth token (for CI/CD pipelines) |
364
- | `EDGE_OTA_UPDATING` | Internal — prevents recursive auto-updates |
365
-
366
- <br>
367
-
368
- ---
369
-
370
- <br>
371
-
372
- ## 🔄 Auto-Update
373
-
374
- The CLI checks for newer versions on npm every 15 minutes. If a newer version is found, it auto-installs globally. This happens silently in the background and never blocks command execution.
215
+ ## Self-Hosting Options
375
216
 
376
- To disable: set `EDGE_OTA_UPDATING=true` in your environment.
217
+ EdgeOTA is 100% open-core. You can host the entire platform on your own cloud infrastructure:
377
218
 
378
- <br>
219
+ ### Option 1: Cloudflare Worker + D1 + R2 (Recommended)
379
220
 
380
- ---
221
+ Deploy a global serverless OTA engine with zero egress fees:
381
222
 
382
- <br>
223
+ ```bash
224
+ cd apps/worker-oss
225
+ npm install
383
226
 
384
- ## 🔐 Security
227
+ # Create Cloudflare D1 database and R2 storage bucket
228
+ npx wrangler d1 create edge-ota-db
229
+ npx wrangler r2 bucket create edge-ota-assets
385
230
 
231
+ # Deploy to Cloudflare Edge
232
+ npx wrangler deploy
386
233
  ```
387
- ┌──────────────────────────────────────────────────────────────┐
388
- │ │
389
- │ ✅ ECDSA P-256 signing on every bundle upload │
390
- │ ✅ Server rejects invalid or missing signatures │
391
- │ ✅ Private keys stored with 0600 permissions │
392
- │ ✅ Session tokens expire after 24 hours │
393
- │ ✅ Private keys never leave your machine │
394
- │ │
395
- └──────────────────────────────────────────────────────────────┘
396
- ```
397
-
398
- <br>
399
-
400
- ---
401
-
402
- <br>
403
234
 
404
- ## 🖥 Server Setup
235
+ ### Option 2: Docker Compose (Node.js + SQLite / Postgres)
405
236
 
406
- The EdgeOTA server is a standard Node.js/Express app. Self-host it or use the managed cloud service at [ota.renbo.site](https://ota.renbo.site).
407
-
408
- ### Docker
409
-
410
- ```bash
411
- docker pull ghcr.io/itskodaaa/edge-ota-backend:latest
412
- docker run -p 3000:3000 -e DATABASE_URL=... ghcr.io/itskodaaa/edge-ota-backend
413
- ```
414
-
415
- ### Docker Compose
237
+ Deploy anywhere with a single container stack:
416
238
 
417
239
  ```bash
418
- git clone https://github.com/renbostudios/edge-ota-cli
240
+ git clone https://github.com/renbostudios/edge-ota.git
419
241
  cd edge-ota
420
- docker-compose up -d
242
+ docker compose up -d
421
243
  ```
422
244
 
423
- ### Manual
245
+ The Node.js server will boot on port `3000` with automated database migrations and local asset storage.
424
246
 
425
- ```bash
426
- git clone https://github.com/renbostudios/edge-ota-cli
427
- cd edge-ota
428
- pnpm install
429
- pnpm --filter @renbostudios/edge-ota-server-node dev
430
- ```
247
+ ---
431
248
 
432
- <br>
249
+ ## Security Model
433
250
 
434
- ---
251
+ - **ECDSA P-256 Code Signing**: Every bundle is cryptographically verified before serving. Tampered bundles or unauthorized uploads are rejected by the backend.
252
+ - **Local Key Storage**: Private keys are stored on your local disk at `~/.config/edge-ota/keys/<projectId>.key` with restrictive `0600` file permissions. Private keys are never uploaded to EdgeOTA servers.
253
+ - **Structured Field Values**: Signatures are serialized according to RFC 8941 Structured Field Values (`expo-signature="sig=..."`).
435
254
 
436
- <br>
255
+ ---
437
256
 
438
- ## 🐛 Troubleshooting
257
+ ## Troubleshooting
439
258
 
440
259
  <details>
441
260
  <summary><strong>"Failed to check for update" on app launch</strong></summary>
442
261
 
443
262
  <br>
444
263
 
445
- The `expo-updates` native module can't reach your server because the URL was never embedded in the binary.
264
+ The `expo-updates` native module cannot contact your update server because the server URL was not embedded in the native binary.
446
265
 
266
+ Run a clean native build:
447
267
  ```bash
448
268
  npx expo prebuild --clean
449
269
  npx expo run:ios
450
270
  npx expo run:android
451
271
  ```
452
-
453
272
  </details>
454
273
 
455
274
  <details>
456
- <summary><strong>Updates work on iOS but not Android</strong></summary>
275
+ <summary><strong>Updates apply on iOS but not on Android</strong></summary>
457
276
 
458
277
  <br>
459
278
 
460
- The Android native project may still have the default Expo Updates URL. Regenerate and rebuild:
279
+ Ensure `android/app/src/main/AndroidManifest.xml` contains the `EXPO_UPDATE_URL` meta-data entry. If testing against a local dev server, allow cleartext HTTP traffic:
461
280
 
281
+ ```xml
282
+ android:usesCleartextTraffic="true"
283
+ ```
284
+ And forward the port over ADB:
462
285
  ```bash
463
- npx expo prebuild --clean
464
- npx expo run:android
286
+ adb reverse tcp:3000 tcp:3000
465
287
  ```
466
-
467
- Verify by checking `android/app/src/main/AndroidManifest.xml` for the `EXPO_UPDATE_URL` meta-data entry.
468
-
469
288
  </details>
470
289
 
471
290
  <details>
472
- <summary><strong>Updates stopped working after a version bump</strong></summary>
291
+ <summary><strong>"No signing key found for project" during push</strong></summary>
473
292
 
474
293
  <br>
475
294
 
476
- 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.
477
-
295
+ Run `edge-ota init` in your project root to generate and register a key pair, or run `edge-ota keygen` to generate a key manually.
478
296
  </details>
479
297
 
480
298
  <details>
481
- <summary><strong>"No signing key found" error on push</strong></summary>
299
+ <summary><strong>Switching between Cloud and Self-Hosted</strong></summary>
482
300
 
483
301
  <br>
484
302
 
485
- Run `edge-ota init` to generate keys, or use `edge-ota keygen` to generate a new pair and register it manually.
486
-
303
+ Because EdgeOTA uses the standard Expo Updates Protocol v1, there is zero vendor lock-in. To migrate between EdgeOTA Cloud, self-hosted Cloudflare Workers, or custom servers, simply update `expo.updates.url` in `app.json` and run `npx expo prebuild --clean`.
487
304
  </details>
488
305
 
489
- <br>
490
-
491
306
  ---
492
307
 
493
- <br>
494
-
495
- ## 📂 Project Structure
308
+ ## Monorepo Topology
496
309
 
497
310
  ```
498
- edge-ota-cli/
499
- ├── apps/
500
- │ ├── cli/ ← CLI tool (you are here)
501
- │ ├── server-node/ ← Express + TypeScript API
502
- │ ├── worker-oss/ ← Cloudflare Worker (open source)
503
- │ ├── dashboard/ ← SvelteKit frontend
504
- │ └── core/ ← Shared crypto/manifest logic
505
- └── packages/
506
- └── core/ ← @renbostudios/edge-ota-core
311
+ edge-ota/
312
+ ├── apps/
313
+ │ ├── cli/ → @renbostudios/edge-ota CLI (npm distribution)
314
+ │ ├── server-node/ → Self-hosted Express + SQLite/Postgres API
315
+ │ └── worker-oss/ → Serverless Cloudflare Worker + D1 + R2 engine
316
+ ├── packages/
317
+ │ └── core/ → ECDSA P-256 crypto & Expo Updates Protocol v1 manifests
318
+ └── assets/ → Banners, terminal demos, and walkthrough previews
507
319
  ```
508
320
 
509
- <br>
510
-
511
321
  ---
512
322
 
513
- <br>
514
-
515
- <p align="center">
516
- <strong>Built by</strong><br>
517
- <a href="https://renbostudios.com">Renbo Studios</a> — Mobile App Development Studio
518
- </p>
519
-
520
323
  <p align="center">
521
- <sub>MIT License · <code>@renbostudios/edge-ota</code></sub>
324
+ Built with care by <a href="https://renbostudios.com">Renbo Studios</a> & open-source contributors.<br>
325
+ <sub>MIT License · Open Source</sub>
522
326
  </p>
package/dist/index.js CHANGED
@@ -345,36 +345,37 @@ function readAppJson(cwd) {
345
345
  process.exit(1);
346
346
  }
347
347
  const expo = data?.expo ?? {};
348
- let runtimeVersion = expo.runtimeVersion;
349
- if (!runtimeVersion) {
348
+ let runtimeVersion;
349
+ if (typeof expo.runtimeVersion === "string") {
350
+ runtimeVersion = expo.runtimeVersion;
351
+ } else if (typeof expo.runtimeVersion === "object" && expo.runtimeVersion !== null && expo.runtimeVersion.policy === "appVersion") {
352
+ if (typeof expo.version === "string") {
353
+ runtimeVersion = expo.version;
354
+ } else {
355
+ printErrorBox(
356
+ "Missing expo.version in app.json",
357
+ `"expo.runtimeVersion.policy" is set to "appVersion", but "expo.version" is not defined in app.json.`,
358
+ `Add ${c.cyan}"version": "1.0.0"${c.reset} to your ${c.bold}app.json${c.reset} under ${c.dim}"expo"${c.reset}.`
359
+ );
360
+ process.exit(1);
361
+ }
362
+ } else if (!expo.runtimeVersion) {
350
363
  printErrorBox(
351
364
  "Missing expo.runtimeVersion in app.json",
352
- "Expo Updates requires a runtimeVersion to ensure native compatibility between the app binary and JS bundle.",
365
+ `No "runtimeVersion" field found under "expo" in app.json.`,
353
366
  [
354
367
  `Add a fixed version string in ${c.bold}app.json${c.reset} under ${c.dim}"expo"${c.reset}: ${c.cyan}"runtimeVersion": "1.0.0"${c.reset}`,
355
- `Or use an Expo policy helper: ${c.cyan}"runtimeVersion": { "policy": "appVersion" }${c.reset}`
368
+ `Or use the appVersion policy: ${c.cyan}"runtimeVersion": { "policy": "appVersion" }${c.reset}`
356
369
  ]
357
370
  );
358
371
  process.exit(1);
359
- }
360
- if (typeof runtimeVersion === "object" && runtimeVersion !== null) {
361
- const policy = runtimeVersion.policy;
362
- if (policy === "appVersion") {
363
- runtimeVersion = expo.version;
364
- } else if (policy === "sdkVersion") {
365
- runtimeVersion = expo.sdkVersion;
366
- } else {
367
- runtimeVersion = expo.version || expo.sdkVersion;
368
- }
369
- }
370
- if (!runtimeVersion || typeof runtimeVersion !== "string") {
372
+ } else {
371
373
  printErrorBox(
372
- "Could not resolve runtimeVersion",
374
+ "Unsupported expo.runtimeVersion in app.json",
373
375
  `expo.runtimeVersion in app.json resolved to: ${JSON.stringify(runtimeVersion)}`,
374
376
  [
375
- `If using ${c.dim}"policy": "appVersion"${c.reset}, make sure ${c.bold}"expo.version"${c.reset} (e.g. "1.0.0") is defined in app.json.`,
376
- `If using ${c.dim}"policy": "sdkVersion"${c.reset}, make sure ${c.bold}"expo.sdkVersion"${c.reset} (e.g. "52.0.0") is defined.`,
377
- `Or simply specify a static string: ${c.cyan}"runtimeVersion": "1.0.0"${c.reset}`
377
+ `EdgeOTA requires either a string (e.g. "1.0.0") or { "policy": "appVersion" }.`,
378
+ `If using ${c.dim}"policy": "appVersion"${c.reset}, make sure ${c.bold}"expo.version"${c.reset} (e.g. "1.0.0") is defined in app.json.`
378
379
  ]
379
380
  );
380
381
  process.exit(1);
@@ -414,7 +415,13 @@ function readAppJson(cwd) {
414
415
  if (projectServer) {
415
416
  serverUrl = projectServer.replace(/\/$/, "");
416
417
  }
417
- return { serverUrl, projectId, runtimeVersion, publicKey: expo?.extra?.edgeOtaPublicKey };
418
+ return {
419
+ serverUrl,
420
+ projectId,
421
+ runtimeVersion,
422
+ publicKey: expo?.extra?.edgeOtaPublicKey,
423
+ expoConfig: expo
424
+ };
418
425
  }
419
426
  function updateAppJson(cwd, serverUrl, projectId, publicKey) {
420
427
  const appJsonPath = path.resolve(cwd, "app.json");
@@ -569,16 +576,42 @@ async function hashFile(filePath) {
569
576
  }
570
577
  async function collectAssets(distDir) {
571
578
  const entries = [];
579
+ const extMap = {};
580
+ const metadataPath = path.join(distDir, "metadata.json");
581
+ if (fs.existsSync(metadataPath)) {
582
+ try {
583
+ const meta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
584
+ if (meta.fileMetadata) {
585
+ for (const p of Object.keys(meta.fileMetadata)) {
586
+ const pMeta = meta.fileMetadata[p];
587
+ if (Array.isArray(pMeta.assets)) {
588
+ for (const a of pMeta.assets) {
589
+ if (a.path && a.ext) {
590
+ const normPath = a.path.replace(/\\/g, "/");
591
+ extMap[normPath] = a.ext.startsWith(".") ? a.ext.toLowerCase() : `.${a.ext.toLowerCase()}`;
592
+ }
593
+ }
594
+ }
595
+ }
596
+ }
597
+ } catch {
598
+ }
599
+ }
572
600
  function walk(dir) {
573
601
  for (const name of fs.readdirSync(dir)) {
574
602
  const full = path.join(dir, name);
575
603
  if (fs.statSync(full).isDirectory()) {
576
604
  walk(full);
577
605
  } else {
578
- const ext = path.extname(name).toLowerCase();
606
+ const relKey = path.relative(distDir, full).replace(/\\/g, "/");
607
+ let ext = path.extname(name).toLowerCase();
608
+ if (!ext && extMap[relKey]) {
609
+ ext = extMap[relKey];
610
+ }
579
611
  entries.push({
580
612
  localPath: full,
581
- key: path.relative(distDir, full).replace(/\\/g, "/"),
613
+ key: relKey,
614
+ fileExtension: ext,
582
615
  contentType: CONTENT_TYPES[ext] || "application/octet-stream",
583
616
  hash: ""
584
617
  });
@@ -606,9 +639,10 @@ function findBundle(distDir, platform) {
606
639
  return null;
607
640
  }
608
641
  function resolveToken() {
642
+ if (process.env.EDGE_OTA_TOKEN) return process.env.EDGE_OTA_TOKEN;
609
643
  const cfg = loadGlobalConfig();
610
644
  if (cfg?.token) return cfg.token;
611
- return process.env.EDGE_OTA_TOKEN || null;
645
+ return null;
612
646
  }
613
647
  var program = new Command();
614
648
  program.name("edge-ota").description("Zero-SDK, self-hostable OTA update platform for Expo \u2014 by Renbo Studios").version(packageVersion);
@@ -622,7 +656,7 @@ ${c.bold}Quick Start:${c.reset}
622
656
  ${c.bold}Documentation & Support:${c.reset}
623
657
  Website: https://ota.renbo.site
624
658
  Studio: https://renbostudios.com
625
- GitHub: https://github.com/renbostudios/edge-ota-cli
659
+ GitHub: https://github.com/renbostudios/edge-ota
626
660
  `);
627
661
  program.action(() => {
628
662
  printBanner();
@@ -1061,14 +1095,20 @@ ${sep}`);
1061
1095
  platform,
1062
1096
  bundleHash,
1063
1097
  timestamp: Date.now(),
1064
- assets: mediaAssets.map((a) => ({
1065
- hash: a.hash,
1066
- key: a.key,
1067
- fileExtension: path.extname(a.localPath),
1068
- contentType: a.contentType
1069
- })),
1098
+ assets: mediaAssets.map((a) => {
1099
+ const ext = a.fileExtension || path.extname(a.localPath) || "";
1100
+ return {
1101
+ hash: a.hash,
1102
+ key: path.basename(a.key, ext),
1103
+ fileExtension: ext,
1104
+ contentType: a.contentType
1105
+ };
1106
+ }),
1070
1107
  assetCount: mediaAssets.length,
1071
- publicKey: appCfg.publicKey
1108
+ publicKey: appCfg.publicKey,
1109
+ extra: {
1110
+ expoClient: appCfg.expoConfig || {}
1111
+ }
1072
1112
  };
1073
1113
  const payloadStr = JSON.stringify(payloadObj);
1074
1114
  spin.start("signing bundle hash");
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@renbostudios/edge-ota",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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-cli",
8
+ "homepage": "https://ota.renbo.site",
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "https://github.com/itskodaaa/edge-ota.git",
14
+ "url": "https://github.com/renbostudios/edge-ota.git",
15
15
  "directory": "apps/cli"
16
16
  },
17
17
  "bugs": {
18
- "url": "https://github.com/renbostudios/edge-ota-cli/issues"
18
+ "url": "https://github.com/renbostudios/edge-ota/issues"
19
19
  },
20
20
  "keywords": [
21
21
  "expo",