@capuchoo/updater 0.1.1 → 0.4.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 CHANGED
@@ -1,17 +1,22 @@
1
1
  # @capuchoo/updater
2
2
 
3
- The app-side runtime for [Capuchooo](https://github.com/aybinv7/capuchoo): it asks your update
4
- server what to do, downloads OTA bundles or native binaries, and drives the install. Built on
3
+ The app-side runtime for [Capuchoo](https://github.com/aybinv7/capuchoo): it asks your update server
4
+ what to do, downloads OTA bundles or native binaries, and drives the install. Built on
5
5
  `@capgo/capacitor-updater`.
6
6
 
7
7
  ```sh
8
- npm install @capuchoo/updater @capuchoo/core
8
+ npx @capuchoo/cli setup
9
9
  ```
10
10
 
11
- Peers you will already have in a Capacitor app: `@capacitor/core`, `@capacitor/app`,
12
- `@capacitor/filesystem`, `@capacitor/network`, `@capacitor/file-transfer`,
13
- `@capawesome-team/capacitor-file-opener`, `@capgo/capacitor-updater`. `vue` is optional and only
14
- needed for the `/vue` entry point.
11
+ That installs this package, the `@capgo/capacitor-updater` plugin it drives and `@capacitor/app`
12
+ into your app - they must be the _app's_ own dependencies for `cap sync` to wire up their native
13
+ halves - and then runs `cap sync`. Add `--native` if the app installs APKs itself.
14
+
15
+ Required peers, if you would rather add them yourself: `@capacitor/core`, `@capacitor/app`,
16
+ `@capgo/capacitor-updater`. Optional: `@capacitor/device` (reports OS version and emulator flag),
17
+ `vue` (only for the `/vue` entry point), and `@capacitor/filesystem`, `@capacitor/network`,
18
+ `@capacitor/file-transfer`, `@capawesome-team/capacitor-file-opener` - loaded on demand, and only by
19
+ the native-APK path.
15
20
 
16
21
  ## Three things, in order
17
22
 
@@ -29,14 +34,14 @@ does not hear it within `appReadyTimeout` (10 s), it concludes the bundle crashe
29
34
  the previous one — so gating this call behind a condition, or awaiting a network request before it,
30
35
  reverts working updates. It is not a gate on auto-update.
31
36
 
32
- ### 2. Configure the plugin through `capuchoUpdaterConfig()`
37
+ ### 2. Configure the plugin through `capuchooUpdaterConfig()`
33
38
 
34
39
  ```ts
35
40
  // capacitor.config.ts
36
- import { capuchoUpdaterConfig } from "@capuchoo/updater/capacitor";
41
+ import { capuchooUpdaterConfig } from "@capuchoo/updater/capacitor";
37
42
 
38
43
  plugins: {
39
- CapacitorUpdater: capuchoUpdaterConfig({
44
+ CapacitorUpdater: capuchooUpdaterConfig({
40
45
  apiUrl: process.env.VITE_UPDATE_API_URL,
41
46
  channel: process.env.VITE_UPDATE_CHANNEL,
42
47
  }),
@@ -19,7 +19,7 @@
19
19
  */
20
20
  type UpdaterMode = "onlyDownload" | "manual";
21
21
  interface UpdaterPluginOptions {
22
- /** Base URL of the Capucho backend. No trailing slash needed. */
22
+ /** Base URL of the Capuchoo backend. No trailing slash needed. */
23
23
  apiUrl: string;
24
24
  /** Channel this build defaults to. */
25
25
  channel: string;
@@ -52,7 +52,7 @@ interface CapacitorUpdaterPluginConfig {
52
52
  responseTimeout: number;
53
53
  allowModifyUrl: boolean;
54
54
  }
55
- declare function capuchoUpdaterConfig(options: UpdaterPluginOptions): CapacitorUpdaterPluginConfig;
55
+ declare function capuchooUpdaterConfig(options: UpdaterPluginOptions): CapacitorUpdaterPluginConfig;
56
56
  //#endregion
57
- export { CapacitorUpdaterPluginConfig, UpdaterMode, UpdaterPluginOptions, capuchoUpdaterConfig };
57
+ export { CapacitorUpdaterPluginConfig, UpdaterMode, UpdaterPluginOptions, capuchooUpdaterConfig };
58
58
  //# sourceMappingURL=capacitor-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"capacitor-config.d.ts","names":[],"sources":["../src/capacitor-config.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KAmBY;UAEK;;EAEf;;EAEA;;;;;;EAMA;EACA,OAAO;;EAEP;EACA;;;;;;EAMA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGc,qBAAqB,SAAS,uBAAuB"}
1
+ {"version":3,"file":"capacitor-config.d.ts","names":[],"sources":["../src/capacitor-config.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KAmBY;UAEK;;EAEf;;EAEA;;;;;;EAMA;EACA,OAAO;;EAEP;EACA;;;;;;EAMA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGc,sBAAsB,SAAS,uBAAuB"}
@@ -1,7 +1,7 @@
1
1
  //#region src/capacitor-config.ts
2
- function capuchoUpdaterConfig(options) {
2
+ function capuchooUpdaterConfig(options) {
3
3
  const apiUrl = options.apiUrl.replace(/\/+$/, "");
4
- if (!apiUrl) throw new Error("capuchoUpdaterConfig: apiUrl is empty. Set VITE_UPDATE_API_URL for this flavour before building, otherwise the app ships with updates disabled.");
4
+ if (!apiUrl) throw new Error("capuchooUpdaterConfig: apiUrl is empty. Set VITE_UPDATE_API_URL for this flavour before building, otherwise the app ships with updates disabled.");
5
5
  return {
6
6
  autoUpdate: (options.mode ?? "onlyDownload") === "onlyDownload" ? "onlyDownload" : false,
7
7
  updateUrl: `${apiUrl}/api/update`,
@@ -16,6 +16,6 @@ function capuchoUpdaterConfig(options) {
16
16
  };
17
17
  }
18
18
  //#endregion
19
- export { capuchoUpdaterConfig };
19
+ export { capuchooUpdaterConfig };
20
20
 
21
21
  //# sourceMappingURL=capacitor-config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"capacitor-config.js","names":[],"sources":["../src/capacitor-config.ts"],"sourcesContent":["/**\n * Builds the `CapacitorUpdater` plugin block for `capacitor.config.ts`.\n *\n * This exists because the plugin's `autoUpdate` mode has to agree with how the\n * app drives updates, and getting it wrong fails in a way that is very hard to\n * diagnose. The app template shipped `autoUpdate: true` while also calling\n * `download()` and `set()` from JavaScript: the plugin applied bundles on its\n * own schedule at the same time as the UI was downloading them, so a device\n * could download the same bundle twice, or reload mid-prompt.\n *\n * `\"onlyDownload\"` is the mode this package is written for. The plugin fetches\n * the bundle in the background and raises `updateAvailable`; the app decides\n * when to apply it. Pass `mode: \"manual\"` to disable background downloads\n * entirely and drive everything from `useUpdater`.\n *\n * Imported from `capacitor.config.ts`, so it must stay free of any runtime or\n * DOM dependency.\n */\n\nexport type UpdaterMode = \"onlyDownload\" | \"manual\";\n\nexport interface UpdaterPluginOptions {\n /** Base URL of the Capucho backend. No trailing slash needed. */\n apiUrl: string;\n /** Channel this build defaults to. */\n channel: string;\n /**\n * Version the plugin reports as the built-in bundle. Pass the app's\n * package.json version - if this is stale, the server compares against the\n * wrong baseline and re-serves bundles the device already has.\n */\n version: string;\n mode?: UpdaterMode;\n /** Milliseconds the plugin waits for `notifyAppReady` before rolling back. */\n appReadyTimeout?: number;\n responseTimeout?: number;\n /**\n * Whether the app may point the plugin at a different server at runtime.\n * Leave off in production: it lets anything running in the WebView redirect\n * update downloads.\n */\n allowModifyUrl?: boolean;\n}\n\nexport interface CapacitorUpdaterPluginConfig {\n autoUpdate: boolean | \"onlyDownload\";\n updateUrl: string;\n statsUrl: string;\n channelUrl: string;\n defaultChannel: string;\n version: string;\n directUpdate: boolean;\n appReadyTimeout: number;\n responseTimeout: number;\n allowModifyUrl: boolean;\n}\n\nexport function capuchoUpdaterConfig(options: UpdaterPluginOptions): CapacitorUpdaterPluginConfig {\n const apiUrl = options.apiUrl.replace(/\\/+$/, \"\");\n\n if (!apiUrl) {\n // The plugin accepts an empty updateUrl and then silently never checks for\n // updates, which is the worst possible outcome. Fail the build instead.\n throw new Error(\n \"capuchoUpdaterConfig: apiUrl is empty. Set VITE_UPDATE_API_URL for this \" +\n \"flavour before building, otherwise the app ships with updates disabled.\",\n );\n }\n\n const mode = options.mode ?? \"onlyDownload\";\n\n return {\n // \"manual\" means the plugin does nothing on its own.\n autoUpdate: mode === \"onlyDownload\" ? \"onlyDownload\" : false,\n updateUrl: `${apiUrl}/api/update`,\n statsUrl: `${apiUrl}/api/stats`,\n channelUrl: `${apiUrl}/api/channel_self`,\n defaultChannel: options.channel,\n version: options.version,\n // The app shows a prompt and calls set() itself; letting the plugin apply\n // the bundle immediately would reload the WebView under the user.\n directUpdate: false,\n appReadyTimeout: options.appReadyTimeout ?? 10_000,\n responseTimeout: options.responseTimeout ?? 30_000,\n allowModifyUrl: options.allowModifyUrl ?? false,\n };\n}\n"],"mappings":";AAyDA,SAAgB,qBAAqB,SAA6D;CAChG,MAAM,SAAS,QAAQ,OAAO,QAAQ,QAAQ,EAAE;CAEhD,IAAI,CAAC,QAGH,MAAM,IAAI,MACR,iJAEF;CAKF,OAAO;EAEL,aAJW,QAAQ,QAAQ,oBAIN,iBAAiB,iBAAiB;EACvD,WAAW,GAAG,OAAO;EACrB,UAAU,GAAG,OAAO;EACpB,YAAY,GAAG,OAAO;EACtB,gBAAgB,QAAQ;EACxB,SAAS,QAAQ;EAGjB,cAAc;EACd,iBAAiB,QAAQ,mBAAmB;EAC5C,iBAAiB,QAAQ,mBAAmB;EAC5C,gBAAgB,QAAQ,kBAAkB;CAC5C;AACF"}
