@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 CHANGED
@@ -1,125 +1,26 @@
1
- # `@edge-ota/cli`
1
+ # @renbostudios/edge-ota CLI
2
2
 
3
- > Zero-SDK, self-hostable OTA update CLI for Expo apps.
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
- ## Install
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
- ### `edge-ota status [options]`
73
-
74
- Lists recent releases from the server as a table.
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 keygen
21
+ npm install -g @renbostudios/edge-ota
95
22
  ```
96
23
 
97
24
  ---
98
25
 
99
- ## How it works
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
@@ -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 proper boundary
7
+ * - Multipart/mixed response body with FormData-compatible boundary
7
8
  * - expo-signature header for ECDSA bundle verification
8
- * - Structured extension (extra) metadata passthrough
9
+ * - fileExtension on launchAsset and assets
10
+ * - extra.expoClient config passthrough
9
11
  */
10
12
  /**
11
- * Build the Expo Updates v1 manifest object.
12
- * The client SDK deserialises this from the `manifest` part of the
13
- * multipart response (or directly from the response body in non-multipart mode).
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: "bundle",
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, must-revalidate",
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 when the client sends
77
- * `Accept: multipart/mixed` (expo-updates >=0.18).
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
- * <manifest JSON>
85
- * --<boundary>--
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 = "expo-update-boundary-" + Math.random().toString(36).slice(2, 10);
86
+ const boundary = "ota" + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
89
87
  const manifestJson = JSON.stringify(manifest);
90
- let partHeaders = "Content-Disposition: inline; name=\"manifest\"\r\n";
91
- partHeaders += "Content-Type: application/json\r\n";
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
- partHeaders += `expo-signature: sig="${signature}", keyid="root", alg="ecdsa-p256-sha256"\r\n`;
93
+ manifestPart += `expo-signature: sig="${signature}", keyid="root", alg="ecdsa-p256-sha256"\r\n`;
94
94
  }
95
- const body = `--${boundary}\r\n` +
96
- `${partHeaders}\r\n` +
97
- `${manifestJson}\r\n` +
98
- `--${boundary}--\r\n`;
99
- return { body, boundary };
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
  }