tauri-plugin-hot-update-api 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,58 @@
1
+ # tauri-plugin-hot-update-api
2
+
3
+ TypeScript bindings for [`tauri-plugin-hot-update`](https://github.com/yanqianglu/tauri-plugin-hot-update)
4
+ — hot update / OTA live updates (CodePush-style, self-hosted) for Tauri v2
5
+ mobile and desktop apps. Ship frontend bundle updates to installed apps
6
+ without a store release, guarded by an automatic anti-brick rollback state
7
+ machine.
8
+
9
+ Requires the Rust plugin to be installed and configured (manifest URL +
10
+ minisign public keys in `tauri.conf.json`) — see the
11
+ [plugin README](https://github.com/yanqianglu/tauri-plugin-hot-update) for
12
+ setup, signing, and publishing.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import {
18
+ check,
19
+ download,
20
+ notifyAppReady,
21
+ currentBundle,
22
+ onDownloadProgress,
23
+ } from "tauri-plugin-hot-update-api";
24
+
25
+ // 1. On every launch, as soon as your app shell has rendered:
26
+ // commits the running bundle as last-known-good. A launch that never
27
+ // acks is rolled back on the next boot. Safe to call unconditionally.
28
+ await notifyAppReady();
29
+
30
+ // 2. Whenever the app decides (launch/resume — the plugin never auto-polls):
31
+ const unlisten = await onDownloadProgress(({ downloaded, total }) => {
32
+ console.log(`OTA download: ${Math.round((downloaded / total) * 100)}%`);
33
+ });
34
+ const outcome = await download();
35
+ unlisten();
36
+
37
+ if (outcome.status === "staged") {
38
+ // The update applies on the next cold launch.
39
+ console.log(`v${outcome.version} ready — restart to apply`);
40
+ }
41
+
42
+ // Introspection:
43
+ const bundle = await currentBundle();
44
+ // { source: "ota" | "embedded", seq: number | null, version: string }
45
+ ```
46
+
47
+ `check()` fetches and verifies the manifest without downloading, for apps
48
+ that want to ask before pulling the archive.
49
+
50
+ ## Permissions
51
+
52
+ Grant the plugin's commands in your capability file:
53
+
54
+ ```json
55
+ { "permissions": ["hot-update:default"] }
56
+ ```
57
+
58
+ Dual-licensed under MIT or Apache-2.0, at your option.
package/dist/index.cjs ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ check: () => check,
24
+ currentBundle: () => currentBundle,
25
+ download: () => download,
26
+ notifyAppReady: () => notifyAppReady,
27
+ onDownloadProgress: () => onDownloadProgress,
28
+ reset: () => reset
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+ var import_core = require("@tauri-apps/api/core");
32
+ var import_event = require("@tauri-apps/api/event");
33
+ async function check() {
34
+ return (0, import_core.invoke)("plugin:hot-update|check");
35
+ }
36
+ async function download() {
37
+ return (0, import_core.invoke)("plugin:hot-update|download");
38
+ }
39
+ async function notifyAppReady() {
40
+ return (0, import_core.invoke)("plugin:hot-update|notify_app_ready");
41
+ }
42
+ async function currentBundle() {
43
+ return (0, import_core.invoke)("plugin:hot-update|current_bundle");
44
+ }
45
+ async function reset() {
46
+ return (0, import_core.invoke)("plugin:hot-update|reset");
47
+ }
48
+ async function onDownloadProgress(handler) {
49
+ return (0, import_event.listen)("hot-update://progress", (event) => {
50
+ handler(event.payload);
51
+ });
52
+ }
53
+ // Annotate the CommonJS export names for ESM import in node:
54
+ 0 && (module.exports = {
55
+ check,
56
+ currentBundle,
57
+ download,
58
+ notifyAppReady,
59
+ onDownloadProgress,
60
+ reset
61
+ });
@@ -0,0 +1,153 @@
1
+ import { UnlistenFn } from '@tauri-apps/api/event';
2
+
3
+ /** Where the bundle archive lives and what it must hash to. */
4
+ interface ArchiveInfo {
5
+ url: string;
6
+ /** Hex sha256 of the archive bytes. */
7
+ sha256: string;
8
+ /** Exact archive byte count. */
9
+ size: number;
10
+ }
11
+ /** The signed update manifest, as verified by the plugin. */
12
+ interface Manifest {
13
+ /** Bundle version; strictly newer than anything this install has seen. */
14
+ version: string;
15
+ /** Informational publish timestamp (RFC 3339). */
16
+ createdAt: string;
17
+ /** Minimum shell (app) version able to run this bundle. */
18
+ minShellVersion: string;
19
+ archive: ArchiveInfo;
20
+ }
21
+ /**
22
+ * Result of a check or download pass. Refusals are first-class outcomes,
23
+ * not errors — the pipeline saying "this manifest is not for us", with the
24
+ * reason preserved:
25
+ *
26
+ * - `available` — a verified, applicable update is offered (`check` only).
27
+ * - `staged` — downloaded, verified, and staged; it becomes active on the
28
+ * next cold launch (`download` only).
29
+ * - `upToDate` — the offered version is not newer than what this install
30
+ * has already seen (the everyday "no update" answer).
31
+ * - `blacklisted` — the offered archive previously failed a trial boot; a
32
+ * fixed release must ship under a new hash.
33
+ * - `shellTooOld` — the bundle needs a newer app binary; a store update is
34
+ * required first.
35
+ * - `alreadyStaged` — exactly this archive is already waiting for its
36
+ * trial boot.
37
+ */
38
+ type UpdateOutcome = {
39
+ status: "available";
40
+ manifest: Manifest;
41
+ } | {
42
+ status: "staged";
43
+ seq: number;
44
+ version: string;
45
+ } | {
46
+ status: "upToDate";
47
+ offered: string;
48
+ watermark: string;
49
+ } | {
50
+ status: "blacklisted";
51
+ version: string;
52
+ } | {
53
+ status: "shellTooOld";
54
+ required: string;
55
+ shell: string;
56
+ } | {
57
+ status: "alreadyStaged";
58
+ seq: number;
59
+ version: string;
60
+ };
61
+ /** {@link check} never downloads, so it never reports `staged`. */
62
+ type CheckOutcome = Exclude<UpdateOutcome, {
63
+ status: "staged";
64
+ }>;
65
+ /** {@link download} resolves what happened, never a bare offer. */
66
+ type DownloadOutcome = Exclude<UpdateOutcome, {
67
+ status: "available";
68
+ }>;
69
+ /**
70
+ * Result of {@link notifyAppReady}:
71
+ *
72
+ * - `committed` — this launch's trial bundle is now the last-known-good.
73
+ * - `alreadyCommitted` — steady state; the ack is idempotent.
74
+ * - `embeddedNoop` — serving embedded assets (or the plugin is disabled);
75
+ * nothing to commit.
76
+ * - `stale` — the booted bundle no longer matches on-disk state (e.g. a
77
+ * `reset()` ran mid-session); refusing to commit is the safe answer.
78
+ */
79
+ type AckOutcome = {
80
+ status: "committed";
81
+ seq: number;
82
+ } | {
83
+ status: "alreadyCommitted";
84
+ seq: number;
85
+ } | {
86
+ status: "embeddedNoop";
87
+ } | {
88
+ status: "stale";
89
+ seq: number;
90
+ };
91
+ /** Where the currently served frontend comes from. */
92
+ type BundleSource = "embedded" | "ota";
93
+ /** Snapshot of what this process is serving. */
94
+ interface CurrentBundle {
95
+ source: BundleSource;
96
+ /** Bundle sequence number when `source` is `"ota"`, otherwise null. */
97
+ seq: number | null;
98
+ version: string;
99
+ }
100
+ /** Payload of the download progress event (bytes). */
101
+ interface DownloadProgress {
102
+ downloaded: number;
103
+ total: number;
104
+ }
105
+ /**
106
+ * Fetch and verify the signed manifest, then report whether an update
107
+ * applies. Never downloads the archive. The app owns timing — the plugin
108
+ * does not auto-poll; call this on launch/resume.
109
+ *
110
+ * Rejects on transport or signature-verification failures, and when the
111
+ * plugin is disabled by config.
112
+ */
113
+ declare function check(): Promise<CheckOutcome>;
114
+ /**
115
+ * The full pipeline: check, and if an update applies — download, verify,
116
+ * extract, and stage it for the next cold launch. Emits throttled
117
+ * {@link onDownloadProgress} events while the archive streams.
118
+ *
119
+ * Concurrent calls are serialized; the loser reports `alreadyStaged` /
120
+ * `upToDate` instead of downloading twice. Rejects on transport or
121
+ * verification failures, and when the plugin is disabled by config.
122
+ */
123
+ declare function download(): Promise<DownloadOutcome>;
124
+ /**
125
+ * Commit the bundle this launch booted as last-known-good.
126
+ *
127
+ * Call once per launch, as soon as the app shell has mounted and rendered —
128
+ * deliberately independent of network reachability or auth, so a backend
129
+ * outage can never condemn a good bundle. A launch that never acks is
130
+ * rolled back and its bundle blacklisted on the next boot.
131
+ *
132
+ * Idempotent, and safe to call unconditionally: on embedded assets or with
133
+ * the plugin disabled it resolves `{ status: "embeddedNoop" }`.
134
+ */
135
+ declare function notifyAppReady(): Promise<AckOutcome>;
136
+ /** What is being served right now. */
137
+ declare function currentBundle(): Promise<CurrentBundle>;
138
+ /**
139
+ * Debug/support escape hatch: wipe all OTA state and bundles. The current
140
+ * session keeps serving what it booted; the next launch reverts to the
141
+ * embedded assets. A no-op while the plugin is disabled.
142
+ */
143
+ declare function reset(): Promise<void>;
144
+ /**
145
+ * Listen to {@link download} progress. Events are throttled plugin-side
146
+ * (at most ~10/s); the final 100% event (`downloaded === total`) is always
147
+ * delivered.
148
+ *
149
+ * Returns an unlisten function.
150
+ */
151
+ declare function onDownloadProgress(handler: (progress: DownloadProgress) => void): Promise<UnlistenFn>;
152
+
153
+ export { type AckOutcome, type ArchiveInfo, type BundleSource, type CheckOutcome, type CurrentBundle, type DownloadOutcome, type DownloadProgress, type Manifest, type UpdateOutcome, check, currentBundle, download, notifyAppReady, onDownloadProgress, reset };
@@ -0,0 +1,153 @@
1
+ import { UnlistenFn } from '@tauri-apps/api/event';
2
+
3
+ /** Where the bundle archive lives and what it must hash to. */
4
+ interface ArchiveInfo {
5
+ url: string;
6
+ /** Hex sha256 of the archive bytes. */
7
+ sha256: string;
8
+ /** Exact archive byte count. */
9
+ size: number;
10
+ }
11
+ /** The signed update manifest, as verified by the plugin. */
12
+ interface Manifest {
13
+ /** Bundle version; strictly newer than anything this install has seen. */
14
+ version: string;
15
+ /** Informational publish timestamp (RFC 3339). */
16
+ createdAt: string;
17
+ /** Minimum shell (app) version able to run this bundle. */
18
+ minShellVersion: string;
19
+ archive: ArchiveInfo;
20
+ }
21
+ /**
22
+ * Result of a check or download pass. Refusals are first-class outcomes,
23
+ * not errors — the pipeline saying "this manifest is not for us", with the
24
+ * reason preserved:
25
+ *
26
+ * - `available` — a verified, applicable update is offered (`check` only).
27
+ * - `staged` — downloaded, verified, and staged; it becomes active on the
28
+ * next cold launch (`download` only).
29
+ * - `upToDate` — the offered version is not newer than what this install
30
+ * has already seen (the everyday "no update" answer).
31
+ * - `blacklisted` — the offered archive previously failed a trial boot; a
32
+ * fixed release must ship under a new hash.
33
+ * - `shellTooOld` — the bundle needs a newer app binary; a store update is
34
+ * required first.
35
+ * - `alreadyStaged` — exactly this archive is already waiting for its
36
+ * trial boot.
37
+ */
38
+ type UpdateOutcome = {
39
+ status: "available";
40
+ manifest: Manifest;
41
+ } | {
42
+ status: "staged";
43
+ seq: number;
44
+ version: string;
45
+ } | {
46
+ status: "upToDate";
47
+ offered: string;
48
+ watermark: string;
49
+ } | {
50
+ status: "blacklisted";
51
+ version: string;
52
+ } | {
53
+ status: "shellTooOld";
54
+ required: string;
55
+ shell: string;
56
+ } | {
57
+ status: "alreadyStaged";
58
+ seq: number;
59
+ version: string;
60
+ };
61
+ /** {@link check} never downloads, so it never reports `staged`. */
62
+ type CheckOutcome = Exclude<UpdateOutcome, {
63
+ status: "staged";
64
+ }>;
65
+ /** {@link download} resolves what happened, never a bare offer. */
66
+ type DownloadOutcome = Exclude<UpdateOutcome, {
67
+ status: "available";
68
+ }>;
69
+ /**
70
+ * Result of {@link notifyAppReady}:
71
+ *
72
+ * - `committed` — this launch's trial bundle is now the last-known-good.
73
+ * - `alreadyCommitted` — steady state; the ack is idempotent.
74
+ * - `embeddedNoop` — serving embedded assets (or the plugin is disabled);
75
+ * nothing to commit.
76
+ * - `stale` — the booted bundle no longer matches on-disk state (e.g. a
77
+ * `reset()` ran mid-session); refusing to commit is the safe answer.
78
+ */
79
+ type AckOutcome = {
80
+ status: "committed";
81
+ seq: number;
82
+ } | {
83
+ status: "alreadyCommitted";
84
+ seq: number;
85
+ } | {
86
+ status: "embeddedNoop";
87
+ } | {
88
+ status: "stale";
89
+ seq: number;
90
+ };
91
+ /** Where the currently served frontend comes from. */
92
+ type BundleSource = "embedded" | "ota";
93
+ /** Snapshot of what this process is serving. */
94
+ interface CurrentBundle {
95
+ source: BundleSource;
96
+ /** Bundle sequence number when `source` is `"ota"`, otherwise null. */
97
+ seq: number | null;
98
+ version: string;
99
+ }
100
+ /** Payload of the download progress event (bytes). */
101
+ interface DownloadProgress {
102
+ downloaded: number;
103
+ total: number;
104
+ }
105
+ /**
106
+ * Fetch and verify the signed manifest, then report whether an update
107
+ * applies. Never downloads the archive. The app owns timing — the plugin
108
+ * does not auto-poll; call this on launch/resume.
109
+ *
110
+ * Rejects on transport or signature-verification failures, and when the
111
+ * plugin is disabled by config.
112
+ */
113
+ declare function check(): Promise<CheckOutcome>;
114
+ /**
115
+ * The full pipeline: check, and if an update applies — download, verify,
116
+ * extract, and stage it for the next cold launch. Emits throttled
117
+ * {@link onDownloadProgress} events while the archive streams.
118
+ *
119
+ * Concurrent calls are serialized; the loser reports `alreadyStaged` /
120
+ * `upToDate` instead of downloading twice. Rejects on transport or
121
+ * verification failures, and when the plugin is disabled by config.
122
+ */
123
+ declare function download(): Promise<DownloadOutcome>;
124
+ /**
125
+ * Commit the bundle this launch booted as last-known-good.
126
+ *
127
+ * Call once per launch, as soon as the app shell has mounted and rendered —
128
+ * deliberately independent of network reachability or auth, so a backend
129
+ * outage can never condemn a good bundle. A launch that never acks is
130
+ * rolled back and its bundle blacklisted on the next boot.
131
+ *
132
+ * Idempotent, and safe to call unconditionally: on embedded assets or with
133
+ * the plugin disabled it resolves `{ status: "embeddedNoop" }`.
134
+ */
135
+ declare function notifyAppReady(): Promise<AckOutcome>;
136
+ /** What is being served right now. */
137
+ declare function currentBundle(): Promise<CurrentBundle>;
138
+ /**
139
+ * Debug/support escape hatch: wipe all OTA state and bundles. The current
140
+ * session keeps serving what it booted; the next launch reverts to the
141
+ * embedded assets. A no-op while the plugin is disabled.
142
+ */
143
+ declare function reset(): Promise<void>;
144
+ /**
145
+ * Listen to {@link download} progress. Events are throttled plugin-side
146
+ * (at most ~10/s); the final 100% event (`downloaded === total`) is always
147
+ * delivered.
148
+ *
149
+ * Returns an unlisten function.
150
+ */
151
+ declare function onDownloadProgress(handler: (progress: DownloadProgress) => void): Promise<UnlistenFn>;
152
+
153
+ export { type AckOutcome, type ArchiveInfo, type BundleSource, type CheckOutcome, type CurrentBundle, type DownloadOutcome, type DownloadProgress, type Manifest, type UpdateOutcome, check, currentBundle, download, notifyAppReady, onDownloadProgress, reset };
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ // index.ts
2
+ import { invoke } from "@tauri-apps/api/core";
3
+ import { listen } from "@tauri-apps/api/event";
4
+ async function check() {
5
+ return invoke("plugin:hot-update|check");
6
+ }
7
+ async function download() {
8
+ return invoke("plugin:hot-update|download");
9
+ }
10
+ async function notifyAppReady() {
11
+ return invoke("plugin:hot-update|notify_app_ready");
12
+ }
13
+ async function currentBundle() {
14
+ return invoke("plugin:hot-update|current_bundle");
15
+ }
16
+ async function reset() {
17
+ return invoke("plugin:hot-update|reset");
18
+ }
19
+ async function onDownloadProgress(handler) {
20
+ return listen("hot-update://progress", (event) => {
21
+ handler(event.payload);
22
+ });
23
+ }
24
+ export {
25
+ check,
26
+ currentBundle,
27
+ download,
28
+ notifyAppReady,
29
+ onDownloadProgress,
30
+ reset
31
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "tauri-plugin-hot-update-api",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript API for tauri-plugin-hot-update — hot update / OTA live updates (CodePush-style, self-hosted) for Tauri v2 mobile and desktop apps",
5
+ "license": "(MIT OR Apache-2.0)",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/yanqianglu/tauri-plugin-hot-update.git"
9
+ },
10
+ "type": "module",
11
+ "main": "dist/index.cjs",
12
+ "module": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup index.ts --format esm,cjs --dts --clean",
27
+ "typecheck": "tsc --noEmit",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "peerDependencies": {
31
+ "@tauri-apps/api": ">=2.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@tauri-apps/api": "^2.10.1",
35
+ "tsup": "^8.5.1",
36
+ "typescript": "^5.9.3"
37
+ },
38
+ "keywords": [
39
+ "tauri",
40
+ "tauri-plugin",
41
+ "hot-update",
42
+ "ota",
43
+ "over-the-air",
44
+ "live-update",
45
+ "codepush",
46
+ "expo-updates",
47
+ "hotfix",
48
+ "self-hosted",
49
+ "ios",
50
+ "android",
51
+ "mobile"
52
+ ]
53
+ }