1
+ {"version":3,"file":"capacitor-config.js","names":[],"sources":["../src/capacitor-config.ts"],"sourcesContent":["/**\n * Builds the `CapacitorUpdater` plugin block for `capacitor.config.ts`.\n *\n * This exists because the plugin's `autoUpdate` mode has to agree with how the\n * app drives updates, and getting it wrong fails in a way that is very hard to\n * diagnose. The app template shipped `autoUpdate: true` while also calling\n * `download()` and `set()` from JavaScript: the plugin applied bundles on its\n * own schedule at the same time as the UI was downloading them, so a device\n * could download the same bundle twice, or reload mid-prompt.\n *\n * `\"onlyDownload\"` is the mode this package is written for. The plugin fetches\n * the bundle in the background and raises `updateAvailable`; the app decides\n * when to apply it. Pass `mode: \"manual\"` to disable background downloads\n * entirely and drive everything from `useUpdater`.\n *\n * Imported from `capacitor.config.ts`, so it must stay free of any runtime or\n * DOM dependency.\n */\n\nexport type UpdaterMode = \"onlyDownload\" | \"manual\";\n\nexport interface UpdaterPluginOptions {\n /** Base URL of the Capuchoo backend. No trailing slash needed. */\n apiUrl: string;\n /** Channel this build defaults to. */\n channel: string;\n /**\n * Version the plugin reports as the built-in bundle. Pass the app's\n * package.json version - if this is stale, the server compares against the\n * wrong baseline and re-serves bundles the device already has.\n */\n version: string;\n mode?: UpdaterMode;\n /** Milliseconds the plugin waits for `notifyAppReady` before rolling back. */\n appReadyTimeout?: number;\n responseTimeout?: number;\n /**\n * Whether the app may point the plugin at a different server at runtime.\n * Leave off in production: it lets anything running in the WebView redirect\n * update downloads.\n */\n allowModifyUrl?: boolean;\n}\n\nexport interface CapacitorUpdaterPluginConfig {\n autoUpdate: boolean | \"onlyDownload\";\n updateUrl: string;\n statsUrl: string;\n channelUrl: string;\n defaultChannel: string;\n version: string;\n directUpdate: boolean;\n appReadyTimeout: number;\n responseTimeout: number;\n allowModifyUrl: boolean;\n}\n\nexport function capuchooUpdaterConfig(options: UpdaterPluginOptions): CapacitorUpdaterPluginConfig {\n const apiUrl = options.apiUrl.replace(/\\/+$/, \"\");\n\n if (!apiUrl) {\n // The plugin accepts an empty updateUrl and then silently never checks for\n // updates, which is the worst possible outcome. Fail the build instead.\n throw new Error(\n \"capuchooUpdaterConfig: apiUrl is empty. Set VITE_UPDATE_API_URL for this \" +\n \"flavour before building, otherwise the app ships with updates disabled.\",\n );\n }\n\n const mode = options.mode ?? \"onlyDownload\";\n\n return {\n // \"manual\" means the plugin does nothing on its own.\n autoUpdate: mode === \"onlyDownload\" ? \"onlyDownload\" : false,\n updateUrl: `${apiUrl}/api/update`,\n statsUrl: `${apiUrl}/api/stats`,\n channelUrl: `${apiUrl}/api/channel_self`,\n defaultChannel: options.channel,\n version: options.version,\n // The app shows a prompt and calls set() itself; letting the plugin apply\n // the bundle immediately would reload the WebView under the user.\n directUpdate: false,\n appReadyTimeout: options.appReadyTimeout ?? 10_000,\n responseTimeout: options.responseTimeout ?? 30_000,\n allowModifyUrl: options.allowModifyUrl ?? false,\n };\n}\n"],"mappings":";AAyDA,SAAgB,sBAAsB,SAA6D;CACjG,MAAM,SAAS,QAAQ,OAAO,QAAQ,QAAQ,EAAE;CAEhD,IAAI,CAAC,QAGH,MAAM,IAAI,MACR,kJAEF;CAKF,OAAO;EAEL,aAJW,QAAQ,QAAQ,oBAIN,iBAAiB,iBAAiB;EACvD,WAAW,GAAG,OAAO;EACrB,UAAU,GAAG,OAAO;EACpB,YAAY,GAAG,OAAO;EACtB,gBAAgB,QAAQ;EACxB,SAAS,QAAQ;EAGjB,cAAc;EACd,iBAAiB,QAAQ,mBAAmB;EAC5C,iBAAiB,QAAQ,mBAAmB;EAC5C,gBAAgB,QAAQ,kBAAkB;CAC5C;AACF"}
package/dist/index.d.ts CHANGED
@@ -39,6 +39,14 @@ declare function isNative(): boolean;
39
39
  * app version does.
