@capuchoo/updater 0.1.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 ADDED
@@ -0,0 +1,85 @@
1
+ # @capuchoo/updater
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
5
+ `@capgo/capacitor-updater`.
6
+
7
+ ```sh
8
+ npm install @capuchoo/updater @capuchoo/core
9
+ ```
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.
15
+
16
+ ## Three things, in order
17
+
18
+ ### 1. Call `notifyAppReady()` first
19
+
20
+ ```ts
21
+ // src/main.ts
22
+ import { notifyAppReady } from "@capuchoo/updater";
23
+
24
+ void notifyAppReady();
25
+ ```
26
+
27
+ Early, and unconditionally. It confirms that the bundle **currently running** booted. If the plugin
28
+ does not hear it within `appReadyTimeout` (10 s), it concludes the bundle crashed and rolls back to
29
+ the previous one — so gating this call behind a condition, or awaiting a network request before it,
30
+ reverts working updates. It is not a gate on auto-update.
31
+
32
+ ### 2. Configure the plugin through `capuchoUpdaterConfig()`
33
+
34
+ ```ts
35
+ // capacitor.config.ts
36
+ import { capuchoUpdaterConfig } from "@capuchoo/updater/capacitor";
37
+
38
+ plugins: {
39
+ CapacitorUpdater: capuchoUpdaterConfig({
40
+ apiUrl: process.env.VITE_UPDATE_API_URL,
41
+ channel: process.env.VITE_UPDATE_CHANNEL,
42
+ }),
43
+ }
44
+ ```
45
+
46
+ This returns `autoUpdate: "onlyDownload"`, because the app drives the install itself — with
47
+ `autoUpdate: true` the plugin and your UI both apply bundles, and a device can download the same
48
+ bundle twice or reload mid-prompt. It also **throws on an empty `apiUrl`** rather than accepting
49
+ one: an empty update URL does not fail at runtime, it silently disables updates, which ships a build
50
+ that never checks.
51
+
52
+ ### 3. Drive it from your UI
53
+
54
+ ```ts
55
+ import { useUpdater } from "@capuchoo/updater/vue";
56
+
57
+ const updater = useUpdater();
58
+ await updater.init();
59
+ ```
60
+
61
+ `UpdaterState` exposes `checking`, `downloading`, `installing`, `updateAvailable`, `currentUpdate`,
62
+ `progress`, `cachedPath`, `error`, `statusMessage` and `lastCheckMessage`.
63
+
64
+ Without Vue, use the services directly: `checkForUpdate()`, `downloadNativeUpdate()`,
65
+ `openNativeInstaller()`, `applyOtaUpdate()`, `getCurrentBundle()`, `discardBundle()`.
66
+
67
+ ## Errors are not "up to date"
68
+
69
+ `checkForUpdate()` throws `UpdateCheckBlockedError` when the server reports a configuration problem
70
+ — an unknown channel, or an environment mismatch between the build and the channel. Show it.
71
+ Treating every non-update response as "nothing to do" is how a broken channel goes unnoticed for
72
+ weeks.
73
+
74
+ `UpdaterConfigError` means the runtime was never configured — usually a missing `apiUrl`.
75
+
76
+ ## One request decides everything
77
+
78
+ The runtime asks `POST /api/update` and nothing else. It is the only endpoint that consults the
79
+ channel's assigned native version _and_ an OTA bundle's `min_update_version` gate, and it sends the
80
+ real current bundle version rather than a constant. Native updates outrank OTA, because the server
81
+ can legitimately return both.
82
+
83
+ ## Stability
84
+
85
+ Pre-1.0: the surface may change between minor versions.
@@ -0,0 +1,58 @@
1
+ //#region src/capacitor-config.d.ts
2
+ /**
3
+ * Builds the `CapacitorUpdater` plugin block for `capacitor.config.ts`.
4
+ *
5
+ * This exists because the plugin's `autoUpdate` mode has to agree with how the
6
+ * app drives updates, and getting it wrong fails in a way that is very hard to
7
+ * diagnose. The app template shipped `autoUpdate: true` while also calling
8
+ * `download()` and `set()` from JavaScript: the plugin applied bundles on its
9
+ * own schedule at the same time as the UI was downloading them, so a device
10
+ * could download the same bundle twice, or reload mid-prompt.
11
+ *
12
+ * `"onlyDownload"` is the mode this package is written for. The plugin fetches
13
+ * the bundle in the background and raises `updateAvailable`; the app decides
14
+ * when to apply it. Pass `mode: "manual"` to disable background downloads
15
+ * entirely and drive everything from `useUpdater`.
16
+ *
17
+ * Imported from `capacitor.config.ts`, so it must stay free of any runtime or
18
+ * DOM dependency.
19
+ */
20
+ type UpdaterMode = "onlyDownload" | "manual";
21
+ interface UpdaterPluginOptions {
22
+ /** Base URL of the Capucho backend. No trailing slash needed. */
23
+ apiUrl: string;
24
+ /** Channel this build defaults to. */
25
+ channel: string;
26
+ /**
27
+ * Version the plugin reports as the built-in bundle. Pass the app's
28
+ * package.json version - if this is stale, the server compares against the
29
+ * wrong baseline and re-serves bundles the device already has.
30
+ */
31
+ version: string;
32
+ mode?: UpdaterMode;
33
+ /** Milliseconds the plugin waits for `notifyAppReady` before rolling back. */
34
+ appReadyTimeout?: number;
35
+ responseTimeout?: number;
36
+ /**
37
+ * Whether the app may point the plugin at a different server at runtime.
38
+ * Leave off in production: it lets anything running in the WebView redirect
39
+ * update downloads.
40
+ */
41
+ allowModifyUrl?: boolean;
42
+ }
43
+ interface CapacitorUpdaterPluginConfig {
44
+ autoUpdate: boolean | "onlyDownload";
45
+ updateUrl: string;
46
+ statsUrl: string;
47
+ channelUrl: string;
48
+ defaultChannel: string;
49
+ version: string;
50
+ directUpdate: boolean;
51
+ appReadyTimeout: number;
52
+ responseTimeout: number;
53
+ allowModifyUrl: boolean;
54
+ }
55
+ declare function capuchoUpdaterConfig(options: UpdaterPluginOptions): CapacitorUpdaterPluginConfig;
56
+ //#endregion
57
+ export { CapacitorUpdaterPluginConfig, UpdaterMode, UpdaterPluginOptions, capuchoUpdaterConfig };
58
+ //# sourceMappingURL=capacitor-config.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,21 @@
1
+ //#region src/capacitor-config.ts
2
+ function capuchoUpdaterConfig(options) {
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.");
5
+ return {
6
+ autoUpdate: (options.mode ?? "onlyDownload") === "onlyDownload" ? "onlyDownload" : false,
7
+ updateUrl: `${apiUrl}/api/update`,
8
+ statsUrl: `${apiUrl}/api/stats`,
9
+ channelUrl: `${apiUrl}/api/channel_self`,
10
+ defaultChannel: options.channel,
11
+ version: options.version,
12
+ directUpdate: false,
13
+ appReadyTimeout: options.appReadyTimeout ?? 1e4,
14
+ responseTimeout: options.responseTimeout ?? 3e4,
15
+ allowModifyUrl: options.allowModifyUrl ?? false
16
+ };
17
+ }
18
+ //#endregion
19
+ export { capuchoUpdaterConfig };
20
+
21
+ //# sourceMappingURL=capacitor-config.js.map
@@ -0,0 +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"}
@@ -0,0 +1,121 @@
1
+ import { a as DownloadProgress, i as notifyAppReady, n as discardBundle, o as cleanApkCache, r as getCurrentBundle, s as downloadNativeUpdate, t as applyOtaUpdate } from "./ota.service-B3LrcCPr.js";
2
+ import { Environment, Platform, ResolvedUpdate, ResolvedUpdate as ResolvedUpdate$1, UpdateCheckResponse, UpdateCheckResponse as UpdateCheckResponse$1, UpdateEvent, UpdateEvent as UpdateEvent$1, UpdateKind } from "@capuchoo/core";
3
+ //#region src/api.service.d.ts
4
+ /** Raised when the updater is misconfigured, rather than reporting "up to date". */
5
+ declare class UpdaterConfigError extends Error {
6
+ readonly problems: string[];
7
+ constructor(problems: string[]);
8
+ }
9
+ /** Raised when the server says the request itself cannot be served. */
10
+ declare class UpdateCheckBlockedError extends Error {
11
+ readonly response: UpdateCheckResponse$1;
12
+ constructor(response: UpdateCheckResponse$1);
13
+ }
14
+ /**
15
+ * Asks the server what this device should be running.
16
+ *
17
+ * One request, one endpoint. The app template used to call
18
+ * `GET /api/native-updates/check` for native updates *and*
19
+ * `POST /api/update` for OTA, which meant two sources of truth: the native
20
+ * endpoint ignores the channel's assigned native version and the
21
+ * `min_update_version` gate, so a device could be told to install an OTA
22
+ * bundle its binary was too old to run.
23
+ */
24
+ declare function checkForUpdate(): Promise<ResolvedUpdate$1 | null>;
25
+ /**
26
+ * Records a native update lifecycle event.
27
+ *
28
+ * Best effort: analytics must never break an update. OTA events are reported
29
+ * by the plugin itself through its `statsUrl`, so only native ones are sent
30
+ * from here.
31
+ */
32
+ declare function logUpdateEvent(event: UpdateEvent$1, update: ResolvedUpdate$1, details?: {
33
+ error?: string;
34
+ }): Promise<void>;
35
+ //#endregion
36
+ //#region src/config.d.ts
37
+ /**
38
+ * Runtime configuration for the updater.
39
+ *
40
+ * Values come from the build's `VITE_*` variables, which the CLI injects from
41
+ * the flavour's env file. `configureUpdater` lets an app override any of them
42
+ * at startup - useful for tests and for apps that resolve their endpoint from
43
+ * a login response rather than at build time.
44
+ */
45
+ interface UpdaterConfig {
46
+ /** Base URL of the Capucho backend, with no trailing slash. */
47
+ apiUrl: string;
48
+ /** Bundle identifier of this build. Must match what the CLI published. */
49
+ appId: string;
50
+ /** Human-readable name, used in prompts and in the APK cache file name. */
51
+ appName: string;
52
+ /** Channel to consult. Bound to an environment server-side. */
53
+ channel: string;
54
+ /** Free-form: an app may use flavours beyond dev/staging/prod. */
55
+ environment: string;
56
+ /** Milliseconds before an update check is abandoned. */
57
+ timeoutMs: number;
58
+ }
59
+ /**
60
+ * Overrides configuration resolved from the build. Call before `init`.
61
+ * Passing `{}` clears previous overrides.
62
+ */
63
+ declare function configureUpdater(next: Partial<UpdaterConfig>): void;
64
+ declare function getUpdaterConfig(): UpdaterConfig;
65
+ /**
66
+ * Reasons the updater cannot run, as user-facing strings.
67
+ *
68
+ * The previous implementation defaulted `apiUrl` to a hard-coded Render URL and
69
+ * `appId` to a hard-coded bundle id. A build with a missing variable therefore
70
+ * silently pointed at somebody else's backend, or asked for the wrong app, and
71
+ * reported "you are up to date". Failing loudly is the whole point of this
72
+ * function.
73
+ */
74
+ declare function describeConfigProblems(config: UpdaterConfig): string[];
75
+ //#endregion
76
+ //#region src/device.d.ts
77
+ /**
78
+ * Facts about the running build that the server needs in order to decide
79
+ * whether an update applies.
80
+ */
81
+ /**
82
+ * Native build number of the installed binary.
83
+ *
84
+ * Returns 0 off-device. The old implementation returned 999999 on web, which
85
+ * meant a browser session claimed to be newer than every published release and
86
+ * so never saw an update - masking the very bug you would be debugging.
87
+ */
88
+ declare function getVersionCode(): Promise<number>;
89
+ /**
90
+ * Semantic version of the web bundle currently applied.
91
+ *
92
+ * `"builtin"` means no OTA bundle has been applied yet and the app is running
93
+ * the assets compiled into the binary. The server treats it as 0.0.0, so it
94
+ * must be reported honestly rather than sent as a constant.
95
+ */
96
+ declare function getBundleVersion(): Promise<string>;
97
+ /**
98
+ * Stable per-install identifier, supplied by the OTA plugin.
99
+ *
100
+ * The plugin persists this natively. Reading `localStorage.device_id` instead -
101
+ * as the app template did - returns null on a fresh install and is wiped
102
+ * whenever the WebView data is cleared, so channel overrides and per-device
103
+ * stats silently stopped working.
104
+ */
105
+ declare function getDeviceId(): Promise<string>;
106
+ declare function getPlatform(): "android" | "ios" | "web";
107
+ declare function isNative(): boolean;
108
+ //#endregion
109
+ //#region src/install.service.d.ts
110
+ /**
111
+ * Hands a downloaded APK to the Android package installer.
112
+ *
113
+ * Requires `REQUEST_INSTALL_PACKAGES` in the manifest - the Trapeze config for
114
+ * each flavour merges it in - and the user must have allowed this app to
115
+ * install unknown apps. Both failures surface as opaque platform errors, so
116
+ * they are translated into something a user can act on.
117
+ */
118
+ declare function openNativeInstaller(path: string): Promise<void>;
119
+ //#endregion
120
+ export { type DownloadProgress, type Environment, type Platform, type ResolvedUpdate, UpdateCheckBlockedError, type UpdateCheckResponse, type UpdateEvent, type UpdateKind, type UpdaterConfig, UpdaterConfigError, applyOtaUpdate, checkForUpdate, cleanApkCache, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, getBundleVersion, getCurrentBundle, getDeviceId, getPlatform, getUpdaterConfig, getVersionCode, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller };
121
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/api.service.ts","../src/config.ts","../src/device.ts","../src/install.service.ts"],"mappings":";;;;cAaa,2BAA2B;WAC7B;EAEG,YAAA;;;cAQD,gCAAgC;WAClC,UAAU;EAEP,YAAA,UAAU;;;;;;;;;;;;iBAuCF,kBAAkB,QAAQ;;;;;;;;iBA8C1B,eACpB,OAAO,eACP,QAAQ,kBACR;EAAY;IACX;;;;;;;;;;;UC5Gc;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;iBAoBc,iBAAiB,MAAM,QAAQ;iBAS/B,oBAAoB;;;;;;;;;;iBAuBpB,uBAAuB,QAAQ;;;;;;;;;;;;;;iBCxDzB,kBAAkB;;;;;;;;iBAkBlB,oBAAoB;;;;;;;;;iBAmBpB,eAAe;iBAWrB;iBAIA;;;;;;;;;;;iBCtDM,oBAAoB,eAAe"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import { _ as configureUpdater, a as openNativeInstaller, c as UpdateCheckBlockedError, d as logUpdateEvent, f as getBundleVersion, g as isNative, h as getVersionCode, i as notifyAppReady, l as UpdaterConfigError, m as getPlatform, n as discardBundle, o as cleanApkCache, p as getDeviceId, r as getCurrentBundle, s as downloadNativeUpdate, t as applyOtaUpdate, u as checkForUpdate, v as describeConfigProblems, y as getUpdaterConfig } from "./ota.service-t2_hIxcy.js";
2
+ export { UpdateCheckBlockedError, UpdaterConfigError, applyOtaUpdate, checkForUpdate, cleanApkCache, configureUpdater, describeConfigProblems, discardBundle, downloadNativeUpdate, getBundleVersion, getCurrentBundle, getDeviceId, getPlatform, getUpdaterConfig, getVersionCode, isNative, logUpdateEvent, notifyAppReady, openNativeInstaller };
@@ -0,0 +1,49 @@
1
+ import { ResolvedUpdate } from "@capuchoo/core";
2
+ //#region src/download.service.d.ts
3
+ interface DownloadProgress {
4
+ loaded: number;
5
+ total: number;
6
+ percent: number;
7
+ }
8
+ /**
9
+ * Downloads a native APK into the app cache and returns its path.
10
+ *
11
+ * The file name embeds the app id, so a staging build and a production build
12
+ * installed side by side cannot overwrite each other's download. The previous
13
+ * implementation prefixed every file with a hard-coded app name.
14
+ */
15
+ declare function downloadNativeUpdate(update: ResolvedUpdate, onProgress: (progress: DownloadProgress) => void): Promise<string>;
16
+ /** Removes this app's cached APKs. Failures are non-fatal. */
17
+ declare function cleanApkCache(): Promise<void>;
18
+ //#endregion
19
+ //#region src/ota.service.d.ts
20
+ /**
21
+ * Thin wrapper over the OTA plugin.
22
+ *
23
+ * Downloading and applying a web bundle stays with `@capgo/capacitor-updater`:
24
+ * it owns the native bundle store, the atomic swap and the rollback. This
25
+ * module only sequences those calls correctly.
26
+ */
27
+ /**
28
+ * Confirms the current bundle booted successfully.
29
+ *
30
+ * **This must be called once, early, on every app start.** If the plugin does
31
+ * not hear it within `appReadyTimeout`, it assumes the new bundle crashed and
32
+ * rolls back to the previous one - which looks exactly like "the update did
33
+ * not install".
34
+ */
35
+ declare function notifyAppReady(): Promise<void>;
36
+ /** The bundle currently applied, or null off-device. */
37
+ declare function getCurrentBundle(): Promise<import("@capgo/capacitor-updater").CurrentBundleResult | null>;
38
+ /**
39
+ * Downloads an OTA bundle and applies it.
40
+ *
41
+ * `set` swaps the active bundle and reloads the WebView, so nothing after it
42
+ * runs. It is called last on purpose.
43
+ */
44
+ declare function applyOtaUpdate(update: ResolvedUpdate): Promise<void>;
45
+ /** Discards a downloaded bundle that will not be applied. */
46
+ declare function discardBundle(bundleId: string): Promise<void>;
47
+ //#endregion
48
+ export { DownloadProgress as a, notifyAppReady as i, discardBundle as n, cleanApkCache as o, getCurrentBundle as r, downloadNativeUpdate as s, applyOtaUpdate as t };
49
+ //# sourceMappingURL=ota.service-B3LrcCPr.d.ts.map
@@ -0,0 +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"}