@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 the download fails, or if `expectedMd5` is set and doesn't
22
- * match the downloaded file.
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":"AASA,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;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,MAAM,EACN,QAAQ,EACR,UAAU,EACV,WAAW,GACZ,EAAE,4BAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B9C"}
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 the download fails, or if `expectedMd5` is set and doesn't
14
- * match the downloaded file.
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.5.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.11.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.8.0",
39
- "@forthtilliath/test-kit": "0.3.0",
40
- "@forthtilliath/typescript-config": "0.1.1"
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",