40
40
  */
41
41
  declare function getPluginVersion(): Promise<string | undefined>;
42
+ /**
43
+ * Bundle version compiled into the binary.
44
+ *
45
+ * Distinct from `getBundleVersion()`, which reports the OTA bundle currently
46
+ * applied - that one says `"builtin"` when none has been, and this says which
47
+ * builtin that is.
48
+ */
49
+ declare function getBuiltinVersion(): Promise<string | undefined>;
42
50
  /**
43
51
  * OS version and emulator flag, from `@capacitor/device`.
44
52
  *
@@ -82,6 +90,7 @@ interface DeviceFacts {
82
90
  versionName: string;
83
91
  deviceId: string;
84
92
  pluginVersion?: string | undefined;
93
+ versionBuiltin?: string | undefined;
85
94
  versionOs?: string | undefined;
86
95
  isEmulator?: boolean | undefined;
87
96
  customId?: string | undefined;
@@ -118,7 +127,7 @@ declare function logUpdateEvent(event: UpdateEvent$1, update: ResolvedUpdate$1,
118
127
  * a login response rather than at build time.
119
128
  */
120
129
  interface UpdaterConfig {
121
- /** Base URL of the Capucho backend, with no trailing slash. */
130
+ /** Base URL of the Capuchoo backend, with no trailing slash. */
122
131
  apiUrl: string;
123
132
  /** Bundle identifier of this build. Must match what the CLI published. */
124
133
  appId: string;
@@ -159,5 +168,5 @@ declare function describeConfigProblems(config: UpdaterConfig): string[];
159
168
  */
160
169
  declare function openNativeInstaller(path: string): Promise<void>;
161
170
  //#endregion
162
- export { type DeviceFacts, type DownloadProgress, type Environment, type Platform, type ResolvedUpdate, UpdateCheckBlockedError, type UpdateCheckResponse, type UpdateEvent, type UpdateKind, type UpdaterConfig, UpdaterConfigError, applyOtaUpdate, buildCheckRequest, checkForUpdate, cleanApkCache, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, getBundleVersion, getCurrentBundle, getDeviceId, getOsFacts, getPlatform, getPluginVersion, getUpdaterConfig, getVersionCode, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller };
171
+ export { type DeviceFacts, type DownloadProgress, type Environment, type Platform, type ResolvedUpdate, UpdateCheckBlockedError, type UpdateCheckResponse, type UpdateEvent, type UpdateKind, type UpdaterConfig, UpdaterConfigError, applyOtaUpdate, buildCheckRequest, checkForUpdate, cleanApkCache, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, getBuiltinVersion, getBundleVersion, getCurrentBundle, getDeviceId, getOsFacts, getPlatform, getPluginVersion, getUpdaterConfig, getVersionCode, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller };
163
172
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/device.ts","../src/api.service.ts","../src/config.ts","../src/install.service.ts"],"mappings":";;;;;;;;;;;;;;iBAgBsB,kBAAkB;;;;;;;;iBAkBlB,oBAAoB;;;;;;;;;iBAmBpB,eAAe;iBAWrB;iBAIA;;;;;;;iBAUM,oBAAoB;;;;;;;;;iBAmBpB,cAAc;EAAU;EAAoB;;;;;cC5ErD,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;;;;;;;;;;;iBAYc,kBAAkB,OAAO,cAAc;iBAwBjC,kBAAkB,QAAQ;;;;;;;;iBAgD1B,eACpB,OAAO,eACP,QAAQ,kBACR;EAAY;IACX;;;;;;;;;;;UCrKc;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;iBAoBc,iBAAiB,MAAM,QAAQ;iBAS/B,oBAAoB;;;;;;;;;;iBAuBpB,uBAAuB,QAAQ;;;;;;;;;;;iBC1DzB,oBAAoB,eAAe"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/device.ts","../src/api.service.ts","../src/config.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;;;;;;;;;;;iBC1DzB,oBAAoB,eAAe"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { S as getUpdaterConfig, _ as getPluginVersion, a as openNativeInstaller, b as configureUpdater, c as UpdateCheckBlockedError, d as checkForUpdate, f as logUpdateEvent, g as getPlatform, h as getOsFacts, i as notifyAppReady, l as UpdaterConfigError, m as getDeviceId, n as discardBundle, o as cleanApkCache, p as getBundleVersion, r as getCurrentBundle, s as downloadNativeUpdate, t as applyOtaUpdate, u as buildCheckRequest, v as getVersionCode, x as describeConfigProblems, y as isNative } from "./ota.service-tv6eNQcB.js";
2
- export { UpdateCheckBlockedError, UpdaterConfigError, applyOtaUpdate, buildCheckRequest, checkForUpdate, cleanApkCache, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, getBundleVersion, getCurrentBundle, getDeviceId, getOsFacts, getPlatform, getPluginVersion, getUpdaterConfig, getVersionCode, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller };
1
+ import { C as getUpdaterConfig, S as describeConfigProblems, _ as getPlatform, a as openNativeInstaller, b as isNative, c as UpdateCheckBlockedError, d as checkForUpdate, f as logUpdateEvent, g as getOsFacts, h as getDeviceId, i as notifyAppReady, l as UpdaterConfigError, m as getBundleVersion, n as discardBundle, o as cleanApkCache, p as getBuiltinVersion, r as getCurrentBundle, s as downloadNativeUpdate, t as applyOtaUpdate, u as buildCheckRequest, v as getPluginVersion, x as configureUpdater, y as getVersionCode } from "./ota.service-SyGTRpY2.js";
2
+ export { UpdateCheckBlockedError, UpdaterConfigError, applyOtaUpdate, buildCheckRequest, checkForUpdate, cleanApkCache, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, getBuiltinVersion, getBundleVersion, getCurrentBundle, getDeviceId, getOsFacts, getPlatform, getPluginVersion, getUpdaterConfig, getVersionCode, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller };
@@ -1 +1 @@
1
- {"version":3,"file":"ota.service-B3LrcCPr.d.ts","names":[],"sources":["../src/download.service.ts","../src/ota.service.ts"],"mappings":";;UAOiB;EACf;EACA;EACA;;;;;;;;;iBAqBoB,qBACpB,QAAQ,gBACR,aAAa,UAAU,4BACtB;;iBAgDmB,iBAAiB;;;;;;;;;;;;;;;;;;iBC9DjB,kBAAkB;;iBAWlB,oBAAgB,2CAAA;;;;;;;iBAiBhB,eAAe,QAAQ,iBAAiB;;iBA4BxC,cAAc,mBAAmB"}
1
+ {"version":3,"file":"ota.service-B3LrcCPr.d.ts","names":[],"sources":["../src/download.service.ts","../src/ota.service.ts"],"mappings":";;UAKiB;EACf;EACA;EACA;;;;;;;;;iBAqBoB,qBACpB,QAAQ,gBACR,aAAa,UAAU,4BACtB;;iBAsDmB,iBAAiB;;;;;;;;;;;;;;;;;;iBClEjB,kBAAkB;;iBAWlB,oBAAgB,2CAAA;;;;;;;iBAiBhB,eAAe,QAAQ,iBAAiB;;iBA4BxC,cAAc,mBAAmB"}
@@ -2,10 +2,6 @@ import { isBlockingResponse, resolveUpdate } from "@capuchoo/core";
2
2
  import { App } from "@capacitor/app";
3
3
  import { Capacitor } from "@capacitor/core";
4
4
  import { CapacitorUpdater } from "@capgo/capacitor-updater";
5
- import { FileTransfer } from "@capacitor/file-transfer";
6
- import { Directory, Filesystem } from "@capacitor/filesystem";
7
- import { Network } from "@capacitor/network";
8
- import { FileOpener } from "@capawesome-team/capacitor-file-opener";
9
5
  //#region src/config.ts
10
6
  function env(key) {
11
7
  return import.meta.env?.[key];
@@ -125,6 +121,22 @@ async function getPluginVersion() {
125
121
  }
126
122
  }
127
123
  /**
124
+ * Bundle version compiled into the binary.
125
+ *
126
+ * Distinct from `getBundleVersion()`, which reports the OTA bundle currently
127
+ * applied - that one says `"builtin"` when none has been, and this says which
128
+ * builtin that is.
129
+ */
130
+ async function getBuiltinVersion() {
131
+ if (!Capacitor.isNativePlatform()) return void 0;
132
+ try {
133
+ const { version } = await CapacitorUpdater.getBuiltinVersion();
134
+ return version || void 0;
135
+ } catch {
136
+ return;
137
+ }
138
+ }
139
+ /**
128
140
  * OS version and emulator flag, from `@capacitor/device`.
129
141
  *
130
142
  * That package is an *optional* peer: it is a separate install, and an app that
@@ -205,6 +217,7 @@ function buildCheckRequest(facts) {
205
217
  request.defaultChannel = facts.channel;
206
218
  }
207
219
  if (facts.pluginVersion) request.pluginVersion = facts.pluginVersion;
220
+ if (facts.versionBuiltin) request.versionBuiltin = facts.versionBuiltin;
208
221
  if (facts.versionOs) request.versionOs = facts.versionOs;
209
222
  if (typeof facts.isEmulator === "boolean") request.isEmulator = facts.isEmulator;
210
223
  if (facts.customId) request.customId = facts.customId;
@@ -215,11 +228,12 @@ async function checkForUpdate() {
215
228
  const config = getUpdaterConfig();
216
229
  const problems = describeConfigProblems(config);
217
230
  if (problems.length > 0) throw new UpdaterConfigError(problems);
218
- const [versionCode, versionName, deviceId, pluginVersion, osFacts] = await Promise.all([
231
+ const [versionCode, versionName, deviceId, pluginVersion, versionBuiltin, osFacts] = await Promise.all([
219
232
  getVersionCode(),
220
233
  getBundleVersion(),
221
234
  getDeviceId(),
222
235
  getPluginVersion(),
236
+ getBuiltinVersion(),
223
237
  getOsFacts()
224
238
  ]);
225
239
  const request = buildCheckRequest({
@@ -231,6 +245,7 @@ async function checkForUpdate() {
231
245
  versionName,
232
246
  deviceId,
233
247
  pluginVersion,
248
+ versionBuiltin,
234
249
  ...osFacts
235
250
  });
236
251
  const response = await postJson(`${config.apiUrl}/api/update`, request, config.timeoutMs);
@@ -266,6 +281,55 @@ async function logUpdateEvent(event, update, details) {
266
281
  }
267
282
  }
268
283
  //#endregion
284
+ //#region src/optional-plugins.ts
285
+ /**
286
+ * Plugins only the native-update path needs, loaded when it runs.
287
+ *
288
+ * OTA updates need `@capgo/capacitor-updater` and nothing else. Downloading and
289
+ * installing an APK additionally needs file transfer, filesystem, network and a
290
+ * file opener - four packages an app that only ships web bundles should not have
291
+ * to install.
292
+ *
293
+ * They are optional peers, imported here rather than at module load, so
294
+ * importing this library does not require them. A missing one produces a message
295
+ * naming what to install instead of `Cannot find module` from inside a bundler.
296
+ *
297
+ * They cannot be plain dependencies: `cap sync` discovers plugins by reading the
298
+ * *application's* `dependencies` and `devDependencies` - `getDependencies()` in
299
+ * @capacitor/cli does not recurse - so a plugin pulled in transitively would
300
+ * have its JavaScript installed and its native half never added to the Android
301
+ * or iOS project. `capuchoo setup --native` adds them to the app instead.
302
+ */
303
+ const NATIVE_PACKAGES = [
304
+ "@capacitor/file-transfer",
305
+ "@capacitor/filesystem",
306
+ "@capacitor/network",
307
+ "@capawesome-team/capacitor-file-opener"
308
+ ];
309
+ var MissingNativePluginsError = class extends Error {
310
+ packages;
311
+ constructor(missing) {
312
+ super(`Native updates need ${missing}, which is not installed. Run: npx capuchoo setup --native (adds ${NATIVE_PACKAGES.join(", ")} and runs cap sync). OTA updates do not need any of them.`);
313
+ this.name = "MissingNativePluginsError";
314
+ this.packages = NATIVE_PACKAGES;
315
+ }
316
+ };
317
+ async function load(specifier, importer) {
318
+ try {
319
+ return await importer();
320
+ } catch (error) {
321
+ const message = error instanceof Error ? error.message : String(error);
322
+ if (/cannot find module|failed to resolve|module not found/i.test(message)) throw new MissingNativePluginsError(specifier);
323
+ throw error;
324
+ }
325
+ }
326
+ const nativePlugins = {
327
+ fileTransfer: () => load("@capacitor/file-transfer", () => import("@capacitor/file-transfer")),
328
+ filesystem: () => load("@capacitor/filesystem", () => import("@capacitor/filesystem")),
329
+ network: () => load("@capacitor/network", () => import("@capacitor/network")),
330
+ fileOpener: () => load("@capawesome-team/capacitor-file-opener", () => import("@capawesome-team/capacitor-file-opener"))
331
+ };
332
+ //#endregion
269
333
  //#region src/download.service.ts
270
334
  /** Cache file name prefix, derived from the app id so two flavours never collide. */
271
335
  function cachePrefix() {
@@ -284,9 +348,11 @@ function apkFileName(update) {
284
348
  */
285
349
  async function downloadNativeUpdate(update, onProgress) {
286
350
  if (!update.downloadUrl) throw new Error("This update has no download URL");
351
+ const { Network } = await nativePlugins.network();
287
352
  if (!(await Network.getStatus()).connected) throw new Error("Connect to the internet to download this update");
288
353
  await cleanApkCache();
289
354
  const fileName = apkFileName(update);
355
+ const [{ Directory, Filesystem }, { FileTransfer }] = await Promise.all([nativePlugins.filesystem(), nativePlugins.fileTransfer()]);
290
356
  const destination = await Filesystem.getUri({
291
357
  directory: Directory.Cache,
292
358
  path: fileName
@@ -317,6 +383,7 @@ async function downloadNativeUpdate(update, onProgress) {
317
383
  async function cleanApkCache() {
318
384
  const prefix = cachePrefix();
319
385
  try {
386
+ const { Directory, Filesystem } = await nativePlugins.filesystem();
320
387
  const { files } = await Filesystem.readdir({
321
388
  directory: Directory.Cache,
322
389
  path: ""
@@ -342,6 +409,7 @@ const APK_MIME = "application/vnd.android.package-archive";
342
409
  */
343
410
  async function openNativeInstaller(path) {
344
411
  if (Capacitor.getPlatform() !== "android") throw new Error("Installing a native update from inside the app is only possible on Android. On iOS the update has to come from the App Store or TestFlight.");
412
+ const { FileOpener } = await nativePlugins.fileOpener();
345
413
  try {
346
414
  await FileOpener.openFile({
347
415
  path,
@@ -422,6 +490,6 @@ async function discardBundle(bundleId) {
422
490
  }
423
491
  }
424
492
  //#endregion
425
- export { getUpdaterConfig as S, getPluginVersion as _, openNativeInstaller as a, configureUpdater as b, UpdateCheckBlockedError as c, checkForUpdate as d, logUpdateEvent as f, getPlatform as g, getOsFacts as h, notifyAppReady as i, UpdaterConfigError as l, getDeviceId as m, discardBundle as n, cleanApkCache as o, getBundleVersion as p, getCurrentBundle as r, downloadNativeUpdate as s, applyOtaUpdate as t, buildCheckRequest as u, getVersionCode as v, describeConfigProblems as x, isNative as y };
493
+ export { getUpdaterConfig as C, describeConfigProblems as S, getPlatform as _, openNativeInstaller as a, isNative as b, UpdateCheckBlockedError as c, checkForUpdate as d, logUpdateEvent as f, getOsFacts as g, getDeviceId as h, notifyAppReady as i, UpdaterConfigError as l, getBundleVersion as m, discardBundle as n, cleanApkCache as o, getBuiltinVersion as p, getCurrentBundle as r, downloadNativeUpdate as s, applyOtaUpdate as t, buildCheckRequest as u, getPluginVersion as v, configureUpdater as x, getVersionCode as y };
426
494
 
427
- //# sourceMappingURL=ota.service-tv6eNQcB.js.map
495
+ //# sourceMappingURL=ota.service-SyGTRpY2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ota.service-SyGTRpY2.js","names":[],"sources":["../src/config.ts","../src/device.ts","../src/api.service.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 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 * 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 { 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\n/** Cache file name prefix, derived from the app id so two flavours never collide. */\nfunction cachePrefix(): string {\n const { appId, appName } = getUpdaterConfig();\n const base = appId || appName;\n return `${base.replaceAll(/[^\\w.-]/g, \"-\")}-`;\n}\n\nfunction apkFileName(update: ResolvedUpdate): string {\n return `${cachePrefix()}${update.version}-${update.versionCode ?? 0}.apk`;\n}\n\n/**\n * Downloads a native APK into the app cache and returns its path.\n *\n * The file name embeds the app id, so a staging build and a production build\n * installed side by side cannot overwrite each other's download. The previous\n * implementation prefixed every file with a hard-coded app name.\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 { 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 // Clear older APKs first: they are typically 20-60 MB and the OS can evict\n // the cache mid-download if it is already full.\n await cleanApkCache();\n\n const fileName = apkFileName(update);\n const [{ Directory, Filesystem }, { FileTransfer }] = await Promise.all([\n nativePlugins.filesystem(),\n nativePlugins.fileTransfer(),\n ]);\n\n const destination = await Filesystem.getUri({\n directory: Directory.Cache,\n path: fileName,\n });\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/** Removes this app's cached APKs. Failures are non-fatal. */\nexport async function cleanApkCache(): Promise<void> {\n const prefix = cachePrefix();\n\n try {\n const { Directory, Filesystem } = await nativePlugins.filesystem();\n const { files } = await Filesystem.readdir({\n directory: Directory.Cache,\n path: \"\",\n });\n\n await Promise.all(\n files\n .filter((file) => file.name.startsWith(prefix) && file.name.endsWith(\".apk\"))\n .map((file) => Filesystem.deleteFile({ directory: Directory.Cache, path: file.name })),\n );\n } catch (error) {\n console.warn(\"[capuchoo] could not clean the APK cache\", error);\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 { S as getUpdaterConfig, a as openNativeInstaller, c as UpdateCheckBlockedError, d as checkForUpdate, f as logUpdateEvent, i as notifyAppReady, l as UpdaterConfigError, o as cleanApkCache, r as getCurrentBundle, s as downloadNativeUpdate, t as applyOtaUpdate, y as isNative } from "./ota.service-tv6eNQcB.js";
1
+ import { C as getUpdaterConfig, a as openNativeInstaller, b as isNative, c as UpdateCheckBlockedError, d as checkForUpdate, f as logUpdateEvent, i as notifyAppReady, l as UpdaterConfigError, o as cleanApkCache, r as getCurrentBundle, s as downloadNativeUpdate, t as applyOtaUpdate } from "./ota.service-SyGTRpY2.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.1.1",
3
+ "version": "0.4.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.1.1"
45
+ "@capuchoo/core": "^0.1.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@capacitor/app": "^8.0.0",
@@ -57,22 +57,34 @@
57
57
  "vue": "^3.5.25"
58
58
  },
59
59
  "peerDependencies": {
60
- "@capacitor/app": "^8.0.0",
61
- "@capacitor/core": "^8.0.0",
62
- "@capacitor/device": "^8.0.0",
63
- "@capacitor/file-transfer": "^2.0.5",
64
- "@capacitor/filesystem": "^8.1.2",
65
- "@capacitor/network": "^8.0.1",
66
- "@capawesome-team/capacitor-file-opener": "^8.0.1",
67
- "@capgo/capacitor-updater": "^8.51.8",
68
- "vue": "^3.5.25"
60
+ "@capacitor/app": ">=7.0.0 <9.0.0",
61
+ "@capacitor/core": ">=7.0.0 <9.0.0",
62
+ "@capacitor/device": ">=7.0.0 <9.0.0",
63
+ "@capacitor/file-transfer": ">=1.0.12 <3.0.0",
64
+ "@capacitor/filesystem": ">=7.0.0 <9.0.0",
65
+ "@capacitor/network": ">=7.0.0 <9.0.0",
66
+ "@capawesome-team/capacitor-file-opener": ">=7.0.0 <9.0.0",
67
+ "@capgo/capacitor-updater": ">=7.0.0 <9.0.0",
68
+ "vue": ">=3.4.0"
69
69
  },
70
70
  "peerDependenciesMeta": {
71
- "vue": {
71
+ "@capacitor/file-transfer": {
72
+ "optional": true
73
+ },
74
+ "@capacitor/filesystem": {
75
+ "optional": true
76
+ },
77
+ "@capacitor/network": {
78
+ "optional": true
79
+ },
80
+ "@capawesome-team/capacitor-file-opener": {
72
81
  "optional": true
73
82
  },
74
83
  "@capacitor/device": {
75
84
  "optional": true
85
+ },
86
+ "vue": {
87
+ "optional": true
76
88
  }
77
89
  },
78
90
  "scripts": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"ota.service-tv6eNQcB.js","names":[],"sources":["../src/config.ts","../src/device.ts","../src/api.service.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 Capucho 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 * 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 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 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.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, osFacts] = await Promise.all([\n getVersionCode(),\n getBundleVersion(),\n getDeviceId(),\n getPluginVersion(),\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 ...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 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","import type { PluginListenerHandle } from \"@capacitor/core\";\nimport { FileTransfer } from \"@capacitor/file-transfer\";\nimport { Directory, Filesystem } from \"@capacitor/filesystem\";\nimport { Network } from \"@capacitor/network\";\nimport type { ResolvedUpdate } from \"@capuchoo/core\";\nimport { getUpdaterConfig } from \"./config.js\";\n\nexport interface DownloadProgress {\n loaded: number;\n total: number;\n percent: number;\n}\n\n/** Cache file name prefix, derived from the app id so two flavours never collide. */\nfunction cachePrefix(): string {\n const { appId, appName } = getUpdaterConfig();\n const base = appId || appName;\n return `${base.replaceAll(/[^\\w.-]/g, \"-\")}-`;\n}\n\nfunction apkFileName(update: ResolvedUpdate): string {\n return `${cachePrefix()}${update.version}-${update.versionCode ?? 0}.apk`;\n}\n\n/**\n * Downloads a native APK into the app cache and returns its path.\n *\n * The file name embeds the app id, so a staging build and a production build\n * installed side by side cannot overwrite each other's download. The previous\n * implementation prefixed every file with a hard-coded app name.\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 network = await Network.getStatus();\n if (!network.connected) {\n throw new Error(\"Connect to the internet to download this update\");\n }\n\n // Clear older APKs first: they are typically 20-60 MB and the OS can evict\n // the cache mid-download if it is already full.\n await cleanApkCache();\n\n const fileName = apkFileName(update);\n const destination = await Filesystem.getUri({\n directory: Directory.Cache,\n path: fileName,\n });\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/** Removes this app's cached APKs. Failures are non-fatal. */\nexport async function cleanApkCache(): Promise<void> {\n const prefix = cachePrefix();\n\n try {\n const { files } = await Filesystem.readdir({\n directory: Directory.Cache,\n path: \"\",\n });\n\n await Promise.all(\n files\n .filter((file) => file.name.startsWith(prefix) && file.name.endsWith(\".apk\"))\n .map((file) => Filesystem.deleteFile({ directory: Directory.Cache, path: file.name })),\n );\n } catch (error) {\n console.warn(\"[capuchoo] could not clean the APK cache\", error);\n }\n}\n","import { Capacitor } from \"@capacitor/core\";\nimport { FileOpener } from \"@capawesome-team/capacitor-file-opener\";\nimport { getUpdaterConfig } from \"./config.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 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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"}