@renbostudios/edge-ota 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -355
- package/dist/index.js +140 -37
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,307 +1,200 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
2
|
+
<a href="https://ota.renbo.dev">
|
|
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
|
|
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=
|
|
16
|
-
<a href="https://
|
|
17
|
-
<a href="https://
|
|
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://
|
|
24
|
+
<a href="https://ota.renbo.dev"><strong>Cloud Console</strong></a> ·
|
|
25
|
+
<a href="https://ota.renbo.dev/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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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.dev](https://ota.renbo.dev).
|
|
90
41
|
|
|
91
|
-
|
|
92
|
-
┌──────────────────────────────────────────────────────────────┐
|
|
93
|
-
│ STEP 05 │
|
|
94
|
-
│ Push an OTA update 🚀 │
|
|
95
|
-
└──────────────────────────────────────────────────────────────┘
|
|
96
|
-
```
|
|
42
|
+
---
|
|
97
43
|
|
|
98
|
-
|
|
99
|
-
edge-ota push
|
|
100
|
-
```
|
|
44
|
+
## Video Walkthrough
|
|
101
45
|
|
|
102
|
-
|
|
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://
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
---
|
|
115
60
|
|
|
116
|
-
##
|
|
61
|
+
## 60-Second Quick Start
|
|
117
62
|
|
|
118
|
-
###
|
|
63
|
+
### 1. Install the CLI
|
|
119
64
|
|
|
120
|
-
|
|
65
|
+
Install the official EdgeOTA CLI globally:
|
|
121
66
|
|
|
122
67
|
```bash
|
|
123
|
-
edge-ota
|
|
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
|
-
|
|
73
|
+
Log in to your account (or specify your self-hosted server via `--server`):
|
|
141
74
|
|
|
142
75
|
```bash
|
|
143
|
-
edge-ota
|
|
76
|
+
edge-ota login
|
|
144
77
|
```
|
|
145
78
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
### `edge-ota init`
|
|
79
|
+
### 3. Initialize Your Project
|
|
149
80
|
|
|
150
|
-
|
|
81
|
+
From the root of your Expo React Native project, run:
|
|
151
82
|
|
|
152
83
|
```bash
|
|
153
|
-
edge-ota init
|
|
154
|
-
edge-ota init --server https://my-server.com # override server
|
|
84
|
+
edge-ota init
|
|
155
85
|
```
|
|
156
86
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
119
|
+
---
|
|
222
120
|
|
|
223
|
-
|
|
121
|
+
## CLI Command Reference
|
|
224
122
|
|
|
225
123
|
```bash
|
|
226
|
-
edge-ota
|
|
124
|
+
edge-ota <command> [options]
|
|
227
125
|
```
|
|
228
126
|
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
141
|
+
```bash
|
|
142
|
+
edge-ota push --runtime 1.0.0,1.0.1,1.0.2 --channel production
|
|
143
|
+
```
|
|
238
144
|
|
|
239
|
-
|
|
145
|
+
#### Platform Targeting
|
|
146
|
+
Deploy an update exclusively to iOS or Android devices:
|
|
240
147
|
|
|
241
148
|
```bash
|
|
242
|
-
|
|
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
|
-
|
|
155
|
+
```bash
|
|
156
|
+
# Validate export and ECDSA signature locally
|
|
157
|
+
edge-ota push --dry-run
|
|
262
158
|
|
|
263
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
## 🔁 How It Works
|
|
165
|
+
## Architecture & Data Flow
|
|
270
166
|
|
|
271
167
|
```
|
|
272
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
+
## Configuration (`app.json`)
|
|
297
190
|
|
|
298
|
-
|
|
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://
|
|
197
|
+
"url": "https://ota.renbo.dev/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://
|
|
314
|
-
"edgeOtaPublicKey": "-----BEGIN PUBLIC KEY-----\n
|
|
206
|
+
"edgeOtaServer": "https://ota.renbo.dev",
|
|
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
|
-
|
|
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
|
-
|
|
217
|
+
EdgeOTA is 100% open-core. You can host the entire platform on your own cloud infrastructure:
|
|
377
218
|
|
|
378
|
-
|
|
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
|
-
|
|
223
|
+
```bash
|
|
224
|
+
cd apps/worker-oss
|
|
225
|
+
npm install
|
|
383
226
|
|
|
384
|
-
|
|
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
|
-
|
|
235
|
+
### Option 2: Docker Compose (Node.js + SQLite / Postgres)
|
|
405
236
|
|
|
406
|
-
|
|
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
|
|
240
|
+
git clone https://github.com/renbostudios/edge-ota.git
|
|
419
241
|
cd edge-ota
|
|
420
|
-
docker
|
|
242
|
+
docker compose up -d
|
|
421
243
|
```
|
|
422
244
|
|
|
423
|
-
|
|
245
|
+
The Node.js server will boot on port `3000` with automated database migrations and local asset storage.
|
|
424
246
|
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
+
---
|
|
437
256
|
|
|
438
|
-
##
|
|
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
|
|
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
|
|
275
|
+
<summary><strong>Updates apply on iOS but not on Android</strong></summary>
|
|
457
276
|
|
|
458
277
|
<br>
|
|
459
278
|
|
|
460
|
-
|
|
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
|
-
|
|
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>
|
|
291
|
+
<summary><strong>"No signing key found for project" during push</strong></summary>
|
|
473
292
|
|
|
474
293
|
<br>
|
|
475
294
|
|
|
476
|
-
|
|
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>
|
|
299
|
+
<summary><strong>Switching between Cloud and Self-Hosted</strong></summary>
|
|
482
300
|
|
|
483
301
|
<br>
|
|
484
302
|
|
|
485
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
## 📂 Project Structure
|
|
308
|
+
## Monorepo Topology
|
|
496
309
|
|
|
497
310
|
```
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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
|
@@ -309,6 +309,50 @@ async function checkAndAutoUpdate() {
|
|
|
309
309
|
} catch {
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
|
+
var MIGRATION_NOTICE_FILE = path.join(GLOBAL_CONFIG_DIR, "domain-migration-notice.json");
|
|
313
|
+
function getServerHost(serverUrl) {
|
|
314
|
+
try {
|
|
315
|
+
return new URL(serverUrl || DEFAULT_SERVER).host.toLowerCase();
|
|
316
|
+
} catch {
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function isLegacyEdgeOtaHost(serverUrl) {
|
|
321
|
+
const host = getServerHost(serverUrl);
|
|
322
|
+
return host === getServerHost(LEGACY_SERVER) || host.endsWith(".ota.renbo.site");
|
|
323
|
+
}
|
|
324
|
+
function maybeNotifyDomainMigration(serverUrl) {
|
|
325
|
+
try {
|
|
326
|
+
const legacy = isLegacyEdgeOtaHost(serverUrl);
|
|
327
|
+
if (!legacy) {
|
|
328
|
+
const now = Date.now();
|
|
329
|
+
let lastShown = 0;
|
|
330
|
+
try {
|
|
331
|
+
lastShown = JSON.parse(fs.readFileSync(MIGRATION_NOTICE_FILE, "utf-8")).lastShown || 0;
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
if (now - lastShown < 24 * 60 * 60 * 1e3) return;
|
|
335
|
+
fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
336
|
+
fs.writeFileSync(MIGRATION_NOTICE_FILE, JSON.stringify({ lastShown: now }));
|
|
337
|
+
}
|
|
338
|
+
const sep = `${c.dim}${"\u2500".repeat(56)}${c.reset}`;
|
|
339
|
+
console.log(`
|
|
340
|
+
${sep}`);
|
|
341
|
+
console.log(` ${c.yellow}\u25B2${c.reset} ${c.bold}EdgeOTA has moved to ${c.green}renbo.dev${c.reset}`);
|
|
342
|
+
if (legacy) {
|
|
343
|
+
console.log(` ${c.dim}saved server:${c.reset} ${c.red}${getServerHost(serverUrl)}${c.reset} ${c.dim}(legacy)${c.reset}`);
|
|
344
|
+
console.log(` ${c.dim}Switch with:${c.reset} ${c.cyan}edge-ota login${c.reset} ${c.dim}(new default: ${DEFAULT_SERVER})${c.reset}`);
|
|
345
|
+
} else {
|
|
346
|
+
console.log(` ${c.dim}dashboard:${c.reset} ${c.cyan}${DASHBOARD_URL}${c.reset}`);
|
|
347
|
+
console.log(` ${c.dim}api:${c.reset} ${c.cyan}${DEFAULT_SERVER}${c.reset}`);
|
|
348
|
+
}
|
|
349
|
+
console.log(` ${c.dim}The legacy API stays online \u2014 existing apps keep receiving updates.${c.reset}`);
|
|
350
|
+
console.log(` ${c.dim}Guide:${c.reset} ${c.cyan}${DASHBOARD_URL}/migration${c.reset}`);
|
|
351
|
+
console.log(`${sep}
|
|
352
|
+
`);
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
}
|
|
312
356
|
function printBanner() {
|
|
313
357
|
const sep = `${c.dim}${"\u2500".repeat(56)}${c.reset}`;
|
|
314
358
|
console.log(`
|
|
@@ -319,7 +363,9 @@ ${sep}`);
|
|
|
319
363
|
console.log(`${sep}
|
|
320
364
|
`);
|
|
321
365
|
}
|
|
322
|
-
var DEFAULT_SERVER = "https://api.ota.renbo.
|
|
366
|
+
var DEFAULT_SERVER = "https://api.ota.renbo.dev";
|
|
367
|
+
var LEGACY_SERVER = "https://api.ota.renbo.site";
|
|
368
|
+
var DASHBOARD_URL = "https://ota.renbo.dev";
|
|
323
369
|
function readAppJson(cwd) {
|
|
324
370
|
const appJsonPath = path.resolve(cwd, "app.json");
|
|
325
371
|
if (!fs.existsSync(appJsonPath)) {
|
|
@@ -345,36 +391,37 @@ function readAppJson(cwd) {
|
|
|
345
391
|
process.exit(1);
|
|
346
392
|
}
|
|
347
393
|
const expo = data?.expo ?? {};
|
|
348
|
-
let runtimeVersion
|
|
349
|
-
if (
|
|
394
|
+
let runtimeVersion;
|
|
395
|
+
if (typeof expo.runtimeVersion === "string") {
|
|
396
|
+
runtimeVersion = expo.runtimeVersion;
|
|
397
|
+
} else if (typeof expo.runtimeVersion === "object" && expo.runtimeVersion !== null && expo.runtimeVersion.policy === "appVersion") {
|
|
398
|
+
if (typeof expo.version === "string") {
|
|
399
|
+
runtimeVersion = expo.version;
|
|
400
|
+
} else {
|
|
401
|
+
printErrorBox(
|
|
402
|
+
"Missing expo.version in app.json",
|
|
403
|
+
`"expo.runtimeVersion.policy" is set to "appVersion", but "expo.version" is not defined in app.json.`,
|
|
404
|
+
`Add ${c.cyan}"version": "1.0.0"${c.reset} to your ${c.bold}app.json${c.reset} under ${c.dim}"expo"${c.reset}.`
|
|
405
|
+
);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
} else if (!expo.runtimeVersion) {
|
|
350
409
|
printErrorBox(
|
|
351
410
|
"Missing expo.runtimeVersion in app.json",
|
|
352
|
-
"
|
|
411
|
+
`No "runtimeVersion" field found under "expo" in app.json.`,
|
|
353
412
|
[
|
|
354
413
|
`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
|
|
414
|
+
`Or use the appVersion policy: ${c.cyan}"runtimeVersion": { "policy": "appVersion" }${c.reset}`
|
|
356
415
|
]
|
|
357
416
|
);
|
|
358
417
|
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") {
|
|
418
|
+
} else {
|
|
371
419
|
printErrorBox(
|
|
372
|
-
"
|
|
420
|
+
"Unsupported expo.runtimeVersion in app.json",
|
|
373
421
|
`expo.runtimeVersion in app.json resolved to: ${JSON.stringify(runtimeVersion)}`,
|
|
374
422
|
[
|
|
375
|
-
`
|
|
376
|
-
`If using ${c.dim}"policy": "
|
|
377
|
-
`Or simply specify a static string: ${c.cyan}"runtimeVersion": "1.0.0"${c.reset}`
|
|
423
|
+
`EdgeOTA requires either a string (e.g. "1.0.0") or { "policy": "appVersion" }.`,
|
|
424
|
+
`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
425
|
]
|
|
379
426
|
);
|
|
380
427
|
process.exit(1);
|
|
@@ -386,7 +433,7 @@ function readAppJson(cwd) {
|
|
|
386
433
|
"Your app is not configured to receive updates from an EdgeOTA server.",
|
|
387
434
|
[
|
|
388
435
|
`Run ${c.cyan}edge-ota init${c.reset} in this directory to automatically configure your project.`,
|
|
389
|
-
`Or manually add under ${c.dim}"expo.updates"${c.reset}: ${c.cyan}"url": "https://api.ota.renbo.
|
|
436
|
+
`Or manually add under ${c.dim}"expo.updates"${c.reset}: ${c.cyan}"url": "https://api.ota.renbo.dev/api/projects/<project-id>/updates"${c.reset}`
|
|
390
437
|
]
|
|
391
438
|
);
|
|
392
439
|
process.exit(1);
|
|
@@ -414,7 +461,13 @@ function readAppJson(cwd) {
|
|
|
414
461
|
if (projectServer) {
|
|
415
462
|
serverUrl = projectServer.replace(/\/$/, "");
|
|
416
463
|
}
|
|
417
|
-
return {
|
|
464
|
+
return {
|
|
465
|
+
serverUrl,
|
|
466
|
+
projectId,
|
|
467
|
+
runtimeVersion,
|
|
468
|
+
publicKey: expo?.extra?.edgeOtaPublicKey,
|
|
469
|
+
expoConfig: expo
|
|
470
|
+
};
|
|
418
471
|
}
|
|
419
472
|
function updateAppJson(cwd, serverUrl, projectId, publicKey) {
|
|
420
473
|
const appJsonPath = path.resolve(cwd, "app.json");
|
|
@@ -569,16 +622,42 @@ async function hashFile(filePath) {
|
|
|
569
622
|
}
|
|
570
623
|
async function collectAssets(distDir) {
|
|
571
624
|
const entries = [];
|
|
625
|
+
const extMap = {};
|
|
626
|
+
const metadataPath = path.join(distDir, "metadata.json");
|
|
627
|
+
if (fs.existsSync(metadataPath)) {
|
|
628
|
+
try {
|
|
629
|
+
const meta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
|
630
|
+
if (meta.fileMetadata) {
|
|
631
|
+
for (const p of Object.keys(meta.fileMetadata)) {
|
|
632
|
+
const pMeta = meta.fileMetadata[p];
|
|
633
|
+
if (Array.isArray(pMeta.assets)) {
|
|
634
|
+
for (const a of pMeta.assets) {
|
|
635
|
+
if (a.path && a.ext) {
|
|
636
|
+
const normPath = a.path.replace(/\\/g, "/");
|
|
637
|
+
extMap[normPath] = a.ext.startsWith(".") ? a.ext.toLowerCase() : `.${a.ext.toLowerCase()}`;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
}
|
|
572
646
|
function walk(dir) {
|
|
573
647
|
for (const name of fs.readdirSync(dir)) {
|
|
574
648
|
const full = path.join(dir, name);
|
|
575
649
|
if (fs.statSync(full).isDirectory()) {
|
|
576
650
|
walk(full);
|
|
577
651
|
} else {
|
|
578
|
-
const
|
|
652
|
+
const relKey = path.relative(distDir, full).replace(/\\/g, "/");
|
|
653
|
+
let ext = path.extname(name).toLowerCase();
|
|
654
|
+
if (!ext && extMap[relKey]) {
|
|
655
|
+
ext = extMap[relKey];
|
|
656
|
+
}
|
|
579
657
|
entries.push({
|
|
580
658
|
localPath: full,
|
|
581
|
-
key:
|
|
659
|
+
key: relKey,
|
|
660
|
+
fileExtension: ext,
|
|
582
661
|
contentType: CONTENT_TYPES[ext] || "application/octet-stream",
|
|
583
662
|
hash: ""
|
|
584
663
|
});
|
|
@@ -606,11 +685,21 @@ function findBundle(distDir, platform) {
|
|
|
606
685
|
return null;
|
|
607
686
|
}
|
|
608
687
|
function resolveToken() {
|
|
688
|
+
if (process.env.EDGE_OTA_TOKEN) return process.env.EDGE_OTA_TOKEN;
|
|
609
689
|
const cfg = loadGlobalConfig();
|
|
610
690
|
if (cfg?.token) return cfg.token;
|
|
611
|
-
return
|
|
691
|
+
return null;
|
|
612
692
|
}
|
|
613
693
|
var program = new Command();
|
|
694
|
+
var migrationNoticeShown = false;
|
|
695
|
+
function notifyDomainMigrationOnce(serverUrl) {
|
|
696
|
+
if (migrationNoticeShown) return;
|
|
697
|
+
migrationNoticeShown = true;
|
|
698
|
+
maybeNotifyDomainMigration(serverUrl);
|
|
699
|
+
}
|
|
700
|
+
program.hook("preAction", () => {
|
|
701
|
+
notifyDomainMigrationOnce(loadGlobalConfig()?.serverUrl);
|
|
702
|
+
});
|
|
614
703
|
program.name("edge-ota").description("Zero-SDK, self-hostable OTA update platform for Expo \u2014 by Renbo Studios").version(packageVersion);
|
|
615
704
|
program.addHelpText("after", `
|
|
616
705
|
${c.bold}Quick Start:${c.reset}
|
|
@@ -620,13 +709,15 @@ ${c.bold}Quick Start:${c.reset}
|
|
|
620
709
|
4. ${c.cyan}edge-ota push${c.reset} Export and publish your first OTA update
|
|
621
710
|
|
|
622
711
|
${c.bold}Documentation & Support:${c.reset}
|
|
623
|
-
Website: https://ota.renbo.
|
|
712
|
+
Website: https://ota.renbo.dev
|
|
713
|
+
Migration: https://ota.renbo.dev/migration ${c.dim}(moved from ota.renbo.site)${c.reset}
|
|
624
714
|
Studio: https://renbostudios.com
|
|
625
|
-
GitHub: https://github.com/renbostudios/edge-ota
|
|
715
|
+
GitHub: https://github.com/renbostudios/edge-ota
|
|
626
716
|
`);
|
|
627
717
|
program.action(() => {
|
|
628
718
|
printBanner();
|
|
629
719
|
const cfg = loadGlobalConfig();
|
|
720
|
+
notifyDomainMigrationOnce(cfg?.serverUrl);
|
|
630
721
|
if (cfg?.email) {
|
|
631
722
|
console.log(` ${c.green}\u25CF${c.reset} Logged in as ${c.bold}${cfg.email}${c.reset} ${c.dim}(${cfg.serverUrl})${c.reset}`);
|
|
632
723
|
} else {
|
|
@@ -685,7 +776,7 @@ ${c.bold}CI/CD Environments:${c.reset}
|
|
|
685
776
|
text || "The credentials provided were rejected by the server.",
|
|
686
777
|
[
|
|
687
778
|
`Check your email and password for typos.`,
|
|
688
|
-
`If you don't have an account yet, register at ${c.bold}https://ota.renbo.
|
|
779
|
+
`If you don't have an account yet, register at ${c.bold}https://ota.renbo.dev${c.reset}`,
|
|
689
780
|
`If using self-hosted server, verify ${c.bold}${serverUrl}${c.reset} is correct.`
|
|
690
781
|
]
|
|
691
782
|
);
|
|
@@ -930,6 +1021,12 @@ ${c.bold}How It Works:${c.reset}
|
|
|
930
1021
|
const appCfg = readAppJson(cwd);
|
|
931
1022
|
const { serverUrl, projectId } = appCfg;
|
|
932
1023
|
const runtimeVersion = options.runtime || appCfg.runtimeVersion;
|
|
1024
|
+
if (isLegacyEdgeOtaHost(serverUrl)) {
|
|
1025
|
+
console.log(` ${c.yellow}\u25B2${c.reset} ${c.dim}app.json still targets the legacy server:${c.reset} ${c.red}${serverUrl}${c.reset}`);
|
|
1026
|
+
console.log(` ${c.dim}This push will work (legacy API stays online), but new installs should use:${c.reset} ${c.cyan}${DEFAULT_SERVER}${c.reset}`);
|
|
1027
|
+
console.log(` ${c.dim}Update ${c.reset}expo.updates.url${c.dim} + ${c.reset}expo.extra.edgeOtaServer${c.dim}, then run ${c.reset}${c.cyan}npx expo prebuild --clean${c.reset}
|
|
1028
|
+
`);
|
|
1029
|
+
}
|
|
933
1030
|
const privateKey = projectId ? loadPrivateKey(projectId) : null;
|
|
934
1031
|
if (!privateKey) {
|
|
935
1032
|
const keyPath = projectId ? path.join(GLOBAL_KEYS_DIR, `${projectId}.key`) : "unknown";
|
|
@@ -1061,14 +1158,20 @@ ${sep}`);
|
|
|
1061
1158
|
platform,
|
|
1062
1159
|
bundleHash,
|
|
1063
1160
|
timestamp: Date.now(),
|
|
1064
|
-
assets: mediaAssets.map((a) =>
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1161
|
+
assets: mediaAssets.map((a) => {
|
|
1162
|
+
const ext = a.fileExtension || path.extname(a.localPath) || "";
|
|
1163
|
+
return {
|
|
1164
|
+
hash: a.hash,
|
|
1165
|
+
key: path.basename(a.key, ext),
|
|
1166
|
+
fileExtension: ext,
|
|
1167
|
+
contentType: a.contentType
|
|
1168
|
+
};
|
|
1169
|
+
}),
|
|
1070
1170
|
assetCount: mediaAssets.length,
|
|
1071
|
-
publicKey: appCfg.publicKey
|
|
1171
|
+
publicKey: appCfg.publicKey,
|
|
1172
|
+
extra: {
|
|
1173
|
+
expoClient: appCfg.expoConfig || {}
|
|
1174
|
+
}
|
|
1072
1175
|
};
|
|
1073
1176
|
const payloadStr = JSON.stringify(payloadObj);
|
|
1074
1177
|
spin.start("signing bundle hash");
|
|
@@ -1114,7 +1217,7 @@ ${sep}`);
|
|
|
1114
1217
|
printErrorBox(
|
|
1115
1218
|
"Permission denied",
|
|
1116
1219
|
text || `You do not have permission to publish updates to project ${projectId}.`,
|
|
1117
|
-
`Check your team role on ${c.bold}https://ota.renbo.
|
|
1220
|
+
`Check your team role on ${c.bold}https://ota.renbo.dev${c.reset} or verify API key permissions.`
|
|
1118
1221
|
);
|
|
1119
1222
|
} else if (response.status === 404) {
|
|
1120
1223
|
printErrorBox(
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renbostudios/edge-ota",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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://
|
|
8
|
+
"homepage": "https://ota.renbo.dev",
|
|
9
9
|
"publishConfig": {
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "https://github.com/
|
|
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
|
|
18
|
+
"url": "https://github.com/renbostudios/edge-ota/issues"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"expo",
|