@renbostudios/edge-ota 0.2.7 → 0.2.9
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 +12 -111
- package/dist/core/manifest.js +38 -30
- package/dist/index.js +653 -249
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,125 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @renbostudios/edge-ota CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> Push cryptographically signed JS bundles to any EdgeOTA server — no Expo account required.
|
|
3
|
+
Zero-SDK, self-hostable OTA updates for Expo.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
npm install -g @edge-ota/cli
|
|
10
|
-
# or use without installing:
|
|
11
|
-
npx @edge-ota/cli <command>
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Quick Start
|
|
15
|
-
|
|
16
|
-
Run these commands **inside your Expo project directory**:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
# 1. Generate ECDSA P-256 keys and write config
|
|
20
|
-
edge-ota init
|
|
21
|
-
|
|
22
|
-
# 2. Push your first update
|
|
23
|
-
edge-ota push --channel production --runtime 1.0.0
|
|
24
|
-
|
|
25
|
-
# 3. Check what's deployed
|
|
26
|
-
edge-ota status
|
|
27
|
-
```
|
|
5
|
+
See the [main README](../../README.md) for full documentation.
|
|
28
6
|
|
|
29
7
|
## Commands
|
|
30
8
|
|
|
31
|
-
### `edge-ota init [options]`
|
|
32
|
-
|
|
33
|
-
Generates an ECDSA P-256 key pair, writes `edge-ota.config.json` and `.edge-ota.private.key`, auto-adds the private key to `.gitignore`, and automatically updates your Expo `app.json` configuration file with the new updates URL.
|
|
34
|
-
|
|
35
|
-
If `--server` is not specified, you will be prompted interactively to enter the URL of your hosted or local EdgeOTA server and your optional Project ID.
|
|
36
|
-
|
|
37
|
-
| Option | Default | Description |
|
|
38
|
-
|--------|---------|-------------|
|
|
39
|
-
| `-s, --server <url>` | — | Your EdgeOTA server URL (prompts if omitted) |
|
|
40
|
-
| `--project <id>` | — | Your EdgeOTA Project ID (prompts if omitted, optional) |
|
|
41
|
-
|
|
42
|
-
**Output:**
|
|
43
|
-
- `edge-ota.config.json` — server URL, public key, and optional project ID (safe to commit)
|
|
44
|
-
- `.edge-ota.private.key` — private signing key (**never commit this**)
|
|
45
|
-
- **Auto-configured:** `app.json` is modified to point `expo.updates.url` to your server's updates endpoint. If a Project ID is configured, it points to `/api/projects/:projectId/updates`; otherwise, it falls back to the global `/api/updates` endpoint.
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
### `edge-ota push [options]`
|
|
50
|
-
|
|
51
|
-
Runs `expo export`, hashes every asset with SHA-256, signs the payload with your private key, then uploads the bundle to your server.
|
|
52
|
-
|
|
53
|
-
| Option | Default | Description |
|
|
54
|
-
|--------|---------|-------------|
|
|
55
|
-
| `-c, --channel <name>` | `production` | Deployment channel |
|
|
56
|
-
| `-r, --runtime <version>` | `1.0.0` | Native runtime version (must match `runtimeVersion` in app.json) |
|
|
57
|
-
| `-p, --platform <platform>` | `all` | `ios`, `android`, or `all` |
|
|
58
|
-
| `--project <id>` | — | Project ID (overrides value in `edge-ota.config.json`) |
|
|
59
|
-
| `--skip-export` | — | Skip `expo export` — use existing `./dist` |
|
|
60
|
-
| `--dry-run` | — | Sign the payload but don't upload |
|
|
61
|
-
|
|
62
|
-
**Example — CI/CD:**
|
|
63
|
-
```bash
|
|
64
|
-
edge-ota push \
|
|
65
|
-
--channel production \
|
|
66
|
-
--runtime 1.2.0 \
|
|
67
|
-
--platform all
|
|
68
9
|
```
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
| Option | Default | Description |
|
|
77
|
-
|--------|---------|-------------|
|
|
78
|
-
| `-t, --token <token>` | `$EDGE_OTA_TOKEN` | Auth token from the dashboard |
|
|
79
|
-
| `-n, --limit <n>` | `10` | Number of releases to show |
|
|
80
|
-
| `--project <id>` | — | Project ID to query (overrides config) |
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
export EDGE_OTA_TOKEN="your-dashboard-token"
|
|
84
|
-
edge-ota status
|
|
10
|
+
edge-ota login # Log in globally
|
|
11
|
+
edge-ota logout # Log out
|
|
12
|
+
edge-ota init # Set up a new project
|
|
13
|
+
edge-ota push # Publish an OTA update
|
|
14
|
+
edge-ota status # View recent deployments
|
|
15
|
+
edge-ota keygen # Generate a new key pair
|
|
85
16
|
```
|
|
86
17
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
### `edge-ota keygen`
|
|
90
|
-
|
|
91
|
-
Prints a fresh ECDSA P-256 key pair to stdout without writing any files. Useful for generating secrets to store in CI/CD environment variables.
|
|
18
|
+
## Install
|
|
92
19
|
|
|
93
20
|
```bash
|
|
94
|
-
edge-ota
|
|
21
|
+
npm install -g @renbostudios/edge-ota
|
|
95
22
|
```
|
|
96
23
|
|
|
97
24
|
---
|
|
98
25
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
1. **Build** — `expo export` produces a JS bundle + assets in `./dist/`
|
|
102
|
-
2. **Hash** — every file gets a SHA-256 content hash (enables deduplication on the server)
|
|
103
|
-
3. **Sign** — the upload payload (`{ channel, runtimeVersion, bundleHash, ... }`) is signed with your ECDSA P-256 private key
|
|
104
|
-
4. **Upload** — `POST /api/updates` with `multipart/form-data` containing the bundle zip, payload JSON, and signature
|
|
105
|
-
5. **Serve** — when your app checks for updates, the server verifies the stored signature and returns a signed manifest
|
|
106
|
-
6. **Verify** — the Expo SDK verifies the manifest signature and bundle hash before swapping the active bundle
|
|
107
|
-
|
|
108
|
-
## Expo App Setup
|
|
109
|
-
|
|
110
|
-
Add to your `app.json`:
|
|
111
|
-
|
|
112
|
-
```json
|
|
113
|
-
{
|
|
114
|
-
"expo": {
|
|
115
|
-
"updates": {
|
|
116
|
-
"url": "https://your-edge-ota-server.com/api/updates",
|
|
117
|
-
"runtimeVersion": { "policy": "nativeVersion" }
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## License
|
|
124
|
-
|
|
125
|
-
MIT
|
|
26
|
+
by **[Renbo Studios](https://renbostudios.com)** — Mobile App Development Studio
|
package/dist/core/manifest.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EdgeOTA Core - Expo Updates Protocol v1 Manifest Engine
|
|
3
3
|
*
|
|
4
|
-
* Implements the exact wire format consumed by expo-updates SDK
|
|
4
|
+
* Implements the exact wire format consumed by expo-updates SDK,
|
|
5
|
+
* matching the official expo/custom-expo-updates-server reference:
|
|
5
6
|
* - EAS Update-compatible manifest structure
|
|
6
|
-
* - Multipart/mixed response body with
|
|
7
|
+
* - Multipart/mixed response body with FormData-compatible boundary
|
|
7
8
|
* - expo-signature header for ECDSA bundle verification
|
|
8
|
-
* -
|
|
9
|
+
* - fileExtension on launchAsset and assets
|
|
10
|
+
* - extra.expoClient config passthrough
|
|
9
11
|
*/
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* Convert a hex SHA-256 hash to base64url encoding.
|
|
14
|
+
* expo-updates verifies downloaded assets by computing SHA-256, base64url-encoding it,
|
|
15
|
+
* and comparing against the manifest `hash` field.
|
|
14
16
|
*/
|
|
15
17
|
function hexToBase64Url(hex) {
|
|
16
18
|
if (hex.length !== 64 || !/^[0-9a-fA-F]+$/.test(hex)) {
|
|
@@ -30,6 +32,10 @@ function hexToBase64Url(hex) {
|
|
|
30
32
|
.replace(/\//g, '_')
|
|
31
33
|
.replace(/=+$/, '');
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the Expo Updates v1 manifest object.
|
|
37
|
+
* Matches the format produced by expo/custom-expo-updates-server.
|
|
38
|
+
*/
|
|
33
39
|
export function generateExpoManifest(params) {
|
|
34
40
|
return {
|
|
35
41
|
id: params.updateId,
|
|
@@ -37,7 +43,8 @@ export function generateExpoManifest(params) {
|
|
|
37
43
|
runtimeVersion: params.runtimeVersion,
|
|
38
44
|
launchAsset: {
|
|
39
45
|
hash: hexToBase64Url(params.bundleHash),
|
|
40
|
-
key:
|
|
46
|
+
key: params.bundleKey || params.bundleHash.slice(0, 32),
|
|
47
|
+
fileExtension: ".bundle",
|
|
41
48
|
contentType: "application/javascript",
|
|
42
49
|
url: params.bundleUrl
|
|
43
50
|
},
|
|
@@ -56,45 +63,46 @@ export function generateExpoManifest(params) {
|
|
|
56
63
|
* expo-sfv-version: "0" — Structured Fields version
|
|
57
64
|
* cache-control — Updates must never be stale; client always revalidates
|
|
58
65
|
* expo-signature — ECDSA P-256 + SHA-256 signature over the manifest JSON
|
|
59
|
-
* (base64url-encoded DER). Omitted when no key is configured.
|
|
60
66
|
*/
|
|
61
67
|
export function createExpoHeaders(signature) {
|
|
62
68
|
const headers = {
|
|
63
69
|
"expo-protocol-version": "1",
|
|
64
70
|
"expo-sfv-version": "0",
|
|
65
|
-
"cache-control": "private, max-age=0
|
|
66
|
-
"content-type": "application/json"
|
|
71
|
+
"cache-control": "private, max-age=0",
|
|
67
72
|
};
|
|
68
73
|
if (signature) {
|
|
69
|
-
// EAS format: sig="<base64>", keyid="root", alg="sig-rs256"
|
|
70
|
-
// For ECDSA P-256 we use the simpler bare signature header
|
|
71
74
|
headers["expo-signature"] = `sig="${signature}", keyid="root", alg="ecdsa-p256-sha256"`;
|
|
72
75
|
}
|
|
73
76
|
return headers;
|
|
74
77
|
}
|
|
75
78
|
/**
|
|
76
|
-
* Build the multipart/mixed response body required
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* Layout:
|
|
80
|
-
* --<boundary>
|
|
81
|
-
* Content-Type: application/json
|
|
82
|
-
* expo-signature: sig="...", keyid="root", alg="ecdsa-p256-sha256"
|
|
79
|
+
* Build the multipart/mixed response body required by expo-updates protocol v1.
|
|
80
|
+
* Uses FormData-compatible boundary formatting matching the official Expo server.
|
|
83
81
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* The expo-updates native client parses this multipart body to extract the manifest
|
|
83
|
+
* from the part named "manifest".
|
|
86
84
|
*/
|
|
87
85
|
export function buildMultipartManifestBody(manifest, signature) {
|
|
88
|
-
const boundary = "
|
|
86
|
+
const boundary = "ota" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
89
87
|
const manifestJson = JSON.stringify(manifest);
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
// Build the manifest part headers exactly as FormData would
|
|
89
|
+
let manifestPart = `--${boundary}\r\n`;
|
|
90
|
+
manifestPart += `Content-Disposition: form-data; name="manifest"\r\n`;
|
|
91
|
+
manifestPart += `Content-Type: application/json; charset=utf-8\r\n`;
|
|
92
92
|
if (signature) {
|
|
93
|
-
|
|
93
|
+
manifestPart += `expo-signature: sig="${signature}", keyid="root", alg="ecdsa-p256-sha256"\r\n`;
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
manifestPart += `\r\n`;
|
|
96
|
+
manifestPart += manifestJson;
|
|
97
|
+
manifestPart += `\r\n`;
|
|
98
|
+
// Build the extensions part (included by official Expo server)
|
|
99
|
+
const extensions = { assetRequestHeaders: {} };
|
|
100
|
+
let extensionsPart = `--${boundary}\r\n`;
|
|
101
|
+
extensionsPart += `Content-Disposition: form-data; name="extensions"\r\n`;
|
|
102
|
+
extensionsPart += `Content-Type: application/json\r\n`;
|
|
103
|
+
extensionsPart += `\r\n`;
|
|
104
|
+
extensionsPart += JSON.stringify(extensions);
|
|
105
|
+
extensionsPart += `\r\n`;
|
|
106
|
+
const body = manifestPart + extensionsPart + `--${boundary}--\r\n`;
|
|
107
|
+
return { body, boundary, contentType: `multipart/mixed; boundary=${boundary}` };
|
|
100
108
|
}
|