@renbostudios/edge-ota 0.4.0 → 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.
package/README.md CHANGED
@@ -1,195 +1,200 @@
1
- # @renbostudios/edge-ota CLI
1
+ <p align="center">
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>
5
+ </p>
6
+
7
+ <h1 align="center">@renbostudios/edge-ota</h1>
8
+
9
+ <p align="center">
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
+ </p>
13
+
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=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>
21
+ </p>
22
+
23
+ <p align="center">
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>
29
+ </p>
2
30
 
3
- Zero-SDK, self-hostable OTA update CLI for Expo React Native. Push signed JavaScript bundles to any EdgeOTA server.
31
+ ---
4
32
 
5
- ## Install
33
+ ## Highlights
6
34
 
7
- ```bash
8
- npm install -g @renbostudios/edge-ota
9
- ```
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).
10
41
 
11
- Requires Node.js 18+ and an Expo project with `expo-updates` installed.
42
+ ---
12
43
 
13
- ## Quick Start
44
+ ## Video Walkthrough
14
45
 
15
- ```bash
16
- # 1. Log in to your EdgeOTA account
17
- edge-ota login
46
+ Watch the 45-second terminal walkthrough demonstrating `edge-ota init`, project registration, and publishing an instant OTA hotfix:
18
47
 
19
- # 2. Register your project and generate signing keys
20
- edge-ota init
48
+ <p align="center">
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;">
50
+ </video>
51
+ </p>
21
52
 
22
- # 3. Rebuild your native app (REQUIRED — see below)
23
- npx expo prebuild --clean
24
- npx expo run:ios
25
- npx expo run:android
53
+ ### Live Terminal Flow
26
54
 
27
- # 4. Push an OTA update
28
- edge-ota push
29
- ```
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>
30
58
 
31
59
  ---
32
60
 
33
- ## Commands
61
+ ## 60-Second Quick Start
34
62
 
35
- ### `edge-ota login`
63
+ ### 1. Install the CLI
36
64
 
37
- Authenticate with your EdgeOTA account. Credentials are stored globally at `~/.config/edge-ota/config.json`.
65
+ Install the official EdgeOTA CLI globally:
38
66
 
39
67
  ```bash
40
- edge-ota login
41
- edge-ota login --server https://my-server.com # custom server
68
+ npm install -g @renbostudios/edge-ota
42
69
  ```
43
70
 
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`
71
+ ### 2. Authenticate
47
72
 
48
- Remove stored credentials.
73
+ Log in to your account (or specify your self-hosted server via `--server`):
49
74
 
50
75
  ```bash
51
- edge-ota logout
76
+ edge-ota login
52
77
  ```
53
78
 
54
- ### `edge-ota init`
79
+ ### 3. Initialize Your Project
55
80
 
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.
81
+ From the root of your Expo React Native project, run:
57
82
 
58
83
  ```bash
59
84
  edge-ota init
60
- edge-ota init --server https://my-server.com # override server
61
85
  ```
62
86
 
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.
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.
79
91
 
80
- ### `edge-ota push`
92
+ ### 4. Rebuild Native Binary (Required Once)
81
93
 
82
- Export your Expo bundle and publish an OTA update to the server.
94
+ The `expo-updates` native client embeds the update server URL into your native binary at compile time:
83
95
 
84
96
  ```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
97
+ # Clean prebuild and compile locally
98
+ npx expo prebuild --clean
99
+ npx expo run:ios
100
+ npx expo run:android
102
101
 
103
- **How it reads your config:**
102
+ # Or build in the cloud with EAS
103
+ eas build --profile production
104
+ ```
104
105
 
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`
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.
110
108
 
111
- ### `edge-ota status`
109
+ ### 5. Publish an OTA Update
112
110
 
113
- List recent deployments for the current project.
111
+ Push your updated JavaScript code and assets live:
114
112
 
115
113
  ```bash
116
- edge-ota status
117
- edge-ota status --limit 20 # show more releases
114
+ edge-ota push --channel production
118
115
  ```
119
116
 
120
- ### `edge-ota keygen`
117
+ Your app silently downloads the update in the background and applies it seamlessly on the next cold launch.
118
+
119
+ ---
121
120
 
122
- Generate a new ECDSA P-256 key pair and print it to stdout. Does not write any files.
121
+ ## CLI Command Reference
123
122
 
124
123
  ```bash
125
- edge-ota keygen
124
+ edge-ota <command> [options]
126
125
  ```
127
126
 
128
- Useful for manual key rotation or generating keys for server-side signing.
129
-
130
- ---
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 | — |
131
135
 
132
- ## Rebuild Required
136
+ ### Advanced Workflows
133
137
 
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.
138
+ #### Multi-Runtime Matrix Deployment
139
+ Deploy a single JS patch simultaneously across multiple released versions of your native app:
135
140
 
136
141
  ```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
142
+ edge-ota push --runtime 1.0.0,1.0.1,1.0.2 --channel production
146
143
  ```
147
144
 
148
- **If you skip this step**, the app will fail with:
145
+ #### Platform Targeting
146
+ Deploy an update exclusively to iOS or Android devices:
149
147
 
150
- ```
151
- Failed to check for update
148
+ ```bash
149
+ edge-ota push --platform ios --channel production
152
150
  ```
153
151
 
154
- The native module has no valid server URL to reach because it was never embedded in the binary.
152
+ #### Dry Run & CI/CD Pipelines
153
+ Export and sign the bundle without uploading to test your pipeline:
155
154
 
156
- 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
158
+
159
+ # Upload pre-exported ./dist bundle from previous CI build step
160
+ edge-ota push --skip-export --channel production
161
+ ```
157
162
 
158
163
  ---
159
164
 
