@capuchoo/updater 0.5.0 → 0.6.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/dist/index.d.ts +10 -45
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{ota.service-DPjhi4rw.js → ota.service-DmSAtu9q.js} +11 -46
- package/dist/ota.service-DmSAtu9q.js.map +1 -0
- package/dist/vue.js +1 -1
- package/package.json +2 -2
- package/dist/ota.service-DPjhi4rw.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -161,21 +161,7 @@ declare function describeConfigProblems(config: UpdaterConfig): string[];
|
|
|
161
161
|
/**
|
|
162
162
|
* Which downloaded APK to keep, reuse, or throw away.
|
|
163
163
|
*
|
|
164
|
-
*
|
|
165
|
-
* The previous implementation spent that repeatedly and needlessly:
|
|
166
|
-
*
|
|
167
|
-
* - `downloadNativeUpdate` called `cleanApkCache()` as its first step, which
|
|
168
|
-
* deleted every APK for this app - including the exact file it was about to
|
|
169
|
-
* fetch. A second press always re-downloaded.
|
|
170
|
-
* - The only record that a download had finished was `state.cachedPath`, in
|
|
171
|
-
* memory. Closing the app threw it away, so the next launch downloaded the
|
|
172
|
-
* same bytes again while the file sat on disk.
|
|
173
|
-
* - Nothing ever deleted a successfully installed APK, so it stayed in the
|
|
174
|
-
* cache forever.
|
|
175
|
-
*
|
|
176
|
-
* The decisions are here, as pure functions over file names and sizes, because
|
|
177
|
-
* the alternative is discovering the behaviour on a phone with a data plan.
|
|
178
|
-
* `download.service.ts` supplies the filesystem.
|
|
164
|
+
* Pure over file names and sizes; `download.service.ts` supplies the filesystem.
|
|
179
165
|
*/
|
|
180
166
|
/** Cache file name prefix, derived from the app id so two flavours never collide. */
|
|
181
167
|
declare function cachePrefix(appId: string): string;
|
|
@@ -186,24 +172,15 @@ interface ApkIdentity {
|
|
|
186
172
|
/** `com.efficy.app-1.0.56-67.apk` */
|
|
187
173
|
declare function apkFileName(appId: string, update: ApkIdentity): string;
|
|
188
174
|
/**
|
|
189
|
-
* Reads a name produced by `apkFileName
|
|
190
|
-
*
|
|
191
|
-
* Returns null for anything else in the cache directory - the WebView's own
|
|
192
|
-
* files live there too, and deleting one of those would be a very expensive
|
|
193
|
-
* mistake to make with a regex.
|
|
175
|
+
* Reads a name produced by `apkFileName`, or null for anything else - the
|
|
176
|
+
* WebView's own files share this directory and the caller deletes what we claim.
|
|
194
177
|
*/
|
|
195
178
|
declare function parseApkFileName(appId: string, fileName: string): ApkIdentity | null;
|
|
196
179
|
/**
|
|
197
|
-
* Whether a cached file is the update
|
|
198
|
-
*
|
|
199
|
-
* Size is the check. A download interrupted by a dead connection leaves a
|
|
200
|
-
* partial file at the right path, and reusing that hands the Android installer
|
|
201
|
-
* a truncated APK - which fails with "There was a problem parsing the package",
|
|
202
|
-
* a message that says nothing about the real cause.
|
|
180
|
+
* Whether a cached file is the offered update, complete.
|
|
203
181
|
*
|
|
204
|
-
*
|
|
205
|
-
* the file is not trusted.
|
|
206
|
-
* truncated binary costs a support call.
|
|
182
|
+
* Size is the check: an interrupted download leaves a partial file at the right
|
|
183
|
+
* path. Without an expected size the file is not trusted.
|
|
207
184
|
*/
|
|
208
185
|
declare function isCompleteDownload(cached: {
|
|
209
186
|
size: number;
|
|
@@ -212,23 +189,11 @@ interface CachedApk extends ApkIdentity {
|
|
|
212
189
|
fileName: string;
|
|
213
190
|
}
|
|
214
191
|
/**
|
|
215
|
-
* Which cached APKs to delete
|
|
216
|
-
*
|
|
217
|
-
* Two situations, deliberately one function because they differ by one rule:
|
|
218
|
-
*
|
|
219
|
-
* **Pruning** (no `keep`) removes only what the device has outgrown - anything
|
|
220
|
-
* whose build number the installed binary has caught up with. That is how an
|
|
221
|
-
* installed APK finally gets deleted: the Android installer never calls back,
|
|
222
|
-
* but the next launch reports a higher build number, which says the same thing.
|
|
223
|
-
* A *newer* APK is left alone, because it is an update already downloaded and
|
|
224
|
-
* waiting to be installed, and throwing it away would mean paying for it twice.
|
|
225
|
-
*
|
|
226
|
-
* **Making room** (`keep` given) additionally removes other pending downloads,
|
|
227
|
-
* because the file about to be written is tens of megabytes and the OS will
|
|
228
|
-
* evict from a full cache mid-download.
|
|
192
|
+
* Which cached APKs to delete.
|
|
229
193
|
*
|
|
230
|
-
* `keep
|
|
231
|
-
* downloaded
|
|
194
|
+
* Without `keep`: only what the installed build number has caught up with, so a
|
|
195
|
+
* newer downloaded-but-uninstalled APK survives. With `keep`: also other pending
|
|
196
|
+
* downloads, to make room. `keep` itself is never deleted.
|
|
232
197
|
*/
|
|
233
198
|
declare function apksToDelete(input: {
|
|
234
199
|
cached: CachedApk[];
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/device.ts","../src/api.service.ts","../src/config.ts","../src/apk-cache.ts","../src/install.service.ts"],"mappings":";;;;;;;;;;;;;;iBAgBsB,kBAAkB;;;;;;;;iBAkBlB,oBAAoB;;;;;;;;;iBAmBpB,eAAe;iBAWrB;iBAIA;;;;;;;iBAUM,oBAAoB;;;;;;;;iBAkBpB,qBAAqB;;;;;;;;;iBAmBrB,cAAc;EAAU;EAAoB;;;;;cC7FrD,2BAA2B;WAC7B;EAEG,YAAA;;;cAQD,gCAAgC;WAClC,UAAU;EAEP,YAAA,UAAU;;;;;;;;;;;;UAuCP;EACf;EACA,UAAU,kBAAkB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;iBAYc,kBAAkB,OAAO,cAAc;iBAyBjC,kBAAkB,QAAQ;;;;;;;;iBAmD1B,eACpB,OAAO,eACP,QAAQ,kBACR;EAAY;IACX;;;;;;;;;;;UC3Kc;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;iBAoBc,iBAAiB,MAAM,QAAQ;iBAS/B,oBAAoB;;;;;;;;;;iBAuBpB,uBAAuB,QAAQ
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/device.ts","../src/api.service.ts","../src/config.ts","../src/apk-cache.ts","../src/install.service.ts"],"mappings":";;;;;;;;;;;;;;iBAgBsB,kBAAkB;;;;;;;;iBAkBlB,oBAAoB;;;;;;;;;iBAmBpB,eAAe;iBAWrB;iBAIA;;;;;;;iBAUM,oBAAoB;;;;;;;;iBAkBpB,qBAAqB;;;;;;;;;iBAmBrB,cAAc;EAAU;EAAoB;;;;;cC7FrD,2BAA2B;WAC7B;EAEG,YAAA;;;cAQD,gCAAgC;WAClC,UAAU;EAEP,YAAA,UAAU;;;;;;;;;;;;UAuCP;EACf;EACA,UAAU,kBAAkB;EAC5B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;iBAYc,kBAAkB,OAAO,cAAc;iBAyBjC,kBAAkB,QAAQ;;;;;;;;iBAmD1B,eACpB,OAAO,eACP,QAAQ,kBACR;EAAY;IACX;;;;;;;;;;;UC3Kc;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;iBAoBc,iBAAiB,MAAM,QAAQ;iBAS/B,oBAAoB;;;;;;;;;;iBAuBpB,uBAAuB,QAAQ;;;;;;;;;iBCjE/B,YAAY;UAIX;EACf;EACA;;;iBAIc,YAAY,eAAe,QAAQ;;;;;iBAQnC,iBAAiB,eAAe,mBAAmB;;;;;;;iBAiBnD,mBACd;EAAU;UACV;UAMe,kBAAkB;EACjC;;;;;;;;;iBAUc,aAAa;EAC3B,QAAQ;EACR;EACA;;;;;;;;;;;;iBClDoB,oBAAoB,eAAe"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as getPlatform, D as configureUpdater, E as isNative, O as describeConfigProblems, S as getOsFacts, T as getVersionCode, _ as checkForUpdate, a as openNativeInstaller, b as getBundleVersion, c as pruneApkCache, d as cachePrefix, f as isCompleteDownload, g as buildCheckRequest, h as UpdaterConfigError, i as notifyAppReady, k as getUpdaterConfig, l as apkFileName, m as UpdateCheckBlockedError, n as discardBundle, o as downloadNativeUpdate, p as parseApkFileName, r as getCurrentBundle, s as findCachedApk, t as applyOtaUpdate, u as apksToDelete, v as logUpdateEvent, w as getPluginVersion, x as getDeviceId, y as getBuiltinVersion } from "./ota.service-
|
|
1
|
+
import { C as getPlatform, D as configureUpdater, E as isNative, O as describeConfigProblems, S as getOsFacts, T as getVersionCode, _ as checkForUpdate, a as openNativeInstaller, b as getBundleVersion, c as pruneApkCache, d as cachePrefix, f as isCompleteDownload, g as buildCheckRequest, h as UpdaterConfigError, i as notifyAppReady, k as getUpdaterConfig, l as apkFileName, m as UpdateCheckBlockedError, n as discardBundle, o as downloadNativeUpdate, p as parseApkFileName, r as getCurrentBundle, s as findCachedApk, t as applyOtaUpdate, u as apksToDelete, v as logUpdateEvent, w as getPluginVersion, x as getDeviceId, y as getBuiltinVersion } from "./ota.service-DmSAtu9q.js";
|
|
2
2
|
export { UpdateCheckBlockedError, UpdaterConfigError, apkFileName, apksToDelete, applyOtaUpdate, buildCheckRequest, cachePrefix, checkForUpdate, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, findCachedApk, getBuiltinVersion, getBundleVersion, getCurrentBundle, getDeviceId, getOsFacts, getPlatform, getPluginVersion, getUpdaterConfig, getVersionCode, isCompleteDownload, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller, parseApkFileName, pruneApkCache };
|
|
@@ -286,21 +286,7 @@ async function logUpdateEvent(event, update, details) {
|
|
|
286
286
|
/**
|
|
287
287
|
* Which downloaded APK to keep, reuse, or throw away.
|
|
288
288
|
*
|
|
289
|
-
*
|
|
290
|
-
* The previous implementation spent that repeatedly and needlessly:
|
|
291
|
-
*
|
|
292
|
-
* - `downloadNativeUpdate` called `cleanApkCache()` as its first step, which
|
|
293
|
-
* deleted every APK for this app - including the exact file it was about to
|
|
294
|
-
* fetch. A second press always re-downloaded.
|
|
295
|
-
* - The only record that a download had finished was `state.cachedPath`, in
|
|
296
|
-
* memory. Closing the app threw it away, so the next launch downloaded the
|
|
297
|
-
* same bytes again while the file sat on disk.
|
|
298
|
-
* - Nothing ever deleted a successfully installed APK, so it stayed in the
|
|
299
|
-
* cache forever.
|
|
300
|
-
*
|
|
301
|
-
* The decisions are here, as pure functions over file names and sizes, because
|
|
302
|
-
* the alternative is discovering the behaviour on a phone with a data plan.
|
|
303
|
-
* `download.service.ts` supplies the filesystem.
|
|
289
|
+
* Pure over file names and sizes; `download.service.ts` supplies the filesystem.
|
|
304
290
|
*/
|
|
305
291
|
/** Cache file name prefix, derived from the app id so two flavours never collide. */
|
|
306
292
|
function cachePrefix(appId) {
|
|
@@ -311,11 +297,8 @@ function apkFileName(appId, update) {
|
|
|
311
297
|
return `${cachePrefix(appId)}${update.version}-${update.versionCode}.apk`;
|
|
312
298
|
}
|
|
313
299
|
/**
|
|
314
|
-
* Reads a name produced by `apkFileName
|
|
315
|
-
*
|
|
316
|
-
* Returns null for anything else in the cache directory - the WebView's own
|
|
317
|
-
* files live there too, and deleting one of those would be a very expensive
|
|
318
|
-
* mistake to make with a regex.
|
|
300
|
+
* Reads a name produced by `apkFileName`, or null for anything else - the
|
|
301
|
+
* WebView's own files share this directory and the caller deletes what we claim.
|
|
319
302
|
*/
|
|
320
303
|
function parseApkFileName(appId, fileName) {
|
|
321
304
|
const prefix = cachePrefix(appId);
|
|
@@ -329,39 +312,21 @@ function parseApkFileName(appId, fileName) {
|
|
|
329
312
|
};
|
|
330
313
|
}
|
|
331
314
|
/**
|
|
332
|
-
* Whether a cached file is the update
|
|
333
|
-
*
|
|
334
|
-
* Size is the check. A download interrupted by a dead connection leaves a
|
|
335
|
-
* partial file at the right path, and reusing that hands the Android installer
|
|
336
|
-
* a truncated APK - which fails with "There was a problem parsing the package",
|
|
337
|
-
* a message that says nothing about the real cause.
|
|
315
|
+
* Whether a cached file is the offered update, complete.
|
|
338
316
|
*
|
|
339
|
-
*
|
|
340
|
-
* the file is not trusted.
|
|
341
|
-
* truncated binary costs a support call.
|
|
317
|
+
* Size is the check: an interrupted download leaves a partial file at the right
|
|
318
|
+
* path. Without an expected size the file is not trusted.
|
|
342
319
|
*/
|
|
343
320
|
function isCompleteDownload(cached, expectedSize) {
|
|
344
321
|
if (!cached || !expectedSize) return false;
|
|
345
322
|
return cached.size === expectedSize;
|
|
346
323
|
}
|
|
347
324
|
/**
|
|
348
|
-
* Which cached APKs to delete
|
|
349
|
-
*
|
|
350
|
-
* Two situations, deliberately one function because they differ by one rule:
|
|
351
|
-
*
|
|
352
|
-
* **Pruning** (no `keep`) removes only what the device has outgrown - anything
|
|
353
|
-
* whose build number the installed binary has caught up with. That is how an
|
|
354
|
-
* installed APK finally gets deleted: the Android installer never calls back,
|
|
355
|
-
* but the next launch reports a higher build number, which says the same thing.
|
|
356
|
-
* A *newer* APK is left alone, because it is an update already downloaded and
|
|
357
|
-
* waiting to be installed, and throwing it away would mean paying for it twice.
|
|
358
|
-
*
|
|
359
|
-
* **Making room** (`keep` given) additionally removes other pending downloads,
|
|
360
|
-
* because the file about to be written is tens of megabytes and the OS will
|
|
361
|
-
* evict from a full cache mid-download.
|
|
325
|
+
* Which cached APKs to delete.
|
|
362
326
|
*
|
|
363
|
-
* `keep
|
|
364
|
-
* downloaded
|
|
327
|
+
* Without `keep`: only what the installed build number has caught up with, so a
|
|
328
|
+
* newer downloaded-but-uninstalled APK survives. With `keep`: also other pending
|
|
329
|
+
* downloads, to make room. `keep` itself is never deleted.
|
|
365
330
|
*/
|
|
366
331
|
function apksToDelete(input) {
|
|
367
332
|
const { cached, installedVersionCode, keep } = input;
|
|
@@ -655,4 +620,4 @@ async function discardBundle(bundleId) {
|
|
|
655
620
|
//#endregion
|
|
656
621
|
export { getPlatform as C, configureUpdater as D, isNative as E, describeConfigProblems as O, getOsFacts as S, getVersionCode as T, checkForUpdate as _, openNativeInstaller as a, getBundleVersion as b, pruneApkCache as c, cachePrefix as d, isCompleteDownload as f, buildCheckRequest as g, UpdaterConfigError as h, notifyAppReady as i, getUpdaterConfig as k, apkFileName as l, UpdateCheckBlockedError as m, discardBundle as n, downloadNativeUpdate as o, parseApkFileName as p, getCurrentBundle as r, findCachedApk as s, applyOtaUpdate as t, apksToDelete as u, logUpdateEvent as v, getPluginVersion as w, getDeviceId as x, getBuiltinVersion as y };
|
|
657
622
|
|
|
658
|
-
//# sourceMappingURL=ota.service-
|
|
623
|
+
//# sourceMappingURL=ota.service-DmSAtu9q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ota.service-DmSAtu9q.js","names":[],"sources":["../src/config.ts","../src/device.ts","../src/api.service.ts","../src/apk-cache.ts","../src/optional-plugins.ts","../src/download.service.ts","../src/install.service.ts","../src/ota.service.ts"],"sourcesContent":["/**\n * Runtime configuration for the updater.\n *\n * Values come from the build's `VITE_*` variables, which the CLI injects from\n * the flavour's env file. `configureUpdater` lets an app override any of them\n * at startup - useful for tests and for apps that resolve their endpoint from\n * a login response rather than at build time.\n */\nexport interface UpdaterConfig {\n /** Base URL of the Capuchoo backend, with no trailing slash. */\n apiUrl: string;\n /** Bundle identifier of this build. Must match what the CLI published. */\n appId: string;\n /** Human-readable name, used in prompts and in the APK cache file name. */\n appName: string;\n /** Channel to consult. Bound to an environment server-side. */\n channel: string;\n /** Free-form: an app may use flavours beyond dev/staging/prod. */\n environment: string;\n /** Milliseconds before an update check is abandoned. */\n timeoutMs: number;\n}\n\nfunction env(key: string): string | undefined {\n // `import.meta.env` is replaced at build time by Vite. Guard the access so\n // the module can also be imported from Node (tests, SSR) without throwing.\n const source = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;\n return source?.[key];\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nlet overrides: Partial<UpdaterConfig> = {};\n\n/**\n * Overrides configuration resolved from the build. Call before `init`.\n * Passing `{}` clears previous overrides.\n */\nexport function configureUpdater(next: Partial<UpdaterConfig>): void {\n overrides = { ...overrides, ...next };\n}\n\n/** @internal exposed for tests. */\nexport function resetUpdaterConfig(): void {\n overrides = {};\n}\n\nexport function getUpdaterConfig(): UpdaterConfig {\n const apiUrl = overrides.apiUrl ?? env(\"VITE_UPDATE_API_URL\") ?? \"\";\n\n return {\n apiUrl: stripTrailingSlash(apiUrl),\n appId: overrides.appId ?? env(\"VITE_APP_ID\") ?? \"\",\n appName: overrides.appName ?? env(\"VITE_APP_NAME\") ?? \"the app\",\n channel: overrides.channel ?? env(\"VITE_UPDATE_CHANNEL\") ?? \"prod\",\n environment:\n overrides.environment ?? env(\"VITE_ENVIRONMENT\") ?? (env(\"PROD\") === \"true\" ? \"prod\" : \"dev\"),\n timeoutMs: overrides.timeoutMs ?? 30_000,\n };\n}\n\n/**\n * Reasons the updater cannot run, as user-facing strings.\n *\n * The previous implementation defaulted `apiUrl` to a hard-coded Render URL and\n * `appId` to a hard-coded bundle id. A build with a missing variable therefore\n * silently pointed at somebody else's backend, or asked for the wrong app, and\n * reported \"you are up to date\". Failing loudly is the whole point of this\n * function.\n */\nexport function describeConfigProblems(config: UpdaterConfig): string[] {\n const problems: string[] = [];\n if (!config.apiUrl) {\n problems.push(\"VITE_UPDATE_API_URL is not set, so updates cannot be checked\");\n }\n if (!config.appId) {\n problems.push(\"VITE_APP_ID is not set, so the server cannot identify this build\");\n }\n return problems;\n}\n","import { App } from \"@capacitor/app\";\nimport { Capacitor } from \"@capacitor/core\";\nimport { CapacitorUpdater } from \"@capgo/capacitor-updater\";\n\n/**\n * Facts about the running build that the server needs in order to decide\n * whether an update applies.\n */\n\n/**\n * Native build number of the installed binary.\n *\n * Returns 0 off-device. The old implementation returned 999999 on web, which\n * meant a browser session claimed to be newer than every published release and\n * so never saw an update - masking the very bug you would be debugging.\n */\nexport async function getVersionCode(): Promise<number> {\n if (!Capacitor.isNativePlatform()) return 0;\n\n try {\n const info = await App.getInfo();\n return Number.parseInt(info.build, 10) || 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Semantic version of the web bundle currently applied.\n *\n * `\"builtin\"` means no OTA bundle has been applied yet and the app is running\n * the assets compiled into the binary. The server treats it as 0.0.0, so it\n * must be reported honestly rather than sent as a constant.\n */\nexport async function getBundleVersion(): Promise<string> {\n if (!Capacitor.isNativePlatform()) return \"builtin\";\n\n try {\n const current = await CapacitorUpdater.current();\n return current.bundle.version || \"builtin\";\n } catch {\n return \"builtin\";\n }\n}\n\n/**\n * Stable per-install identifier, supplied by the OTA plugin.\n *\n * The plugin persists this natively. Reading `localStorage.device_id` instead -\n * as the app template did - returns null on a fresh install and is wiped\n * whenever the WebView data is cleared, so channel overrides and per-device\n * stats silently stopped working.\n */\nexport async function getDeviceId(): Promise<string> {\n if (!Capacitor.isNativePlatform()) return \"web\";\n\n try {\n const { deviceId } = await CapacitorUpdater.getDeviceId();\n return deviceId || \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\nexport function getPlatform(): \"android\" | \"ios\" | \"web\" {\n return Capacitor.getPlatform() as \"android\" | \"ios\" | \"web\";\n}\n\nexport function isNative(): boolean {\n return Capacitor.isNativePlatform();\n}\n\n/**\n * Version of the OTA plugin the app is running.\n *\n * Useful when a device misbehaves: plugin version explains more failures than\n * app version does.\n */\nexport async function getPluginVersion(): Promise<string | undefined> {\n if (!Capacitor.isNativePlatform()) return undefined;\n\n try {\n const { version } = await CapacitorUpdater.getPluginVersion();\n return version || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Bundle version compiled into the binary.\n *\n * Distinct from `getBundleVersion()`, which reports the OTA bundle currently\n * applied - that one says `\"builtin\"` when none has been, and this says which\n * builtin that is.\n */\nexport async function getBuiltinVersion(): Promise<string | undefined> {\n if (!Capacitor.isNativePlatform()) return undefined;\n\n try {\n const { version } = await CapacitorUpdater.getBuiltinVersion();\n return version || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * OS version and emulator flag, from `@capacitor/device`.\n *\n * That package is an *optional* peer: it is a separate install, and an app that\n * does not have it should still be able to check for updates. So it is imported\n * dynamically and every failure - not installed, not registered, throwing on an\n * odd platform - resolves to `undefined`, which the request builder omits.\n */\nexport async function getOsFacts(): Promise<{ versionOs?: string; isEmulator?: boolean }> {\n if (!Capacitor.isNativePlatform()) return {};\n\n try {\n const { Device } = await import(\"@capacitor/device\");\n const info = await Device.getInfo();\n return {\n ...(info.osVersion ? { versionOs: info.osVersion } : {}),\n ...(typeof info.isVirtual === \"boolean\" ? { isEmulator: info.isVirtual } : {}),\n };\n } catch {\n return {};\n }\n}\n","import {\n isBlockingResponse,\n resolveUpdate,\n type ResolvedUpdate,\n type UpdateCheckRequest,\n type UpdateCheckResponse,\n type UpdateEvent,\n type UpdateEventPayload,\n} from \"@capuchoo/core\";\nimport { getUpdaterConfig, describeConfigProblems } from \"./config.js\";\nimport {\n getBuiltinVersion,\n getBundleVersion,\n getDeviceId,\n getOsFacts,\n getPlatform,\n getPluginVersion,\n getVersionCode,\n isNative,\n} from \"./device.js\";\n\n/** Raised when the updater is misconfigured, rather than reporting \"up to date\". */\nexport class UpdaterConfigError extends Error {\n readonly problems: string[];\n\n constructor(problems: string[]) {\n super(problems.join(\"; \"));\n this.name = \"UpdaterConfigError\";\n this.problems = problems;\n }\n}\n\n/** Raised when the server says the request itself cannot be served. */\nexport class UpdateCheckBlockedError extends Error {\n readonly response: UpdateCheckResponse;\n\n constructor(response: UpdateCheckResponse) {\n super(response.message ?? \"The update service rejected this request\");\n this.name = \"UpdateCheckBlockedError\";\n this.response = response;\n }\n}\n\nasync function postJson<T>(url: string, body: unknown, timeoutMs: number): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(`${url} responded ${response.status} ${response.statusText}`);\n }\n\n return (await response.json()) as T;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Asks the server what this device should be running.\n *\n * One request, one endpoint. The app template used to call\n * `GET /api/native-updates/check` for native updates *and*\n * `POST /api/update` for OTA, which meant two sources of truth: the native\n * endpoint ignores the channel's assigned native version and the\n * `min_update_version` gate, so a device could be told to install an OTA\n * bundle its binary was too old to run.\n */\nexport interface DeviceFacts {\n appId: string;\n platform: ReturnType<typeof getPlatform>;\n channel?: string | undefined;\n isProd: boolean;\n versionCode: number;\n versionName: string;\n deviceId: string;\n pluginVersion?: string | undefined;\n versionBuiltin?: string | undefined;\n versionOs?: string | undefined;\n isEmulator?: boolean | undefined;\n customId?: string | undefined;\n}\n\n/**\n * Assemble the check payload.\n *\n * Pure, and separate from the plugin calls that gather the facts, so the shape\n * of what goes on the wire can be tested without a device. Fields the app could\n * not determine are *omitted* rather than sent empty: the server writes only the\n * keys it receives, so a placeholder would overwrite a better value that an\n * earlier, better-informed check had already stored.\n */\nexport function buildCheckRequest(facts: DeviceFacts): UpdateCheckRequest {\n const request: UpdateCheckRequest = {\n appId: facts.appId,\n platform: facts.platform,\n versionCode: String(facts.versionCode),\n // Historical alias. The server accepts either and prefers versionCode.\n versionBuild: String(facts.versionCode),\n version_name: facts.versionName,\n deviceId: facts.deviceId,\n isProd: facts.isProd,\n };\n\n if (facts.channel) {\n request.channel = facts.channel;\n request.defaultChannel = facts.channel;\n }\n if (facts.pluginVersion) request.pluginVersion = facts.pluginVersion;\n if (facts.versionBuiltin) request.versionBuiltin = facts.versionBuiltin;\n if (facts.versionOs) request.versionOs = facts.versionOs;\n if (typeof facts.isEmulator === \"boolean\") request.isEmulator = facts.isEmulator;\n if (facts.customId) request.customId = facts.customId;\n\n return request;\n}\n\nexport async function checkForUpdate(): Promise<ResolvedUpdate | null> {\n if (!isNative()) return null;\n\n const config = getUpdaterConfig();\n const problems = describeConfigProblems(config);\n if (problems.length > 0) throw new UpdaterConfigError(problems);\n\n const [versionCode, versionName, deviceId, pluginVersion, versionBuiltin, osFacts] =\n await Promise.all([\n getVersionCode(),\n getBundleVersion(),\n getDeviceId(),\n getPluginVersion(),\n getBuiltinVersion(),\n getOsFacts(),\n ]);\n\n const request = buildCheckRequest({\n appId: config.appId,\n platform: getPlatform(),\n channel: config.channel,\n isProd: config.environment === \"prod\",\n versionCode,\n versionName,\n deviceId,\n pluginVersion,\n versionBuiltin,\n ...osFacts,\n });\n\n const response = await postJson<UpdateCheckResponse>(\n `${config.apiUrl}/api/update`,\n request,\n config.timeoutMs,\n );\n\n // \"Channel not found\" and \"Environment mismatch\" are deployment mistakes.\n // Treating them as \"no update\" is how a broken channel goes unnoticed for\n // weeks.\n if (isBlockingResponse(response)) throw new UpdateCheckBlockedError(response);\n\n return resolveUpdate(response);\n}\n\n/**\n * Records a native update lifecycle event.\n *\n * Best effort: analytics must never break an update. OTA events are reported\n * by the plugin itself through its `statsUrl`, so only native ones are sent\n * from here.\n */\nexport async function logUpdateEvent(\n event: UpdateEvent,\n update: ResolvedUpdate,\n details?: { error?: string },\n): Promise<void> {\n if (update.kind !== \"native\") return;\n\n const config = getUpdaterConfig();\n if (!config.apiUrl) return;\n\n const payload: UpdateEventPayload = {\n event,\n platform: getPlatform(),\n // Without this the server has no row to attach the event to and answers\n // 400, which is what it did for every native event ever sent.\n app_id: config.appId,\n device_id: await getDeviceId(),\n current_version_code: await getVersionCode(),\n new_version: update.version,\n new_version_code: update.versionCode,\n channel: config.channel,\n environment: String(config.environment),\n ...details,\n };\n\n try {\n await postJson(`${config.apiUrl}/api/native-updates/log`, payload, config.timeoutMs);\n } catch (error) {\n console.warn(\"[capuchoo] could not record update event\", error);\n }\n}\n","/**\n * Which downloaded APK to keep, reuse, or throw away.\n *\n * Pure over file names and sizes; `download.service.ts` supplies the filesystem.\n */\n\n/** Cache file name prefix, derived from the app id so two flavours never collide. */\nexport function cachePrefix(appId: string): string {\n return `${appId.replaceAll(/[^\\w.-]/g, \"-\")}-`;\n}\n\nexport interface ApkIdentity {\n version: string;\n versionCode: number;\n}\n\n/** `com.efficy.app-1.0.56-67.apk` */\nexport function apkFileName(appId: string, update: ApkIdentity): string {\n return `${cachePrefix(appId)}${update.version}-${update.versionCode}.apk`;\n}\n\n/**\n * Reads a name produced by `apkFileName`, or null for anything else - the\n * WebView's own files share this directory and the caller deletes what we claim.\n */\nexport function parseApkFileName(appId: string, fileName: string): ApkIdentity | null {\n const prefix = cachePrefix(appId);\n if (!fileName.startsWith(prefix) || !fileName.endsWith(\".apk\")) return null;\n\n const middle = fileName.slice(prefix.length, -\".apk\".length);\n const match = /^(.+)-(\\d+)$/.exec(middle);\n if (!match) return null;\n\n return { version: match[1]!, versionCode: Number(match[2]) };\n}\n\n/**\n * Whether a cached file is the offered update, complete.\n *\n * Size is the check: an interrupted download leaves a partial file at the right\n * path. Without an expected size the file is not trusted.\n */\nexport function isCompleteDownload(\n cached: { size: number } | null,\n expectedSize: number | undefined,\n): boolean {\n if (!cached || !expectedSize) return false;\n return cached.size === expectedSize;\n}\n\nexport interface CachedApk extends ApkIdentity {\n fileName: string;\n}\n\n/**\n * Which cached APKs to delete.\n *\n * Without `keep`: only what the installed build number has caught up with, so a\n * newer downloaded-but-uninstalled APK survives. With `keep`: also other pending\n * downloads, to make room. `keep` itself is never deleted.\n */\nexport function apksToDelete(input: {\n cached: CachedApk[];\n installedVersionCode: number;\n keep?: string | undefined;\n}): string[] {\n const { cached, installedVersionCode, keep } = input;\n const makingRoom = keep !== undefined;\n\n return cached\n .filter((apk) => {\n if (apk.fileName === keep) return false;\n if (apk.versionCode <= installedVersionCode) return true;\n return makingRoom;\n })\n .map((apk) => apk.fileName);\n}\n","/**\n * Plugins only the native-update path needs, loaded when it runs.\n *\n * OTA updates need `@capgo/capacitor-updater` and nothing else. Downloading and\n * installing an APK additionally needs file transfer, filesystem, network and a\n * file opener - four packages an app that only ships web bundles should not have\n * to install.\n *\n * They are optional peers, imported here rather than at module load, so\n * importing this library does not require them. A missing one produces a message\n * naming what to install instead of `Cannot find module` from inside a bundler.\n *\n * They cannot be plain dependencies: `cap sync` discovers plugins by reading the\n * *application's* `dependencies` and `devDependencies` - `getDependencies()` in\n * @capacitor/cli does not recurse - so a plugin pulled in transitively would\n * have its JavaScript installed and its native half never added to the Android\n * or iOS project. `capuchoo setup --native` adds them to the app instead.\n */\n\nconst NATIVE_PACKAGES = [\n \"@capacitor/file-transfer\",\n \"@capacitor/filesystem\",\n \"@capacitor/network\",\n \"@capawesome-team/capacitor-file-opener\",\n] as const;\n\nexport class MissingNativePluginsError extends Error {\n readonly packages: readonly string[];\n\n constructor(missing: string) {\n super(\n `Native updates need ${missing}, which is not installed. ` +\n `Run: npx capuchoo setup --native (adds ${NATIVE_PACKAGES.join(\", \")} and runs cap sync). ` +\n \"OTA updates do not need any of them.\",\n );\n this.name = \"MissingNativePluginsError\";\n this.packages = NATIVE_PACKAGES;\n }\n}\n\nasync function load<T>(specifier: string, importer: () => Promise<T>): Promise<T> {\n try {\n return await importer();\n } catch (error) {\n // A genuine runtime failure inside the plugin should not be reported as a\n // missing install, so only a resolution failure is translated.\n const message = error instanceof Error ? error.message : String(error);\n if (/cannot find module|failed to resolve|module not found/i.test(message)) {\n throw new MissingNativePluginsError(specifier);\n }\n throw error;\n }\n}\n\nexport const nativePlugins = {\n fileTransfer: () => load(\"@capacitor/file-transfer\", () => import(\"@capacitor/file-transfer\")),\n filesystem: () => load(\"@capacitor/filesystem\", () => import(\"@capacitor/filesystem\")),\n network: () => load(\"@capacitor/network\", () => import(\"@capacitor/network\")),\n fileOpener: () =>\n load(\n \"@capawesome-team/capacitor-file-opener\",\n () => import(\"@capawesome-team/capacitor-file-opener\"),\n ),\n};\n","import type { PluginListenerHandle } from \"@capacitor/core\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport {\n apkFileName,\n apksToDelete,\n isCompleteDownload,\n parseApkFileName,\n type CachedApk,\n} from \"./apk-cache.js\";\nimport { getUpdaterConfig } from \"./config.js\";\nimport { nativePlugins } from \"./optional-plugins.js\";\n\nexport interface DownloadProgress {\n loaded: number;\n total: number;\n percent: number;\n}\n\nconst DONE = { loaded: 0, total: 0, percent: 100 };\n\nfunction fileNameFor(update: ResolvedUpdate): string {\n const { appId, appName } = getUpdaterConfig();\n return apkFileName(appId || appName, {\n version: update.version,\n versionCode: update.versionCode ?? 0,\n });\n}\n\n/** Every APK in the cache that belongs to this app. */\nasync function listCachedApks(): Promise<CachedApk[]> {\n const { appId, appName } = getUpdaterConfig();\n const owner = appId || appName;\n\n try {\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n const { files } = await Filesystem.readdir({ directory: Directory.Cache, path: \"\" });\n\n return files.flatMap((file) => {\n const identity = parseApkFileName(owner, file.name);\n return identity ? [{ ...identity, fileName: file.name }] : [];\n });\n } catch {\n // A cache we cannot read is a cache we cannot reuse or prune, and neither\n // is worth failing an update over.\n return [];\n }\n}\n\n/**\n * The path of an already-downloaded, complete copy of this update.\n *\n * The only record that a download had finished used to be an in-memory\n * `cachedPath`, so closing the app threw it away and the next launch downloaded\n * the same 45 MB again while the file sat on disk. Asking the filesystem\n * survives a restart.\n */\nexport async function findCachedApk(update: ResolvedUpdate): Promise<string | null> {\n const fileName = fileNameFor(update);\n\n try {\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n\n const stat = await Filesystem.stat({ directory: Directory.Cache, path: fileName });\n if (!isCompleteDownload({ size: stat.size }, update.fileSize)) return null;\n\n const { uri } = await Filesystem.getUri({ directory: Directory.Cache, path: fileName });\n return uri;\n } catch {\n // stat throws when the file is not there, which is the common case.\n return null;\n }\n}\n\n/**\n * Downloads a native APK into the app cache and returns its path.\n *\n * Returns immediately when a complete copy is already there. The previous\n * implementation could not: its first step was `cleanApkCache()`, which deleted\n * every APK for this app - including the exact file it was about to fetch - so\n * pressing update twice always paid for the binary twice.\n */\nexport async function downloadNativeUpdate(\n update: ResolvedUpdate,\n onProgress: (progress: DownloadProgress) => void,\n): Promise<string> {\n if (!update.downloadUrl) {\n throw new Error(\"This update has no download URL\");\n }\n\n const existing = await findCachedApk(update);\n if (existing) {\n onProgress({ ...DONE });\n return existing;\n }\n\n const { Network } = await nativePlugins.network();\n const network = await Network.getStatus();\n if (!network.connected) {\n throw new Error(\"Connect to the internet to download this update\");\n }\n\n const fileName = fileNameFor(update);\n\n // Make room, but never for the file being written. APKs are tens of megabytes\n // and the OS can evict from a full cache mid-download.\n await pruneApkCache({ installedVersionCode: 0, keep: fileName });\n\n const [{ Directory, Filesystem }, { FileTransfer }] = await Promise.all([\n nativePlugins.filesystem(),\n nativePlugins.fileTransfer(),\n ]);\n\n const destination = await Filesystem.getUri({ directory: Directory.Cache, path: fileName });\n\n let progressListener: PluginListenerHandle | null = null;\n\n try {\n progressListener = await FileTransfer.addListener(\"progress\", (event) => {\n if (event.url !== update.downloadUrl) return;\n\n const percent =\n event.lengthComputable && event.contentLength > 0\n ? Math.round((event.bytes / event.contentLength) * 100)\n : 0;\n onProgress({ loaded: event.bytes, total: event.contentLength, percent });\n });\n\n const result = await FileTransfer.downloadFile({\n url: update.downloadUrl,\n path: destination.uri,\n progress: true,\n connectTimeout: 60_000,\n readTimeout: 300_000,\n });\n\n return result.path || destination.uri;\n } finally {\n await progressListener?.remove();\n }\n}\n\n/**\n * Deletes cached APKs the device has outgrown.\n *\n * Called on start-up with the installed build number, which is how an installed\n * APK is finally cleaned up: the Android installer never calls back, but the\n * next launch reports a higher build number and that says the same thing. A\n * *newer* APK is kept - it is an update already paid for and waiting to be\n * installed.\n *\n * Failures are non-fatal. Reclaiming disk space must never break an update.\n */\nexport async function pruneApkCache(options: {\n installedVersionCode: number;\n keep?: string | undefined;\n}): Promise<string[]> {\n try {\n const cached = await listCachedApks();\n const doomed = apksToDelete({ cached, ...options });\n if (doomed.length === 0) return [];\n\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n await Promise.all(\n doomed.map((fileName) =>\n Filesystem.deleteFile({ directory: Directory.Cache, path: fileName }),\n ),\n );\n\n return doomed;\n } catch (error) {\n console.warn(\"[capuchoo] could not prune the APK cache\", error);\n return [];\n }\n}\n","import { Capacitor } from \"@capacitor/core\";\nimport { getUpdaterConfig } from \"./config.js\";\nimport { nativePlugins } from \"./optional-plugins.js\";\n\nconst APK_MIME = \"application/vnd.android.package-archive\";\n\n/**\n * Hands a downloaded APK to the Android package installer.\n *\n * Requires `REQUEST_INSTALL_PACKAGES` in the manifest - the Trapeze config for\n * each flavour merges it in - and the user must have allowed this app to\n * install unknown apps. Both failures surface as opaque platform errors, so\n * they are translated into something a user can act on.\n */\nexport async function openNativeInstaller(path: string): Promise<void> {\n if (Capacitor.getPlatform() !== \"android\") {\n throw new Error(\n \"Installing a native update from inside the app is only possible on Android. \" +\n \"On iOS the update has to come from the App Store or TestFlight.\",\n );\n }\n\n const { FileOpener } = await nativePlugins.fileOpener();\n\n try {\n await FileOpener.openFile({ path, mimeType: APK_MIME });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const { appName } = getUpdaterConfig();\n\n if (/permission|unknown sources|REQUEST_INSTALL_PACKAGES/i.test(message)) {\n throw new Error(\n `Allow ${appName} to install unknown apps in Android settings, then try again`,\n );\n }\n if (/activity|no app/i.test(message)) {\n throw new Error(\n \"No package installer is available on this device, so the update cannot be installed here\",\n );\n }\n\n throw new Error(`Could not open the Android installer: ${message}`);\n }\n}\n","import { CapacitorUpdater } from \"@capgo/capacitor-updater\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport { isNative } from \"./device.js\";\n\n/**\n * Thin wrapper over the OTA plugin.\n *\n * Downloading and applying a web bundle stays with `@capgo/capacitor-updater`:\n * it owns the native bundle store, the atomic swap and the rollback. This\n * module only sequences those calls correctly.\n */\n\n/**\n * Confirms the current bundle booted successfully.\n *\n * **This must be called once, early, on every app start.** If the plugin does\n * not hear it within `appReadyTimeout`, it assumes the new bundle crashed and\n * rolls back to the previous one - which looks exactly like \"the update did\n * not install\".\n */\nexport async function notifyAppReady(): Promise<void> {\n if (!isNative()) return;\n\n try {\n await CapacitorUpdater.notifyAppReady();\n } catch (error) {\n console.warn(\"[capuchoo] could not mark this bundle as ready\", error);\n }\n}\n\n/** The bundle currently applied, or null off-device. */\nexport async function getCurrentBundle() {\n if (!isNative()) return null;\n\n try {\n return await CapacitorUpdater.current();\n } catch (error) {\n console.warn(\"[capuchoo] could not read the current bundle\", error);\n return null;\n }\n}\n\n/**\n * Downloads an OTA bundle and applies it.\n *\n * `set` swaps the active bundle and reloads the WebView, so nothing after it\n * runs. It is called last on purpose.\n */\nexport async function applyOtaUpdate(update: ResolvedUpdate): Promise<void> {\n if (update.kind !== \"ota\") {\n throw new Error(\"applyOtaUpdate was given a native update\");\n }\n\n // Already downloaded - either by a previous attempt in this session, or by\n // the plugin's own background download when autoUpdate is \"onlyDownload\".\n if (update.bundleId) {\n await CapacitorUpdater.set({ id: update.bundleId });\n return;\n }\n\n if (!update.downloadUrl) {\n throw new Error(\"This update has no download URL\");\n }\n\n const bundle = await CapacitorUpdater.download({\n url: update.downloadUrl,\n version: update.version,\n ...(update.checksum ? { checksum: update.checksum } : {}),\n ...(update.sessionKey ? { sessionKey: update.sessionKey } : {}),\n });\n\n update.bundleId = bundle.id;\n await CapacitorUpdater.set({ id: bundle.id });\n}\n\n/** Discards a downloaded bundle that will not be applied. */\nexport async function discardBundle(bundleId: string): Promise<void> {\n if (!isNative()) return;\n\n try {\n await CapacitorUpdater.delete({ id: bundleId });\n } catch (error) {\n console.warn(\"[capuchoo] could not delete bundle\", bundleId, error);\n }\n}\n"],"mappings":"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"}
|
package/dist/vue.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { E as isNative, _ as checkForUpdate, a as openNativeInstaller, c as pruneApkCache, h as UpdaterConfigError, i as notifyAppReady, k as getUpdaterConfig, m as UpdateCheckBlockedError, o as downloadNativeUpdate, r as getCurrentBundle, s as findCachedApk, t as applyOtaUpdate, v as logUpdateEvent } from "./ota.service-
|
|
1
|
+
import { E as isNative, _ as checkForUpdate, a as openNativeInstaller, c as pruneApkCache, h as UpdaterConfigError, i as notifyAppReady, k as getUpdaterConfig, m as UpdateCheckBlockedError, o as downloadNativeUpdate, r as getCurrentBundle, s as findCachedApk, t as applyOtaUpdate, v as logUpdateEvent } from "./ota.service-DmSAtu9q.js";
|
|
2
2
|
import { CapacitorUpdater } from "@capgo/capacitor-updater";
|
|
3
3
|
import { computed, readonly, ref } from "vue";
|
|
4
4
|
//#region src/vue/useUpdater.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capuchoo/updater",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "App-side runtime for Capucho OTA and native updates in Capacitor apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"capacitor",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@capuchoo/core": "^0.
|
|
45
|
+
"@capuchoo/core": "^0.4.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@capacitor/app": "^8.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ota.service-DPjhi4rw.js","names":[],"sources":["../src/config.ts","../src/device.ts","../src/api.service.ts","../src/apk-cache.ts","../src/optional-plugins.ts","../src/download.service.ts","../src/install.service.ts","../src/ota.service.ts"],"sourcesContent":["/**\n * Runtime configuration for the updater.\n *\n * Values come from the build's `VITE_*` variables, which the CLI injects from\n * the flavour's env file. `configureUpdater` lets an app override any of them\n * at startup - useful for tests and for apps that resolve their endpoint from\n * a login response rather than at build time.\n */\nexport interface UpdaterConfig {\n /** Base URL of the Capuchoo backend, with no trailing slash. */\n apiUrl: string;\n /** Bundle identifier of this build. Must match what the CLI published. */\n appId: string;\n /** Human-readable name, used in prompts and in the APK cache file name. */\n appName: string;\n /** Channel to consult. Bound to an environment server-side. */\n channel: string;\n /** Free-form: an app may use flavours beyond dev/staging/prod. */\n environment: string;\n /** Milliseconds before an update check is abandoned. */\n timeoutMs: number;\n}\n\nfunction env(key: string): string | undefined {\n // `import.meta.env` is replaced at build time by Vite. Guard the access so\n // the module can also be imported from Node (tests, SSR) without throwing.\n const source = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;\n return source?.[key];\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nlet overrides: Partial<UpdaterConfig> = {};\n\n/**\n * Overrides configuration resolved from the build. Call before `init`.\n * Passing `{}` clears previous overrides.\n */\nexport function configureUpdater(next: Partial<UpdaterConfig>): void {\n overrides = { ...overrides, ...next };\n}\n\n/** @internal exposed for tests. */\nexport function resetUpdaterConfig(): void {\n overrides = {};\n}\n\nexport function getUpdaterConfig(): UpdaterConfig {\n const apiUrl = overrides.apiUrl ?? env(\"VITE_UPDATE_API_URL\") ?? \"\";\n\n return {\n apiUrl: stripTrailingSlash(apiUrl),\n appId: overrides.appId ?? env(\"VITE_APP_ID\") ?? \"\",\n appName: overrides.appName ?? env(\"VITE_APP_NAME\") ?? \"the app\",\n channel: overrides.channel ?? env(\"VITE_UPDATE_CHANNEL\") ?? \"prod\",\n environment:\n overrides.environment ?? env(\"VITE_ENVIRONMENT\") ?? (env(\"PROD\") === \"true\" ? \"prod\" : \"dev\"),\n timeoutMs: overrides.timeoutMs ?? 30_000,\n };\n}\n\n/**\n * Reasons the updater cannot run, as user-facing strings.\n *\n * The previous implementation defaulted `apiUrl` to a hard-coded Render URL and\n * `appId` to a hard-coded bundle id. A build with a missing variable therefore\n * silently pointed at somebody else's backend, or asked for the wrong app, and\n * reported \"you are up to date\". Failing loudly is the whole point of this\n * function.\n */\nexport function describeConfigProblems(config: UpdaterConfig): string[] {\n const problems: string[] = [];\n if (!config.apiUrl) {\n problems.push(\"VITE_UPDATE_API_URL is not set, so updates cannot be checked\");\n }\n if (!config.appId) {\n problems.push(\"VITE_APP_ID is not set, so the server cannot identify this build\");\n }\n return problems;\n}\n","import { App } from \"@capacitor/app\";\nimport { Capacitor } from \"@capacitor/core\";\nimport { CapacitorUpdater } from \"@capgo/capacitor-updater\";\n\n/**\n * Facts about the running build that the server needs in order to decide\n * whether an update applies.\n */\n\n/**\n * Native build number of the installed binary.\n *\n * Returns 0 off-device. The old implementation returned 999999 on web, which\n * meant a browser session claimed to be newer than every published release and\n * so never saw an update - masking the very bug you would be debugging.\n */\nexport async function getVersionCode(): Promise<number> {\n if (!Capacitor.isNativePlatform()) return 0;\n\n try {\n const info = await App.getInfo();\n return Number.parseInt(info.build, 10) || 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * Semantic version of the web bundle currently applied.\n *\n * `\"builtin\"` means no OTA bundle has been applied yet and the app is running\n * the assets compiled into the binary. The server treats it as 0.0.0, so it\n * must be reported honestly rather than sent as a constant.\n */\nexport async function getBundleVersion(): Promise<string> {\n if (!Capacitor.isNativePlatform()) return \"builtin\";\n\n try {\n const current = await CapacitorUpdater.current();\n return current.bundle.version || \"builtin\";\n } catch {\n return \"builtin\";\n }\n}\n\n/**\n * Stable per-install identifier, supplied by the OTA plugin.\n *\n * The plugin persists this natively. Reading `localStorage.device_id` instead -\n * as the app template did - returns null on a fresh install and is wiped\n * whenever the WebView data is cleared, so channel overrides and per-device\n * stats silently stopped working.\n */\nexport async function getDeviceId(): Promise<string> {\n if (!Capacitor.isNativePlatform()) return \"web\";\n\n try {\n const { deviceId } = await CapacitorUpdater.getDeviceId();\n return deviceId || \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\nexport function getPlatform(): \"android\" | \"ios\" | \"web\" {\n return Capacitor.getPlatform() as \"android\" | \"ios\" | \"web\";\n}\n\nexport function isNative(): boolean {\n return Capacitor.isNativePlatform();\n}\n\n/**\n * Version of the OTA plugin the app is running.\n *\n * Useful when a device misbehaves: plugin version explains more failures than\n * app version does.\n */\nexport async function getPluginVersion(): Promise<string | undefined> {\n if (!Capacitor.isNativePlatform()) return undefined;\n\n try {\n const { version } = await CapacitorUpdater.getPluginVersion();\n return version || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Bundle version compiled into the binary.\n *\n * Distinct from `getBundleVersion()`, which reports the OTA bundle currently\n * applied - that one says `\"builtin\"` when none has been, and this says which\n * builtin that is.\n */\nexport async function getBuiltinVersion(): Promise<string | undefined> {\n if (!Capacitor.isNativePlatform()) return undefined;\n\n try {\n const { version } = await CapacitorUpdater.getBuiltinVersion();\n return version || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * OS version and emulator flag, from `@capacitor/device`.\n *\n * That package is an *optional* peer: it is a separate install, and an app that\n * does not have it should still be able to check for updates. So it is imported\n * dynamically and every failure - not installed, not registered, throwing on an\n * odd platform - resolves to `undefined`, which the request builder omits.\n */\nexport async function getOsFacts(): Promise<{ versionOs?: string; isEmulator?: boolean }> {\n if (!Capacitor.isNativePlatform()) return {};\n\n try {\n const { Device } = await import(\"@capacitor/device\");\n const info = await Device.getInfo();\n return {\n ...(info.osVersion ? { versionOs: info.osVersion } : {}),\n ...(typeof info.isVirtual === \"boolean\" ? { isEmulator: info.isVirtual } : {}),\n };\n } catch {\n return {};\n }\n}\n","import {\n isBlockingResponse,\n resolveUpdate,\n type ResolvedUpdate,\n type UpdateCheckRequest,\n type UpdateCheckResponse,\n type UpdateEvent,\n type UpdateEventPayload,\n} from \"@capuchoo/core\";\nimport { getUpdaterConfig, describeConfigProblems } from \"./config.js\";\nimport {\n getBuiltinVersion,\n getBundleVersion,\n getDeviceId,\n getOsFacts,\n getPlatform,\n getPluginVersion,\n getVersionCode,\n isNative,\n} from \"./device.js\";\n\n/** Raised when the updater is misconfigured, rather than reporting \"up to date\". */\nexport class UpdaterConfigError extends Error {\n readonly problems: string[];\n\n constructor(problems: string[]) {\n super(problems.join(\"; \"));\n this.name = \"UpdaterConfigError\";\n this.problems = problems;\n }\n}\n\n/** Raised when the server says the request itself cannot be served. */\nexport class UpdateCheckBlockedError extends Error {\n readonly response: UpdateCheckResponse;\n\n constructor(response: UpdateCheckResponse) {\n super(response.message ?? \"The update service rejected this request\");\n this.name = \"UpdateCheckBlockedError\";\n this.response = response;\n }\n}\n\nasync function postJson<T>(url: string, body: unknown, timeoutMs: number): Promise<T> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n throw new Error(`${url} responded ${response.status} ${response.statusText}`);\n }\n\n return (await response.json()) as T;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Asks the server what this device should be running.\n *\n * One request, one endpoint. The app template used to call\n * `GET /api/native-updates/check` for native updates *and*\n * `POST /api/update` for OTA, which meant two sources of truth: the native\n * endpoint ignores the channel's assigned native version and the\n * `min_update_version` gate, so a device could be told to install an OTA\n * bundle its binary was too old to run.\n */\nexport interface DeviceFacts {\n appId: string;\n platform: ReturnType<typeof getPlatform>;\n channel?: string | undefined;\n isProd: boolean;\n versionCode: number;\n versionName: string;\n deviceId: string;\n pluginVersion?: string | undefined;\n versionBuiltin?: string | undefined;\n versionOs?: string | undefined;\n isEmulator?: boolean | undefined;\n customId?: string | undefined;\n}\n\n/**\n * Assemble the check payload.\n *\n * Pure, and separate from the plugin calls that gather the facts, so the shape\n * of what goes on the wire can be tested without a device. Fields the app could\n * not determine are *omitted* rather than sent empty: the server writes only the\n * keys it receives, so a placeholder would overwrite a better value that an\n * earlier, better-informed check had already stored.\n */\nexport function buildCheckRequest(facts: DeviceFacts): UpdateCheckRequest {\n const request: UpdateCheckRequest = {\n appId: facts.appId,\n platform: facts.platform,\n versionCode: String(facts.versionCode),\n // Historical alias. The server accepts either and prefers versionCode.\n versionBuild: String(facts.versionCode),\n version_name: facts.versionName,\n deviceId: facts.deviceId,\n isProd: facts.isProd,\n };\n\n if (facts.channel) {\n request.channel = facts.channel;\n request.defaultChannel = facts.channel;\n }\n if (facts.pluginVersion) request.pluginVersion = facts.pluginVersion;\n if (facts.versionBuiltin) request.versionBuiltin = facts.versionBuiltin;\n if (facts.versionOs) request.versionOs = facts.versionOs;\n if (typeof facts.isEmulator === \"boolean\") request.isEmulator = facts.isEmulator;\n if (facts.customId) request.customId = facts.customId;\n\n return request;\n}\n\nexport async function checkForUpdate(): Promise<ResolvedUpdate | null> {\n if (!isNative()) return null;\n\n const config = getUpdaterConfig();\n const problems = describeConfigProblems(config);\n if (problems.length > 0) throw new UpdaterConfigError(problems);\n\n const [versionCode, versionName, deviceId, pluginVersion, versionBuiltin, osFacts] =\n await Promise.all([\n getVersionCode(),\n getBundleVersion(),\n getDeviceId(),\n getPluginVersion(),\n getBuiltinVersion(),\n getOsFacts(),\n ]);\n\n const request = buildCheckRequest({\n appId: config.appId,\n platform: getPlatform(),\n channel: config.channel,\n isProd: config.environment === \"prod\",\n versionCode,\n versionName,\n deviceId,\n pluginVersion,\n versionBuiltin,\n ...osFacts,\n });\n\n const response = await postJson<UpdateCheckResponse>(\n `${config.apiUrl}/api/update`,\n request,\n config.timeoutMs,\n );\n\n // \"Channel not found\" and \"Environment mismatch\" are deployment mistakes.\n // Treating them as \"no update\" is how a broken channel goes unnoticed for\n // weeks.\n if (isBlockingResponse(response)) throw new UpdateCheckBlockedError(response);\n\n return resolveUpdate(response);\n}\n\n/**\n * Records a native update lifecycle event.\n *\n * Best effort: analytics must never break an update. OTA events are reported\n * by the plugin itself through its `statsUrl`, so only native ones are sent\n * from here.\n */\nexport async function logUpdateEvent(\n event: UpdateEvent,\n update: ResolvedUpdate,\n details?: { error?: string },\n): Promise<void> {\n if (update.kind !== \"native\") return;\n\n const config = getUpdaterConfig();\n if (!config.apiUrl) return;\n\n const payload: UpdateEventPayload = {\n event,\n platform: getPlatform(),\n // Without this the server has no row to attach the event to and answers\n // 400, which is what it did for every native event ever sent.\n app_id: config.appId,\n device_id: await getDeviceId(),\n current_version_code: await getVersionCode(),\n new_version: update.version,\n new_version_code: update.versionCode,\n channel: config.channel,\n environment: String(config.environment),\n ...details,\n };\n\n try {\n await postJson(`${config.apiUrl}/api/native-updates/log`, payload, config.timeoutMs);\n } catch (error) {\n console.warn(\"[capuchoo] could not record update event\", error);\n }\n}\n","/**\n * Which downloaded APK to keep, reuse, or throw away.\n *\n * A native binary is 40-60 MB and users pay for it, sometimes on mobile data.\n * The previous implementation spent that repeatedly and needlessly:\n *\n * - `downloadNativeUpdate` called `cleanApkCache()` as its first step, which\n * deleted every APK for this app - including the exact file it was about to\n * fetch. A second press always re-downloaded.\n * - The only record that a download had finished was `state.cachedPath`, in\n * memory. Closing the app threw it away, so the next launch downloaded the\n * same bytes again while the file sat on disk.\n * - Nothing ever deleted a successfully installed APK, so it stayed in the\n * cache forever.\n *\n * The decisions are here, as pure functions over file names and sizes, because\n * the alternative is discovering the behaviour on a phone with a data plan.\n * `download.service.ts` supplies the filesystem.\n */\n\n/** Cache file name prefix, derived from the app id so two flavours never collide. */\nexport function cachePrefix(appId: string): string {\n return `${appId.replaceAll(/[^\\w.-]/g, \"-\")}-`;\n}\n\nexport interface ApkIdentity {\n version: string;\n versionCode: number;\n}\n\n/** `com.efficy.app-1.0.56-67.apk` */\nexport function apkFileName(appId: string, update: ApkIdentity): string {\n return `${cachePrefix(appId)}${update.version}-${update.versionCode}.apk`;\n}\n\n/**\n * Reads a name produced by `apkFileName` back into what it holds.\n *\n * Returns null for anything else in the cache directory - the WebView's own\n * files live there too, and deleting one of those would be a very expensive\n * mistake to make with a regex.\n */\nexport function parseApkFileName(appId: string, fileName: string): ApkIdentity | null {\n const prefix = cachePrefix(appId);\n if (!fileName.startsWith(prefix) || !fileName.endsWith(\".apk\")) return null;\n\n const middle = fileName.slice(prefix.length, -\".apk\".length);\n const match = /^(.+)-(\\d+)$/.exec(middle);\n if (!match) return null;\n\n return { version: match[1]!, versionCode: Number(match[2]) };\n}\n\n/**\n * Whether a cached file is the update being offered, complete.\n *\n * Size is the check. A download interrupted by a dead connection leaves a\n * partial file at the right path, and reusing that hands the Android installer\n * a truncated APK - which fails with \"There was a problem parsing the package\",\n * a message that says nothing about the real cause.\n *\n * When the server did not send a size there is nothing to verify against, so\n * the file is not trusted. Re-downloading costs bandwidth; installing a\n * truncated binary costs a support call.\n */\nexport function isCompleteDownload(\n cached: { size: number } | null,\n expectedSize: number | undefined,\n): boolean {\n if (!cached || !expectedSize) return false;\n return cached.size === expectedSize;\n}\n\nexport interface CachedApk extends ApkIdentity {\n fileName: string;\n}\n\n/**\n * Which cached APKs to delete, given what is installed and what is wanted.\n *\n * Two situations, deliberately one function because they differ by one rule:\n *\n * **Pruning** (no `keep`) removes only what the device has outgrown - anything\n * whose build number the installed binary has caught up with. That is how an\n * installed APK finally gets deleted: the Android installer never calls back,\n * but the next launch reports a higher build number, which says the same thing.\n * A *newer* APK is left alone, because it is an update already downloaded and\n * waiting to be installed, and throwing it away would mean paying for it twice.\n *\n * **Making room** (`keep` given) additionally removes other pending downloads,\n * because the file about to be written is tens of megabytes and the OS will\n * evict from a full cache mid-download.\n *\n * `keep` itself is never deleted. Deleting the file that is about to be\n * downloaded is the exact bug this module exists to prevent.\n */\nexport function apksToDelete(input: {\n cached: CachedApk[];\n installedVersionCode: number;\n keep?: string | undefined;\n}): string[] {\n const { cached, installedVersionCode, keep } = input;\n const makingRoom = keep !== undefined;\n\n return cached\n .filter((apk) => {\n if (apk.fileName === keep) return false;\n if (apk.versionCode <= installedVersionCode) return true;\n return makingRoom;\n })\n .map((apk) => apk.fileName);\n}\n","/**\n * Plugins only the native-update path needs, loaded when it runs.\n *\n * OTA updates need `@capgo/capacitor-updater` and nothing else. Downloading and\n * installing an APK additionally needs file transfer, filesystem, network and a\n * file opener - four packages an app that only ships web bundles should not have\n * to install.\n *\n * They are optional peers, imported here rather than at module load, so\n * importing this library does not require them. A missing one produces a message\n * naming what to install instead of `Cannot find module` from inside a bundler.\n *\n * They cannot be plain dependencies: `cap sync` discovers plugins by reading the\n * *application's* `dependencies` and `devDependencies` - `getDependencies()` in\n * @capacitor/cli does not recurse - so a plugin pulled in transitively would\n * have its JavaScript installed and its native half never added to the Android\n * or iOS project. `capuchoo setup --native` adds them to the app instead.\n */\n\nconst NATIVE_PACKAGES = [\n \"@capacitor/file-transfer\",\n \"@capacitor/filesystem\",\n \"@capacitor/network\",\n \"@capawesome-team/capacitor-file-opener\",\n] as const;\n\nexport class MissingNativePluginsError extends Error {\n readonly packages: readonly string[];\n\n constructor(missing: string) {\n super(\n `Native updates need ${missing}, which is not installed. ` +\n `Run: npx capuchoo setup --native (adds ${NATIVE_PACKAGES.join(\", \")} and runs cap sync). ` +\n \"OTA updates do not need any of them.\",\n );\n this.name = \"MissingNativePluginsError\";\n this.packages = NATIVE_PACKAGES;\n }\n}\n\nasync function load<T>(specifier: string, importer: () => Promise<T>): Promise<T> {\n try {\n return await importer();\n } catch (error) {\n // A genuine runtime failure inside the plugin should not be reported as a\n // missing install, so only a resolution failure is translated.\n const message = error instanceof Error ? error.message : String(error);\n if (/cannot find module|failed to resolve|module not found/i.test(message)) {\n throw new MissingNativePluginsError(specifier);\n }\n throw error;\n }\n}\n\nexport const nativePlugins = {\n fileTransfer: () => load(\"@capacitor/file-transfer\", () => import(\"@capacitor/file-transfer\")),\n filesystem: () => load(\"@capacitor/filesystem\", () => import(\"@capacitor/filesystem\")),\n network: () => load(\"@capacitor/network\", () => import(\"@capacitor/network\")),\n fileOpener: () =>\n load(\n \"@capawesome-team/capacitor-file-opener\",\n () => import(\"@capawesome-team/capacitor-file-opener\"),\n ),\n};\n","import type { PluginListenerHandle } from \"@capacitor/core\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport {\n apkFileName,\n apksToDelete,\n isCompleteDownload,\n parseApkFileName,\n type CachedApk,\n} from \"./apk-cache.js\";\nimport { getUpdaterConfig } from \"./config.js\";\nimport { nativePlugins } from \"./optional-plugins.js\";\n\nexport interface DownloadProgress {\n loaded: number;\n total: number;\n percent: number;\n}\n\nconst DONE = { loaded: 0, total: 0, percent: 100 };\n\nfunction fileNameFor(update: ResolvedUpdate): string {\n const { appId, appName } = getUpdaterConfig();\n return apkFileName(appId || appName, {\n version: update.version,\n versionCode: update.versionCode ?? 0,\n });\n}\n\n/** Every APK in the cache that belongs to this app. */\nasync function listCachedApks(): Promise<CachedApk[]> {\n const { appId, appName } = getUpdaterConfig();\n const owner = appId || appName;\n\n try {\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n const { files } = await Filesystem.readdir({ directory: Directory.Cache, path: \"\" });\n\n return files.flatMap((file) => {\n const identity = parseApkFileName(owner, file.name);\n return identity ? [{ ...identity, fileName: file.name }] : [];\n });\n } catch {\n // A cache we cannot read is a cache we cannot reuse or prune, and neither\n // is worth failing an update over.\n return [];\n }\n}\n\n/**\n * The path of an already-downloaded, complete copy of this update.\n *\n * The only record that a download had finished used to be an in-memory\n * `cachedPath`, so closing the app threw it away and the next launch downloaded\n * the same 45 MB again while the file sat on disk. Asking the filesystem\n * survives a restart.\n */\nexport async function findCachedApk(update: ResolvedUpdate): Promise<string | null> {\n const fileName = fileNameFor(update);\n\n try {\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n\n const stat = await Filesystem.stat({ directory: Directory.Cache, path: fileName });\n if (!isCompleteDownload({ size: stat.size }, update.fileSize)) return null;\n\n const { uri } = await Filesystem.getUri({ directory: Directory.Cache, path: fileName });\n return uri;\n } catch {\n // stat throws when the file is not there, which is the common case.\n return null;\n }\n}\n\n/**\n * Downloads a native APK into the app cache and returns its path.\n *\n * Returns immediately when a complete copy is already there. The previous\n * implementation could not: its first step was `cleanApkCache()`, which deleted\n * every APK for this app - including the exact file it was about to fetch - so\n * pressing update twice always paid for the binary twice.\n */\nexport async function downloadNativeUpdate(\n update: ResolvedUpdate,\n onProgress: (progress: DownloadProgress) => void,\n): Promise<string> {\n if (!update.downloadUrl) {\n throw new Error(\"This update has no download URL\");\n }\n\n const existing = await findCachedApk(update);\n if (existing) {\n onProgress({ ...DONE });\n return existing;\n }\n\n const { Network } = await nativePlugins.network();\n const network = await Network.getStatus();\n if (!network.connected) {\n throw new Error(\"Connect to the internet to download this update\");\n }\n\n const fileName = fileNameFor(update);\n\n // Make room, but never for the file being written. APKs are tens of megabytes\n // and the OS can evict from a full cache mid-download.\n await pruneApkCache({ installedVersionCode: 0, keep: fileName });\n\n const [{ Directory, Filesystem }, { FileTransfer }] = await Promise.all([\n nativePlugins.filesystem(),\n nativePlugins.fileTransfer(),\n ]);\n\n const destination = await Filesystem.getUri({ directory: Directory.Cache, path: fileName });\n\n let progressListener: PluginListenerHandle | null = null;\n\n try {\n progressListener = await FileTransfer.addListener(\"progress\", (event) => {\n if (event.url !== update.downloadUrl) return;\n\n const percent =\n event.lengthComputable && event.contentLength > 0\n ? Math.round((event.bytes / event.contentLength) * 100)\n : 0;\n onProgress({ loaded: event.bytes, total: event.contentLength, percent });\n });\n\n const result = await FileTransfer.downloadFile({\n url: update.downloadUrl,\n path: destination.uri,\n progress: true,\n connectTimeout: 60_000,\n readTimeout: 300_000,\n });\n\n return result.path || destination.uri;\n } finally {\n await progressListener?.remove();\n }\n}\n\n/**\n * Deletes cached APKs the device has outgrown.\n *\n * Called on start-up with the installed build number, which is how an installed\n * APK is finally cleaned up: the Android installer never calls back, but the\n * next launch reports a higher build number and that says the same thing. A\n * *newer* APK is kept - it is an update already paid for and waiting to be\n * installed.\n *\n * Failures are non-fatal. Reclaiming disk space must never break an update.\n */\nexport async function pruneApkCache(options: {\n installedVersionCode: number;\n keep?: string | undefined;\n}): Promise<string[]> {\n try {\n const cached = await listCachedApks();\n const doomed = apksToDelete({ cached, ...options });\n if (doomed.length === 0) return [];\n\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n await Promise.all(\n doomed.map((fileName) =>\n Filesystem.deleteFile({ directory: Directory.Cache, path: fileName }),\n ),\n );\n\n return doomed;\n } catch (error) {\n console.warn(\"[capuchoo] could not prune the APK cache\", error);\n return [];\n }\n}\n","import { Capacitor } from \"@capacitor/core\";\nimport { getUpdaterConfig } from \"./config.js\";\nimport { nativePlugins } from \"./optional-plugins.js\";\n\nconst APK_MIME = \"application/vnd.android.package-archive\";\n\n/**\n * Hands a downloaded APK to the Android package installer.\n *\n * Requires `REQUEST_INSTALL_PACKAGES` in the manifest - the Trapeze config for\n * each flavour merges it in - and the user must have allowed this app to\n * install unknown apps. Both failures surface as opaque platform errors, so\n * they are translated into something a user can act on.\n */\nexport async function openNativeInstaller(path: string): Promise<void> {\n if (Capacitor.getPlatform() !== \"android\") {\n throw new Error(\n \"Installing a native update from inside the app is only possible on Android. \" +\n \"On iOS the update has to come from the App Store or TestFlight.\",\n );\n }\n\n const { FileOpener } = await nativePlugins.fileOpener();\n\n try {\n await FileOpener.openFile({ path, mimeType: APK_MIME });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const { appName } = getUpdaterConfig();\n\n if (/permission|unknown sources|REQUEST_INSTALL_PACKAGES/i.test(message)) {\n throw new Error(\n `Allow ${appName} to install unknown apps in Android settings, then try again`,\n );\n }\n if (/activity|no app/i.test(message)) {\n throw new Error(\n \"No package installer is available on this device, so the update cannot be installed here\",\n );\n }\n\n throw new Error(`Could not open the Android installer: ${message}`);\n }\n}\n","import { CapacitorUpdater } from \"@capgo/capacitor-updater\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport { isNative } from \"./device.js\";\n\n/**\n * Thin wrapper over the OTA plugin.\n *\n * Downloading and applying a web bundle stays with `@capgo/capacitor-updater`:\n * it owns the native bundle store, the atomic swap and the rollback. This\n * module only sequences those calls correctly.\n */\n\n/**\n * Confirms the current bundle booted successfully.\n *\n * **This must be called once, early, on every app start.** If the plugin does\n * not hear it within `appReadyTimeout`, it assumes the new bundle crashed and\n * rolls back to the previous one - which looks exactly like \"the update did\n * not install\".\n */\nexport async function notifyAppReady(): Promise<void> {\n if (!isNative()) return;\n\n try {\n await CapacitorUpdater.notifyAppReady();\n } catch (error) {\n console.warn(\"[capuchoo] could not mark this bundle as ready\", error);\n }\n}\n\n/** The bundle currently applied, or null off-device. */\nexport async function getCurrentBundle() {\n if (!isNative()) return null;\n\n try {\n return await CapacitorUpdater.current();\n } catch (error) {\n console.warn(\"[capuchoo] could not read the current bundle\", error);\n return null;\n }\n}\n\n/**\n * Downloads an OTA bundle and applies it.\n *\n * `set` swaps the active bundle and reloads the WebView, so nothing after it\n * runs. It is called last on purpose.\n */\nexport async function applyOtaUpdate(update: ResolvedUpdate): Promise<void> {\n if (update.kind !== \"ota\") {\n throw new Error(\"applyOtaUpdate was given a native update\");\n }\n\n // Already downloaded - either by a previous attempt in this session, or by\n // the plugin's own background download when autoUpdate is \"onlyDownload\".\n if (update.bundleId) {\n await CapacitorUpdater.set({ id: update.bundleId });\n return;\n }\n\n if (!update.downloadUrl) {\n throw new Error(\"This update has no download URL\");\n }\n\n const bundle = await CapacitorUpdater.download({\n url: update.downloadUrl,\n version: update.version,\n ...(update.checksum ? { checksum: update.checksum } : {}),\n ...(update.sessionKey ? { sessionKey: update.sessionKey } : {}),\n });\n\n update.bundleId = bundle.id;\n await CapacitorUpdater.set({ id: bundle.id });\n}\n\n/** Discards a downloaded bundle that will not be applied. */\nexport async function discardBundle(bundleId: string): Promise<void> {\n if (!isNative()) return;\n\n try {\n await CapacitorUpdater.delete({ id: bundleId });\n } catch (error) {\n console.warn(\"[capuchoo] could not delete bundle\", bundleId, error);\n }\n}\n"],"mappings":"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"}
|