@forthtilliath/expo-release-updates 0.5.0 → 0.6.1
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
|
@@ -104,9 +104,9 @@ const history = await fetchReleaseHistory({
|
|
|
104
104
|
// [{ version: "1.11.0", notes: "### Added\n- ...", publishedAt: "2026-07-30T..." }, ...]
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
### `downloadAndInstallApk({ apkUrl, fileName, onProgress?, expectedMd5? })`
|
|
107
|
+
### `downloadAndInstallApk({ apkUrl, fileName, onProgress?, expectedMd5?, allowedHosts? })`
|
|
108
108
|
|
|
109
|
-
Downloads an APK to the app's cache directory and triggers the Android install-package intent. **Android only** — there is no iOS equivalent (sideloading isn't possible there). Pass `expectedMd5` to verify the downloaded file before installing — a mismatch deletes the file and throws instead. Publish the checksum alongside the release (e.g. in the release notes or a `.md5` asset) and read it yourself: `fetchLatestRelease` doesn't return it.
|
|
109
|
+
Downloads an APK to the app's cache directory and triggers the Android install-package intent. **Android only** — there is no iOS equivalent (sideloading isn't possible there). `apkUrl` must be an `https://` URL on one of `allowedHosts` (default `["github.com"]`, where `fetchLatestRelease`'s `apkUrl` points) — anything else throws before downloading. Pass `expectedMd5` to verify the downloaded file before installing — a mismatch deletes the file and throws instead. Publish the checksum alongside the release (e.g. in the release notes or a `.md5` asset) and read it yourself: `fetchLatestRelease` doesn't return it.
|
|
110
110
|
|
|
111
111
|
What `expectedMd5` does and doesn't protect against:
|
|
112
112
|
|
|
@@ -12,14 +12,22 @@ export interface DownloadAndInstallApkOptions {
|
|
|
12
12
|
* signed with the same key as the installed app is what guards against that.
|
|
13
13
|
*/
|
|
14
14
|
expectedMd5?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Hosts `apkUrl` may point to — `["github.com"]` by default, the host of
|
|
17
|
+
* the `apkUrl` returned by `fetchLatestRelease` (GitHub then redirects to
|
|
18
|
+
* its own download host). Any other host, or a non-`https://` URL, is
|
|
19
|
+
* rejected before downloading anything.
|
|
20
|
+
*/
|
|
21
|
+
allowedHosts?: string[];
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
17
24
|
* Downloads an APK to the cache directory and triggers the Android
|
|
18
25
|
* install-package intent. Android only — there is no equivalent flow on iOS,
|
|
19
26
|
* which doesn't allow installing a downloaded package outside the App Store.
|
|
20
27
|
*
|
|
21
|
-
* @throws if
|
|
22
|
-
* match the
|
|
28
|
+
* @throws if `apkUrl` isn't an `https://` URL on one of `allowedHosts`, if
|
|
29
|
+
* the download fails, or if `expectedMd5` is set and doesn't match the
|
|
30
|
+
* downloaded file.
|
|
23
31
|
*/
|
|
24
|
-
export declare function downloadAndInstallApk({ apkUrl, fileName, onProgress, expectedMd5, }: DownloadAndInstallApkOptions): Promise<void>;
|
|
32
|
+
export declare function downloadAndInstallApk({ apkUrl, fileName, onProgress, expectedMd5, allowedHosts, }: DownloadAndInstallApkOptions): Promise<void>;
|
|
25
33
|
//# sourceMappingURL=downloadAndInstallApk.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"downloadAndInstallApk.d.ts","sourceRoot":"","sources":["../src/downloadAndInstallApk.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"downloadAndInstallApk.d.ts","sourceRoot":"","sources":["../src/downloadAndInstallApk.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,MAAM,EACN,QAAQ,EACR,UAAU,EACV,WAAW,EACX,YAAoC,GACrC,EAAE,4BAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CAqC9C"}
|
|
@@ -5,15 +5,31 @@ import * as IntentLauncher from "expo-intent-launcher";
|
|
|
5
5
|
* installer read the APK through its `content://` URI.
|
|
6
6
|
*/
|
|
7
7
|
const FLAG_GRANT_READ_URI_PERMISSION = 1;
|
|
8
|
+
const DEFAULT_ALLOWED_HOSTS = ["github.com"];
|
|
9
|
+
/**
|
|
10
|
+
* Host of an `https://` URL, lowercased — or `null` for any other scheme, or
|
|
11
|
+
* a URL with credentials (`https://github.com@evil.example/`). Parsed by hand:
|
|
12
|
+
* React Native's `URL` doesn't implement `hostname`.
|
|
13
|
+
*/
|
|
14
|
+
function httpsHost(url) {
|
|
15
|
+
const match = /^https:\/\/([^/?#:@]+)(?::\d+)?(?:[/?#]|$)/i.exec(url);
|
|
16
|
+
return match?.[1]?.toLowerCase() ?? null;
|
|
17
|
+
}
|
|
8
18
|
/**
|
|
9
19
|
* Downloads an APK to the cache directory and triggers the Android
|
|
10
20
|
* install-package intent. Android only — there is no equivalent flow on iOS,
|
|
11
21
|
* which doesn't allow installing a downloaded package outside the App Store.
|
|
12
22
|
*
|
|
13
|
-
* @throws if
|
|
14
|
-
* match the
|
|
23
|
+
* @throws if `apkUrl` isn't an `https://` URL on one of `allowedHosts`, if
|
|
24
|
+
* the download fails, or if `expectedMd5` is set and doesn't match the
|
|
25
|
+
* downloaded file.
|
|
15
26
|
*/
|
|
16
|
-
export async function downloadAndInstallApk({ apkUrl, fileName, onProgress, expectedMd5, }) {
|
|
27
|
+
export async function downloadAndInstallApk({ apkUrl, fileName, onProgress, expectedMd5, allowedHosts = DEFAULT_ALLOWED_HOSTS, }) {
|
|
28
|
+
const host = httpsHost(apkUrl);
|
|
29
|
+
if (!host ||
|
|
30
|
+
!allowedHosts.some((allowed) => allowed.toLowerCase() === host)) {
|
|
31
|
+
throw new Error(`Refusing to download the APK from ${apkUrl}: expected an https:// URL on ${allowedHosts.join(", ")}.`);
|
|
32
|
+
}
|
|
17
33
|
const destination = new File(Paths.cache, fileName);
|
|
18
34
|
if (destination.exists)
|
|
19
35
|
destination.delete();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forthtilliath/expo-release-updates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"expo-intent-launcher": ">=12.0.0"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@forthtilliath/ts-kit": "^0.
|
|
29
|
+
"@forthtilliath/ts-kit": "^0.12.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@vitest/coverage-v8": "5.0.2",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"expo-intent-launcher": "~57.0.1",
|
|
36
36
|
"typescript": "^5.9.3",
|
|
37
37
|
"vitest": "^5.0.2",
|
|
38
|
-
"@forthtilliath/eslint-config": "0.
|
|
39
|
-
"@forthtilliath/
|
|
40
|
-
"@forthtilliath/
|
|
38
|
+
"@forthtilliath/eslint-config": "0.9.0",
|
|
39
|
+
"@forthtilliath/typescript-config": "0.1.1",
|
|
40
|
+
"@forthtilliath/test-kit": "0.3.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"lint": "eslint . --max-warnings 0",
|