160
- ## How It Works
165
+ ## Architecture & Data Flow
161
166
 
162
167
  ```
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 ✓
168
+ Developer Workstation EdgeOTA Backend User Device
169
+ ───────────────────── ─────────────── ───────────
170
+
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)
174
180
  ```
175
181
 
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
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.
181
186
 
182
187
  ---
183
188
 
184
- ## What `init` Writes to `app.json`
189
+ ## Configuration (`app.json`)
185
190
 
186
- After running `edge-ota init`, your `app.json` will contain:
191
+ `edge-ota init` populates your `app.json` with standard Expo configuration:
187
192
 
188
193
  ```json
189
194
  {
190
195
  "expo": {
191
196
  "updates": {
192
- "url": "https://your-server.com/api/projects/<projectId>/updates",
197
+ "url": "https://ota.renbo.site/api/projects/<projectId>/updates",
193
198
  "checkAutomatically": "ON_LOAD",
194
199
  "fallbackToCacheTimeout": 30000,
195
200
  "requestHeaders": {
@@ -198,131 +203,124 @@ After running `edge-ota init`, your `app.json` will contain:
198
203
  },
199
204
  "runtimeVersion": "1.0.0",
200
205
  "extra": {
201
- "edgeOtaServer": "https://your-server.com",
202
- "edgeOtaPublicKey": "-----BEGIN PUBLIC KEY-----\n..."
206
+ "edgeOtaServer": "https://ota.renbo.site",
207
+ "edgeOtaPublicKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...\n-----END PUBLIC KEY-----"
203
208
  }
204
209
  }
205
210
  }
206
211
  ```
207
212
 
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
213
  ---
242
214
 
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.
215
+ ## Self-Hosting Options
246
216
 
247
- 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:
248
218
 
249
- ---
219
+ ### Option 1: Cloudflare Worker + D1 + R2 (Recommended)
250
220
 
251
- ## Security
221
+ Deploy a global serverless OTA engine with zero egress fees:
252
222
 
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
223
+ ```bash
224
+ cd apps/worker-oss
225
+ npm install
258
226
 
259
- ---
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
260
230
 
261
- ## Server Setup
231
+ # Deploy to Cloudflare Edge
232
+ npx wrangler deploy
233
+ ```
262
234
 
263
- The EdgeOTA server is a standard Node.js/Express app. You can self-host it or use the managed cloud service.
235
+ ### Option 2: Docker Compose (Node.js + SQLite / Postgres)
264
236
 
265
- ### Docker
237
+ Deploy anywhere with a single container stack:
266
238
 
267
239
  ```bash
268
- docker pull renbostudios/edge-ota-server:latest
269
- docker run -p 3000:3000 -e DATABASE_URL=... renbostudios/edge-ota-server
240
+ git clone https://github.com/renbostudios/edge-ota.git
241
+ cd edge-ota
242
+ docker compose up -d
270
243
  ```
271
244
 
272
- ### Docker Compose
245
+ The Node.js server will boot on port `3000` with automated database migrations and local asset storage.
273
246
 
274
- ```bash
275
- git clone https://github.com/renbostudios/edge-ota-cli
276
- cd edge-ota
277
- docker-compose up -d
278
- ```
247
+ ---
279
248
 
280
- ### Manual
249
+ ## Security Model
281
250
 
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
- ```
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=..."`).
288
254
 
289
255
  ---
290
256
 
291
257
  ## Troubleshooting
292
258
 
293
- ### "Failed to check for update" on app launch
259
+ <details>
260
+ <summary><strong>"Failed to check for update" on app launch</strong></summary>
261
+
262
+ <br>
294
263
 
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`.
264
+ The `expo-updates` native module cannot contact your update server because the server URL was not embedded in the native binary.
296
265
 
266
+ Run a clean native build:
297
267
  ```bash
298
268
  npx expo prebuild --clean
299
269
  npx expo run:ios
300
270
  npx expo run:android
301
271
  ```
272
+ </details>
302
273
 
303
- ### Updates work on iOS but not Android
274
+ <details>
275
+ <summary><strong>Updates apply on iOS but not on Android</strong></summary>
304
276
 
305
- The Android native project may still have the default Expo Updates URL. Fix: regenerate and rebuild.
277
+ <br>
306
278
 
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:
280
+
281
+ ```xml
282
+ android:usesCleartextTraffic="true"
283
+ ```
284
+ And forward the port over ADB:
307
285
  ```bash
308
- npx expo prebuild --clean
309
- npx expo run:android
286
+ adb reverse tcp:3000 tcp:3000
310
287
  ```
288
+ </details>
289
+
290
+ <details>
291
+ <summary><strong>"No signing key found for project" during push</strong></summary>
311
292
 
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.
293
+ <br>
313
294
 
314
- ### Updates stopped working after a version bump
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.
296
+ </details>
315
297
 
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.
298
+ <details>
299
+ <summary><strong>Switching between Cloud and Self-Hosted</strong></summary>
317
300
 
318
- ### "No signing key found" error on push
301
+ <br>
319
302
 
320
- Run `edge-ota init` to generate keys, or use `edge-ota keygen` to generate a new pair and register it manually.
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`.
304
+ </details>
321
305
 
322
306
  ---
323
307
 
324
- ## Built by Renbo Studios
308
+ ## Monorepo Topology
325
309
 
326
- [renbostudios.com](https://renbostudios.com) — Mobile App Development Studio
310
+ ```
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
319
+ ```
320
+
321
+ ---
327
322
 
328
- MIT License
323
+ <p align="center">
324
+ Built with care by <a href="https://renbostudios.com">Renbo Studios</a> & open-source contributors.<br>
325
+ <sub>MIT License · Open Source</sub>
326
+ </p>