@forthtilliath/expo-release-updates 0.8.0 → 0.10.0
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 +5 -3
- package/dist/githubReleases.d.ts +13 -1
- package/dist/githubReleases.d.ts.map +1 -1
- package/dist/githubReleases.js +41 -20
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -76,12 +76,14 @@ if (release && isUpdateAvailable(currentVersion, release.version)) {
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
### `fetchLatestRelease({ owner, repo, includePrereleases?, token?, timeoutMs?, signal? })`
|
|
79
|
+
### `fetchLatestRelease({ owner, repo, includePrereleases?, requireVerifiedApk?, token?, timeoutMs?, signal? })`
|
|
80
80
|
|
|
81
81
|
Fetches the latest GitHub release and its `.apk` asset: `{ version, notes, apkUrl, apkSha256? }`. `apkSha256` is the SHA-256 GitHub computed when the APK was uploaded — pass it to `downloadAndInstallApk`'s `expectedSha256`. Returns `null` if the latest release has no `.apk` attached; throws if the GitHub API request fails. Pass `token` for private repos, or to raise the API rate limit from 60 to 5000 requests/hour on public ones.
|
|
82
82
|
|
|
83
83
|
GitHub's `/releases/latest` only ever returns a **stable** release: an app that only publishes pre-releases (alpha, beta) would never see an update. Pass `includePrereleases: true` to scan the 10 most recent releases instead (drafts left out) and keep the highest version (semver, `1.0.0-alpha.10` > `1.0.0-alpha.2`) that carries an `.apk`.
|
|
84
84
|
|
|
85
|
+
Pass `requireVerifiedApk: true` to only consider an `.apk` that `downloadAndInstallApk` with `expectedSha256` and `allowedUrlPrefix: releaseDownloadUrlPrefix(...)` would accept: one with a SHA-256, under this repo's release URLs. A release whose APK fails either is skipped (with `includePrereleases`, the next highest version wins), so the app never offers an update the install would then refuse. Every field of GitHub's answer is type-checked and bounded: notes are cut at 100,000 characters, an oversized tag or URL counts as missing, and a release without a tag is ignored.
|
|
86
|
+
|
|
85
87
|
The request gives up after `timeoutMs` (default `15000`) instead of hanging on a bad network, and can be cancelled with `signal` (an `AbortSignal`) — both also apply to `fetchReleaseHistory`.
|
|
86
88
|
|
|
87
89
|
> **Security — the token ships inside your app.** Anything bundled in an APK can be extracted from it, so treat the token as public: use a [fine-grained token](https://github.com/settings/personal-access-tokens) with **read-only "Contents" access to this one repository** and nothing else, never a classic token or one with write scopes. For a public repo, prefer no token at all.
|
|
@@ -93,9 +95,9 @@ if (release && isUpdateAvailable(currentVersion, release.version)) {
|
|
|
93
95
|
}
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
### `fetchReleaseHistory({ owner, repo, limit?, token?, timeoutMs?, signal? })`
|
|
98
|
+
### `fetchReleaseHistory({ owner, repo, limit?, includePrereleases?, token?, timeoutMs?, signal? })`
|
|
97
99
|
|
|
98
|
-
Fetches the most recent published releases (version, notes, publish date), most recent first — draft releases, visible with a token, are left out. `limit` defaults to `10` (clamped to GitHub's 1–100) — useful for a "release history" / "what's new" screen.
|
|
100
|
+
Fetches the most recent published releases (version, notes, publish date), most recent first — draft releases, visible with a token, are left out. `limit` defaults to `10` (clamped to GitHub's 1–100) — useful for a "release history" / "what's new" screen. Pre-releases are listed too, unless `includePrereleases: false` (pass the same value as to `fetchLatestRelease`, so users who don't get test versions don't see them listed either; fewer than `limit` entries may then come back).
|
|
99
101
|
|
|
100
102
|
```ts
|
|
101
103
|
const history = await fetchReleaseHistory({
|
package/dist/githubReleases.d.ts
CHANGED
|
@@ -37,6 +37,15 @@ export interface FetchLatestReleaseOptions extends GithubRepoRef {
|
|
|
37
37
|
* wins. Defaults to `false`.
|
|
38
38
|
*/
|
|
39
39
|
includePrereleases?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Only consider an `.apk` the recommended install would accept: one with a
|
|
42
|
+
* SHA-256 (`apkSha256`, for `downloadAndInstallApk`'s `expectedSha256`)
|
|
43
|
+
* and a URL under `releaseDownloadUrlPrefix(options)` (its
|
|
44
|
+
* `allowedUrlPrefix`). A release whose APK fails either is skipped — with
|
|
45
|
+
* `includePrereleases`, the next highest version wins — so nothing is
|
|
46
|
+
* offered that the install would then refuse. Defaults to `false`.
|
|
47
|
+
*/
|
|
48
|
+
requireVerifiedApk?: boolean;
|
|
40
49
|
}
|
|
41
50
|
export interface ReleaseHistoryEntry {
|
|
42
51
|
version: string;
|
|
@@ -62,9 +71,12 @@ export declare function fetchLatestRelease(options: FetchLatestReleaseOptions):
|
|
|
62
71
|
/**
|
|
63
72
|
* Fetches the most recent published releases for a repo (version, notes,
|
|
64
73
|
* publish date), most recent first — useful for a "release history" screen.
|
|
65
|
-
* Draft releases (only visible with a token) are left out
|
|
74
|
+
* Draft releases (only visible with a token) are left out, and so are
|
|
75
|
+
* pre-releases with `includePrereleases: false` (the list may then hold fewer
|
|
76
|
+
* than `limit` entries).
|
|
66
77
|
*/
|
|
67
78
|
export declare function fetchReleaseHistory(ref: GithubRepoRef & {
|
|
68
79
|
limit?: number;
|
|
80
|
+
includePrereleases?: boolean;
|
|
69
81
|
}): Promise<ReleaseHistoryEntry[]>;
|
|
70
82
|
//# sourceMappingURL=githubReleases.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"githubReleases.d.ts","sourceRoot":"","sources":["../src/githubReleases.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAA0B,SAAQ,aAAa;IAC9D;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"githubReleases.d.ts","sourceRoot":"","sources":["../src/githubReleases.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAA0B,SAAQ,aAAa;IAC9D;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAiCD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,aAAa,UAEtE;AAwGD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAyB/B;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,aAAa,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,GACpE,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAkBhC"}
|
package/dist/githubReleases.js
CHANGED
|
@@ -5,6 +5,10 @@ const MAX_PER_PAGE = 100;
|
|
|
5
5
|
/** Enough to skip a few releases without an APK (notes only, drafts). */
|
|
6
6
|
const PRERELEASE_SCAN = 10;
|
|
7
7
|
const SHA256_DIGEST = /^sha256:([0-9a-f]{64})$/i;
|
|
8
|
+
/** Release notes longer than this are cut: rendered as is, they'd stall the UI. */
|
|
9
|
+
const MAX_NOTES_LENGTH = 100_000;
|
|
10
|
+
/** Tags, asset names, URLs, dates: anything longer is treated as missing. */
|
|
11
|
+
const MAX_FIELD_LENGTH = 2_048;
|
|
8
12
|
function releasesUrl({ owner, repo }) {
|
|
9
13
|
return `https://api.github.com/repos/${owner}/${repo}/releases`;
|
|
10
14
|
}
|
|
@@ -17,7 +21,14 @@ export function releaseDownloadUrlPrefix({ owner, repo }) {
|
|
|
17
21
|
return `https://github.com/${owner}/${repo}/releases/download/`;
|
|
18
22
|
}
|
|
19
23
|
function text(value) {
|
|
20
|
-
return typeof value === "string"
|
|
24
|
+
return typeof value === "string" && value.length <= MAX_FIELD_LENGTH
|
|
25
|
+
? value
|
|
26
|
+
: "";
|
|
27
|
+
}
|
|
28
|
+
function notesOf(release) {
|
|
29
|
+
return typeof release.body === "string"
|
|
30
|
+
? release.body.slice(0, MAX_NOTES_LENGTH)
|
|
31
|
+
: "";
|
|
21
32
|
}
|
|
22
33
|
function isObject(value) {
|
|
23
34
|
return typeof value === "object" && value !== null;
|
|
@@ -28,25 +39,32 @@ function releasesIn(data) {
|
|
|
28
39
|
function versionOf(release) {
|
|
29
40
|
return text(release.tag_name).replace(/^v/, "");
|
|
30
41
|
}
|
|
31
|
-
function apkOf(release) {
|
|
42
|
+
function apkOf(release, options) {
|
|
32
43
|
const assets = Array.isArray(release.assets)
|
|
33
44
|
? release.assets.filter(isObject)
|
|
34
45
|
: [];
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
const prefix = releaseDownloadUrlPrefix(options);
|
|
47
|
+
for (const asset of assets) {
|
|
48
|
+
const apkUrl = text(asset.browser_download_url);
|
|
49
|
+
if (!text(asset.name).endsWith(".apk") || apkUrl === "")
|
|
50
|
+
continue;
|
|
51
|
+
const sha256 = SHA256_DIGEST.exec(text(asset.digest))?.[1]?.toLowerCase();
|
|
52
|
+
if (options.requireVerifiedApk && (!sha256 || !apkUrl.startsWith(prefix))) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
return { apkUrl, ...(sha256 ? { apkSha256: sha256 } : {}) };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
44
58
|
}
|
|
45
|
-
function toLatestRelease(release) {
|
|
46
|
-
const
|
|
59
|
+
function toLatestRelease(release, options) {
|
|
60
|
+
const version = versionOf(release);
|
|
61
|
+
// Without a tag, there's nothing to compare the installed version with.
|
|
62
|
+
if (version === "")
|
|
63
|
+
return null;
|
|
64
|
+
const apk = apkOf(release, options);
|
|
47
65
|
if (!apk)
|
|
48
66
|
return null;
|
|
49
|
-
return { version
|
|
67
|
+
return { version, notes: notesOf(release), ...apk };
|
|
50
68
|
}
|
|
51
69
|
function githubHeaders({ token }) {
|
|
52
70
|
return {
|
|
@@ -106,14 +124,14 @@ async function fetchGithubJson(url, ref) {
|
|
|
106
124
|
export async function fetchLatestRelease(options) {
|
|
107
125
|
if (!options.includePrereleases) {
|
|
108
126
|
const data = await fetchGithubJson(`${releasesUrl(options)}/latest`, options);
|
|
109
|
-
return isObject(data) ? toLatestRelease(data) : null;
|
|
127
|
+
return isObject(data) ? toLatestRelease(data, options) : null;
|
|
110
128
|
}
|
|
111
129
|
const data = await fetchGithubJson(`${releasesUrl(options)}?per_page=${String(PRERELEASE_SCAN)}`, options);
|
|
112
130
|
let latest = null;
|
|
113
131
|
for (const release of releasesIn(data)) {
|
|
114
132
|
if (release.draft === true)
|
|
115
133
|
continue;
|
|
116
|
-
const candidate = toLatestRelease(release);
|
|
134
|
+
const candidate = toLatestRelease(release, options);
|
|
117
135
|
if (candidate &&
|
|
118
136
|
(!latest || compareVersions(candidate.version, latest.version) > 0)) {
|
|
119
137
|
latest = candidate;
|
|
@@ -124,17 +142,20 @@ export async function fetchLatestRelease(options) {
|
|
|
124
142
|
/**
|
|
125
143
|
* Fetches the most recent published releases for a repo (version, notes,
|
|
126
144
|
* publish date), most recent first — useful for a "release history" screen.
|
|
127
|
-
* Draft releases (only visible with a token) are left out
|
|
145
|
+
* Draft releases (only visible with a token) are left out, and so are
|
|
146
|
+
* pre-releases with `includePrereleases: false` (the list may then hold fewer
|
|
147
|
+
* than `limit` entries).
|
|
128
148
|
*/
|
|
129
149
|
export async function fetchReleaseHistory(ref) {
|
|
130
|
-
const { limit = 10 } = ref;
|
|
150
|
+
const { limit = 10, includePrereleases = true } = ref;
|
|
131
151
|
const perPage = Math.min(Math.max(Math.trunc(limit), 1), MAX_PER_PAGE);
|
|
132
152
|
const data = await fetchGithubJson(`${releasesUrl(ref)}?per_page=${String(perPage)}`, ref);
|
|
133
153
|
return releasesIn(data)
|
|
134
|
-
.filter((release) => release.draft !== true
|
|
154
|
+
.filter((release) => release.draft !== true &&
|
|
155
|
+
(includePrereleases || release.prerelease !== true))
|
|
135
156
|
.map((release) => ({
|
|
136
157
|
version: versionOf(release),
|
|
137
|
-
notes:
|
|
158
|
+
notes: notesOf(release),
|
|
138
159
|
publishedAt: text(release.published_at),
|
|
139
160
|
}));
|
|
140
161
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forthtilliath/expo-release-updates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,18 +34,18 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@forthtilliath/ts-kit": "^0.
|
|
37
|
+
"@forthtilliath/ts-kit": "^0.15.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@vitest/coverage-v8": "5.0.
|
|
40
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
41
41
|
"eslint": "10.7.0",
|
|
42
42
|
"expo-file-system": "~57.0.2",
|
|
43
43
|
"expo-intent-launcher": "~57.0.1",
|
|
44
44
|
"typescript": "^5.9.3",
|
|
45
|
-
"vitest": "^5.0.
|
|
46
|
-
"@forthtilliath/eslint-config": "0.
|
|
47
|
-
"@forthtilliath/
|
|
48
|
-
"@forthtilliath/
|
|
45
|
+
"vitest": "^5.0.5",
|
|
46
|
+
"@forthtilliath/eslint-config": "0.10.0",
|
|
47
|
+
"@forthtilliath/test-kit": "0.4.0",
|
|
48
|
+
"@forthtilliath/typescript-config": "0.1.1"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"lint": "eslint . --max-warnings 0",
|