tauri-remote-ui 1.0.1-alpha.8 → 1.0.1
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 +17 -0
- package/api/core/index.cjs +30 -36
- package/api/core/index.d.ts +8 -0
- package/api/core/index.js +31 -37
- package/api/event/index.cjs +13 -19
- package/api/event/index.d.ts +3 -3
- package/api/event/index.js +14 -20
- package/package.json +6 -6
- package/socket.cjs +109 -71
- package/socket.d.ts +27 -10
- package/socket.js +108 -72
- package/version.cjs +13 -0
- package/version.d.ts +9 -0
- package/version.js +11 -0
package/README.md
CHANGED
|
@@ -17,6 +17,23 @@
|
|
|
17
17
|
- Network level access control setting
|
|
18
18
|
- Network latency tracking
|
|
19
19
|
|
|
20
|
+
## ⚠️ Security Warning — Read Before Exposing the Server
|
|
21
|
+
|
|
22
|
+
> **This plugin currently ships with NO authentication, NO authorization, and NO transport encryption.**
|
|
23
|
+
> Anyone who can reach the bound port can drive your application's UI and invoke any Tauri command your app exposes — there is no login, no API key, no TLS, and no rate limit.
|
|
24
|
+
|
|
25
|
+
The only access control today is a coarse **network-scope filter** (`OriginType`) applied to the peer's IP at TCP-accept time:
|
|
26
|
+
|
|
27
|
+
| Scope | Bind address | Who can connect |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `Localhost` *(default, recommended)* | `127.0.0.1` | This machine only. |
|
|
30
|
+
| `Subnet` | `0.0.0.0` | Any host on **any** of this machine's local IPv4/IPv6 subnets, plus loopback. |
|
|
31
|
+
| `Any` | `0.0.0.0` | **Anyone routable to this machine.** No filter applied. |
|
|
32
|
+
|
|
33
|
+
A built-in token / SSL / SSO story is on the roadmap (see *Planned Features*), but until it lands, **the only safe public-network deployment is one where you have added authentication yourself in front of this plugin** (mutual-TLS reverse proxy, WireGuard, Tailscale, Cloudflare Access, etc.).
|
|
34
|
+
|
|
35
|
+
To audit who is being allowed through at runtime, initialize a logger in your host app and run with `RUST_LOG=tauri_remote_ui=debug` — every accept/reject decision is logged with the peer IP and the active scope.
|
|
36
|
+
|
|
20
37
|
## Supports
|
|
21
38
|
|
|
22
39
|
|Environment|Support|
|
package/api/core/index.cjs
CHANGED
|
@@ -8,52 +8,46 @@ var socket = require('../../socket.cjs');
|
|
|
8
8
|
*
|
|
9
9
|
* This module handles sending messages to the Tauri application via WebSocket
|
|
10
10
|
*/
|
|
11
|
+
/** Monotonic request id, encapsulated in module scope. */
|
|
12
|
+
let nextRequestId = 0;
|
|
11
13
|
/**
|
|
12
|
-
* Invoke a command on the Tauri application
|
|
13
|
-
* Falls back to WebSocket if Tauri IPC is not available
|
|
14
|
+
* Invoke a command on the Tauri application.
|
|
15
|
+
* Falls back to a WebSocket transport if Tauri IPC is not available.
|
|
14
16
|
*
|
|
15
17
|
* @param cmd - The command name to invoke
|
|
16
18
|
* @param args - Arguments to pass to the command
|
|
17
19
|
* @param options - Options for the command
|
|
18
20
|
*/
|
|
19
|
-
let msg_id = 0;
|
|
20
21
|
async function invoke(cmd, args, options) {
|
|
21
|
-
if ((
|
|
22
|
-
window.__TAURI__ && window.__TAURI__.invoke) {
|
|
22
|
+
if (socket.hasTauriRuntime()) {
|
|
23
23
|
return await core.invoke(cmd, args, options);
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (socket.wsReady) {
|
|
29
|
-
await socket.wsReady;
|
|
30
|
-
}
|
|
31
|
-
if (socket.ws && socket.ws.readyState === WebSocket.OPEN) {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
const msg = {
|
|
34
|
-
id: ++msg_id,
|
|
35
|
-
cmd, args, options
|
|
36
|
-
};
|
|
37
|
-
let clear = setTimeout(() => {
|
|
38
|
-
delete socket.filterCollection[msg_id];
|
|
39
|
-
reject(`Invoke Timeout. cmd : ${cmd}`);
|
|
40
|
-
}, 30000);
|
|
41
|
-
socket.filterCollection[msg_id] = ({ status, payload }) => {
|
|
42
|
-
clearTimeout(clear);
|
|
43
|
-
if (status = "success") {
|
|
44
|
-
resolve(payload);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
reject(payload);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
socket.ws.send(JSON.stringify(msg));
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
throw new Error('No WebSocket or Tauri IPC available to invoke');
|
|
55
|
-
}
|
|
25
|
+
socket.initWebSocket();
|
|
26
|
+
if (socket.wsReady) {
|
|
27
|
+
await socket.wsReady;
|
|
56
28
|
}
|
|
29
|
+
if (!socket.ws || socket.ws.readyState !== WebSocket.OPEN) {
|
|
30
|
+
throw new Error('No WebSocket or Tauri IPC available to invoke');
|
|
31
|
+
}
|
|
32
|
+
const requestId = ++nextRequestId;
|
|
33
|
+
return await new Promise((resolve, reject) => {
|
|
34
|
+
const message = { id: requestId, cmd, args, options };
|
|
35
|
+
const timeoutHandle = setTimeout(() => {
|
|
36
|
+
delete socket.filterCollection[requestId];
|
|
37
|
+
reject(new Error(`Invoke timeout. cmd: ${cmd}`));
|
|
38
|
+
}, 30000);
|
|
39
|
+
socket.filterCollection[requestId] = ({ status, payload }) => {
|
|
40
|
+
clearTimeout(timeoutHandle);
|
|
41
|
+
delete socket.filterCollection[requestId];
|
|
42
|
+
if (status === socket.RpcStatus.Success) {
|
|
43
|
+
resolve(payload);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
reject(payload);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
socket.ws.send(JSON.stringify(message));
|
|
50
|
+
});
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
exports.invoke = invoke;
|
package/api/core/index.d.ts
CHANGED
|
@@ -4,4 +4,12 @@
|
|
|
4
4
|
* This module handles sending messages to the Tauri application via WebSocket
|
|
5
5
|
*/
|
|
6
6
|
import { InvokeArgs, InvokeOptions } from '@tauri-apps/api/core';
|
|
7
|
+
/**
|
|
8
|
+
* Invoke a command on the Tauri application.
|
|
9
|
+
* Falls back to a WebSocket transport if Tauri IPC is not available.
|
|
10
|
+
*
|
|
11
|
+
* @param cmd - The command name to invoke
|
|
12
|
+
* @param args - Arguments to pass to the command
|
|
13
|
+
* @param options - Options for the command
|
|
14
|
+
*/
|
|
7
15
|
export declare function invoke<T>(cmd: string, args?: InvokeArgs, options?: InvokeOptions): Promise<T>;
|
package/api/core/index.js
CHANGED
|
@@ -1,57 +1,51 @@
|
|
|
1
1
|
import { invoke as invoke$1 } from '@tauri-apps/api/core';
|
|
2
|
-
import { initWebSocket, wsReady, ws, filterCollection } from '../../socket.js';
|
|
2
|
+
import { hasTauriRuntime, initWebSocket, wsReady, ws, filterCollection, RpcStatus } from '../../socket.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Core API module for Tauri Remote UI
|
|
6
6
|
*
|
|
7
7
|
* This module handles sending messages to the Tauri application via WebSocket
|
|
8
8
|
*/
|
|
9
|
+
/** Monotonic request id, encapsulated in module scope. */
|
|
10
|
+
let nextRequestId = 0;
|
|
9
11
|
/**
|
|
10
|
-
* Invoke a command on the Tauri application
|
|
11
|
-
* Falls back to WebSocket if Tauri IPC is not available
|
|
12
|
+
* Invoke a command on the Tauri application.
|
|
13
|
+
* Falls back to a WebSocket transport if Tauri IPC is not available.
|
|
12
14
|
*
|
|
13
15
|
* @param cmd - The command name to invoke
|
|
14
16
|
* @param args - Arguments to pass to the command
|
|
15
17
|
* @param options - Options for the command
|
|
16
18
|
*/
|
|
17
|
-
let msg_id = 0;
|
|
18
19
|
async function invoke(cmd, args, options) {
|
|
19
|
-
if ((
|
|
20
|
-
window.__TAURI__ && window.__TAURI__.invoke) {
|
|
20
|
+
if (hasTauriRuntime()) {
|
|
21
21
|
return await invoke$1(cmd, args, options);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (wsReady) {
|
|
27
|
-
await wsReady;
|
|
28
|
-
}
|
|
29
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
const msg = {
|
|
32
|
-
id: ++msg_id,
|
|
33
|
-
cmd, args, options
|
|
34
|
-
};
|
|
35
|
-
let clear = setTimeout(() => {
|
|
36
|
-
delete filterCollection[msg_id];
|
|
37
|
-
reject(`Invoke Timeout. cmd : ${cmd}`);
|
|
38
|
-
}, 30000);
|
|
39
|
-
filterCollection[msg_id] = ({ status, payload }) => {
|
|
40
|
-
clearTimeout(clear);
|
|
41
|
-
if (status = "success") {
|
|
42
|
-
resolve(payload);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
reject(payload);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
ws.send(JSON.stringify(msg));
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
throw new Error('No WebSocket or Tauri IPC available to invoke');
|
|
53
|
-
}
|
|
23
|
+
initWebSocket();
|
|
24
|
+
if (wsReady) {
|
|
25
|
+
await wsReady;
|
|
54
26
|
}
|
|
27
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
28
|
+
throw new Error('No WebSocket or Tauri IPC available to invoke');
|
|
29
|
+
}
|
|
30
|
+
const requestId = ++nextRequestId;
|
|
31
|
+
return await new Promise((resolve, reject) => {
|
|
32
|
+
const message = { id: requestId, cmd, args, options };
|
|
33
|
+
const timeoutHandle = setTimeout(() => {
|
|
34
|
+
delete filterCollection[requestId];
|
|
35
|
+
reject(new Error(`Invoke timeout. cmd: ${cmd}`));
|
|
36
|
+
}, 30000);
|
|
37
|
+
filterCollection[requestId] = ({ status, payload }) => {
|
|
38
|
+
clearTimeout(timeoutHandle);
|
|
39
|
+
delete filterCollection[requestId];
|
|
40
|
+
if (status === RpcStatus.Success) {
|
|
41
|
+
resolve(payload);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
reject(payload);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
ws.send(JSON.stringify(message));
|
|
48
|
+
});
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
export { invoke };
|
package/api/event/index.cjs
CHANGED
|
@@ -9,34 +9,28 @@ var socket = require('../../socket.cjs');
|
|
|
9
9
|
* This module handles listening to events from the Tauri application via WebSocket
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
|
-
* Listen to events from the Tauri application
|
|
13
|
-
* Falls back to WebSocket if Tauri Event API is not available
|
|
12
|
+
* Listen to events from the Tauri application.
|
|
13
|
+
* Falls back to a WebSocket transport if the Tauri Event API is not available.
|
|
14
14
|
*
|
|
15
15
|
* @param event - The event name to listen for
|
|
16
16
|
* @param handler - Callback to handle the event
|
|
17
17
|
* @param options - Options for the event listener
|
|
18
18
|
*/
|
|
19
19
|
async function listen(event$1, handler, options) {
|
|
20
|
-
if ((
|
|
21
|
-
window.__TAURI__ && window.__TAURI__.invoke) {
|
|
20
|
+
if (socket.hasTauriRuntime()) {
|
|
22
21
|
return await event.listen(event$1, handler, options);
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (socket.wsReady) {
|
|
28
|
-
await socket.wsReady;
|
|
29
|
-
}
|
|
30
|
-
// Handle WebSocket messages for events
|
|
31
|
-
const messageHandler = ({ data }) => {
|
|
32
|
-
handler(data);
|
|
33
|
-
};
|
|
34
|
-
socket.listenEvent.addEventListener(event$1, messageHandler);
|
|
35
|
-
// Return an unlisten function
|
|
36
|
-
return () => {
|
|
37
|
-
socket.listenEvent.removeEventListener(event$1, messageHandler);
|
|
38
|
-
};
|
|
23
|
+
socket.initWebSocket();
|
|
24
|
+
if (socket.wsReady) {
|
|
25
|
+
await socket.wsReady;
|
|
39
26
|
}
|
|
27
|
+
const messageHandler = (e) => {
|
|
28
|
+
handler(e.data);
|
|
29
|
+
};
|
|
30
|
+
socket.listenEvent.addEventListener(event$1, messageHandler);
|
|
31
|
+
return () => {
|
|
32
|
+
socket.listenEvent.removeEventListener(event$1, messageHandler);
|
|
33
|
+
};
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
Object.defineProperty(exports, "latencyMs", {
|
package/api/event/index.d.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { EventCallback, EventName, Options, UnlistenFn } from '@tauri-apps/api/event';
|
|
7
7
|
export type { UnlistenFn } from '@tauri-apps/api/event';
|
|
8
|
-
export { latencyMs } from
|
|
8
|
+
export { latencyMs } from '../../socket';
|
|
9
9
|
/**
|
|
10
|
-
* Listen to events from the Tauri application
|
|
11
|
-
* Falls back to WebSocket if Tauri Event API is not available
|
|
10
|
+
* Listen to events from the Tauri application.
|
|
11
|
+
* Falls back to a WebSocket transport if the Tauri Event API is not available.
|
|
12
12
|
*
|
|
13
13
|
* @param event - The event name to listen for
|
|
14
14
|
* @param handler - Callback to handle the event
|
package/api/event/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { listen as listen$1 } from '@tauri-apps/api/event';
|
|
2
|
-
import {
|
|
2
|
+
import { hasTauriRuntime, initWebSocket, listenEvent, wsReady } from '../../socket.js';
|
|
3
3
|
export { latencyMs } from '../../socket.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -8,34 +8,28 @@ export { latencyMs } from '../../socket.js';
|
|
|
8
8
|
* This module handles listening to events from the Tauri application via WebSocket
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
|
-
* Listen to events from the Tauri application
|
|
12
|
-
* Falls back to WebSocket if Tauri Event API is not available
|
|
11
|
+
* Listen to events from the Tauri application.
|
|
12
|
+
* Falls back to a WebSocket transport if the Tauri Event API is not available.
|
|
13
13
|
*
|
|
14
14
|
* @param event - The event name to listen for
|
|
15
15
|
* @param handler - Callback to handle the event
|
|
16
16
|
* @param options - Options for the event listener
|
|
17
17
|
*/
|
|
18
18
|
async function listen(event, handler, options) {
|
|
19
|
-
if ((
|
|
20
|
-
window.__TAURI__ && window.__TAURI__.invoke) {
|
|
19
|
+
if (hasTauriRuntime()) {
|
|
21
20
|
return await listen$1(event, handler, options);
|
|
22
21
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (wsReady) {
|
|
27
|
-
await wsReady;
|
|
28
|
-
}
|
|
29
|
-
// Handle WebSocket messages for events
|
|
30
|
-
const messageHandler = ({ data }) => {
|
|
31
|
-
handler(data);
|
|
32
|
-
};
|
|
33
|
-
listenEvent.addEventListener(event, messageHandler);
|
|
34
|
-
// Return an unlisten function
|
|
35
|
-
return () => {
|
|
36
|
-
listenEvent.removeEventListener(event, messageHandler);
|
|
37
|
-
};
|
|
22
|
+
initWebSocket();
|
|
23
|
+
if (wsReady) {
|
|
24
|
+
await wsReady;
|
|
38
25
|
}
|
|
26
|
+
const messageHandler = (e) => {
|
|
27
|
+
handler(e.data);
|
|
28
|
+
};
|
|
29
|
+
listenEvent.addEventListener(event, messageHandler);
|
|
30
|
+
return () => {
|
|
31
|
+
listenEvent.removeEventListener(event, messageHandler);
|
|
32
|
+
};
|
|
39
33
|
}
|
|
40
34
|
|
|
41
35
|
export { listen };
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tauri-remote-ui",
|
|
3
3
|
"license": "AGPL-3.0-only",
|
|
4
|
-
"version": "1.0.1
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"author": "DraviaVemal",
|
|
6
6
|
"description": "A Tauri plugin that exposes the application's UI to a web browser, allowing full interaction while the native app continues running. This enables end-to-end UI testing using existing web-based testing tools without requiring modifications to the app itself.",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"types": "./index.d.ts",
|
|
9
|
-
"main": "./index.cjs",
|
|
10
|
-
"module": "./index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
11
|
"repository": {
|
|
12
|
-
"url": "
|
|
12
|
+
"url": "https://github.com/DraviaVemal/tauri-remote-ui"
|
|
13
13
|
},
|
|
14
14
|
"readme": "./README.md",
|
|
15
15
|
"exports": {
|
|
@@ -50,4 +50,4 @@
|
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@tauri-apps/api": ">=2.0.0"
|
|
52
52
|
}
|
|
53
|
-
}
|
|
53
|
+
}
|
package/socket.cjs
CHANGED
|
@@ -1,100 +1,138 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var version = require('./version.cjs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tauri Remote UI - WebSocket bridge
|
|
7
|
+
*
|
|
8
|
+
* Establishes (and re-uses) a WebSocket connection back to the Tauri host
|
|
9
|
+
* application, providing the transport for the `invoke` and `listen` shims
|
|
10
|
+
* exported from `./api/core` and `./api/event`.
|
|
11
|
+
*/
|
|
12
|
+
/** Wire prefix used by the version-handshake exchange. */
|
|
13
|
+
const VERSION_PREFIX = 'version:';
|
|
3
14
|
/**
|
|
4
|
-
*
|
|
15
|
+
* Status discriminator on the response payload sent back from the Rust side.
|
|
5
16
|
*
|
|
6
|
-
*
|
|
7
|
-
* It provides WebSocket initialization and shared WebSocket state for communicating
|
|
8
|
-
* with a Tauri application.
|
|
17
|
+
* Mirrors the `RpcStatus` enum in `src/models.rs` — keep both in sync.
|
|
9
18
|
*/
|
|
19
|
+
const RpcStatus = {
|
|
20
|
+
Success: 'success',
|
|
21
|
+
Error: 'error',
|
|
22
|
+
};
|
|
23
|
+
/** Returns true if the page is running inside a Tauri webview. */
|
|
24
|
+
function hasTauriRuntime() {
|
|
25
|
+
const w = window;
|
|
26
|
+
return Boolean((w.__TAURI_INTERNALS__ && w.__TAURI_INTERNALS__.invoke) ||
|
|
27
|
+
(w.__TAURI__ && w.__TAURI__.invoke));
|
|
28
|
+
}
|
|
10
29
|
exports.ws = null;
|
|
11
|
-
|
|
30
|
+
const listenEvent = new EventTarget();
|
|
12
31
|
exports.wsReady = null;
|
|
13
32
|
const filterCollection = {};
|
|
14
33
|
exports.latencyMs = 0;
|
|
15
|
-
/**
|
|
16
|
-
* Get the WebSocket URL based on the current window location
|
|
17
|
-
*/
|
|
34
|
+
/** Build the WebSocket URL for the RPC connection. */
|
|
18
35
|
function getWsUrl() {
|
|
19
36
|
const loc = window.location;
|
|
20
37
|
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
21
|
-
|
|
22
|
-
return wsUrl;
|
|
38
|
+
return `${proto}//${loc.host}/remote_ui_ws`;
|
|
23
39
|
}
|
|
24
|
-
|
|
40
|
+
/** Build the disconnect-redirect URL the page navigates to on close. */
|
|
41
|
+
function getDisconnectUrl() {
|
|
25
42
|
const loc = window.location;
|
|
26
|
-
|
|
27
|
-
return wsUrl;
|
|
43
|
+
return `${loc.protocol}//${loc.host}/remote_ui_disconnect`;
|
|
28
44
|
}
|
|
29
45
|
/**
|
|
30
|
-
* Initialize the WebSocket connection
|
|
31
|
-
*
|
|
46
|
+
* Initialize the WebSocket connection on first use. A no-op when running
|
|
47
|
+
* inside Tauri (native IPC is preferred) or when the socket is already open.
|
|
32
48
|
*/
|
|
33
49
|
function initWebSocket() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
throw new Error("Moving to WS backup for Tauri Backend");
|
|
42
|
-
}
|
|
50
|
+
if (hasTauriRuntime()) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (exports.ws) {
|
|
54
|
+
return;
|
|
43
55
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
console.info('Tauri-Remote-UI : Remote RPC Attempting...');
|
|
57
|
+
const wsUrl = getWsUrl();
|
|
58
|
+
try {
|
|
59
|
+
let lastPingTimestamp = Date.now();
|
|
60
|
+
let pingPongTimer;
|
|
61
|
+
const socket = new WebSocket(wsUrl);
|
|
62
|
+
exports.ws = socket;
|
|
63
|
+
exports.wsReady = new Promise((resolve, reject) => {
|
|
64
|
+
socket.onopen = () => {
|
|
65
|
+
console.info('Tauri-Remote-UI : Remote Connected.');
|
|
66
|
+
socket.send(`${VERSION_PREFIX}${version.PACKAGE_VERSION}`);
|
|
67
|
+
lastPingTimestamp = Date.now();
|
|
68
|
+
socket.send('ping');
|
|
69
|
+
pingPongTimer = setInterval(() => {
|
|
56
70
|
lastPingTimestamp = Date.now();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
socket.send('ping');
|
|
72
|
+
}, 10000);
|
|
73
|
+
resolve();
|
|
74
|
+
};
|
|
75
|
+
socket.onmessage = ({ data }) => {
|
|
76
|
+
var _a;
|
|
77
|
+
if (data === 'pong') {
|
|
78
|
+
exports.latencyMs = Date.now() - lastPingTimestamp;
|
|
79
|
+
if (exports.latencyMs > 200) {
|
|
80
|
+
console.warn(`Tauri-Remote-UI : High latency detected - ${exports.latencyMs}ms`);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof data === 'string' && data.startsWith(VERSION_PREFIX)) {
|
|
85
|
+
const serverVersion = data.slice(VERSION_PREFIX.length);
|
|
86
|
+
if (serverVersion !== version.PACKAGE_VERSION) {
|
|
87
|
+
console.warn(`Tauri-Remote-UI : Version mismatch — frontend ` +
|
|
88
|
+
`'tauri-remote-ui' npm package is ${version.PACKAGE_VERSION}, ` +
|
|
89
|
+
`host crate is ${serverVersion}. Behavior is undefined; ` +
|
|
90
|
+
`align both to the same release.`);
|
|
71
91
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let jsonData;
|
|
95
|
+
try {
|
|
96
|
+
jsonData = JSON.parse(data);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.warn('Tauri-Remote-UI : Failed to parse message', err);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (typeof jsonData.id === 'number' && filterCollection[jsonData.id]) {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse((_a = jsonData.payload) !== null && _a !== void 0 ? _a : 'null');
|
|
105
|
+
filterCollection[jsonData.id](parsed);
|
|
75
106
|
}
|
|
76
|
-
|
|
77
|
-
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.warn('Tauri-Remote-UI : Failed to parse RPC payload', err);
|
|
78
109
|
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
}
|
|
111
|
+
else if (jsonData.event) {
|
|
112
|
+
listenEvent.dispatchEvent(new MessageEvent(jsonData.event, { data: jsonData }));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
socket.onclose = () => {
|
|
116
|
+
exports.ws = null;
|
|
117
|
+
exports.wsReady = null;
|
|
118
|
+
if (pingPongTimer !== undefined) {
|
|
119
|
+
clearInterval(pingPongTimer);
|
|
120
|
+
}
|
|
121
|
+
console.info('Tauri-Remote-UI : Remote Disconnected.');
|
|
122
|
+
window.location.href = getDisconnectUrl();
|
|
123
|
+
};
|
|
124
|
+
socket.onerror = (e) => {
|
|
125
|
+
reject(e);
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.error(e);
|
|
95
131
|
}
|
|
96
132
|
}
|
|
97
133
|
|
|
134
|
+
exports.RpcStatus = RpcStatus;
|
|
98
135
|
exports.filterCollection = filterCollection;
|
|
136
|
+
exports.hasTauriRuntime = hasTauriRuntime;
|
|
99
137
|
exports.initWebSocket = initWebSocket;
|
|
100
138
|
exports.listenEvent = listenEvent;
|
package/socket.d.ts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tauri Remote UI -
|
|
2
|
+
* Tauri Remote UI - WebSocket bridge
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Establishes (and re-uses) a WebSocket connection back to the Tauri host
|
|
5
|
+
* application, providing the transport for the `invoke` and `listen` shims
|
|
6
|
+
* exported from `./api/core` and `./api/event`.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Status discriminator on the response payload sent back from the Rust side.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the `RpcStatus` enum in `src/models.rs` — keep both in sync.
|
|
12
|
+
*/
|
|
13
|
+
export declare const RpcStatus: {
|
|
14
|
+
readonly Success: "success";
|
|
15
|
+
readonly Error: "error";
|
|
16
|
+
};
|
|
17
|
+
export type RpcStatus = typeof RpcStatus[keyof typeof RpcStatus];
|
|
18
|
+
/** Shape of the response payload returned for a single RPC call. */
|
|
19
|
+
export interface RpcResponse<T = unknown> {
|
|
20
|
+
status: RpcStatus;
|
|
21
|
+
payload: T;
|
|
22
|
+
}
|
|
23
|
+
/** Callback type stored per outstanding RPC request id. */
|
|
24
|
+
export type RpcResponseHandler = (response: RpcResponse) => void;
|
|
25
|
+
/** Returns true if the page is running inside a Tauri webview. */
|
|
26
|
+
export declare function hasTauriRuntime(): boolean;
|
|
8
27
|
export declare let ws: WebSocket | null;
|
|
9
|
-
export declare
|
|
28
|
+
export declare const listenEvent: EventTarget;
|
|
10
29
|
export declare let wsReady: Promise<void> | null;
|
|
11
|
-
export declare const filterCollection:
|
|
12
|
-
[msg_id: string]: (response: any) => any;
|
|
13
|
-
};
|
|
30
|
+
export declare const filterCollection: Record<number, RpcResponseHandler>;
|
|
14
31
|
export declare let latencyMs: number;
|
|
15
32
|
/**
|
|
16
|
-
* Initialize the WebSocket connection
|
|
17
|
-
*
|
|
33
|
+
* Initialize the WebSocket connection on first use. A no-op when running
|
|
34
|
+
* inside Tauri (native IPC is preferred) or when the socket is already open.
|
|
18
35
|
*/
|
|
19
36
|
export declare function initWebSocket(): void;
|
package/socket.js
CHANGED
|
@@ -1,96 +1,132 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tauri Remote UI - WebSocket bridge
|
|
5
|
+
*
|
|
6
|
+
* Establishes (and re-uses) a WebSocket connection back to the Tauri host
|
|
7
|
+
* application, providing the transport for the `invoke` and `listen` shims
|
|
8
|
+
* exported from `./api/core` and `./api/event`.
|
|
9
|
+
*/
|
|
10
|
+
/** Wire prefix used by the version-handshake exchange. */
|
|
11
|
+
const VERSION_PREFIX = 'version:';
|
|
1
12
|
/**
|
|
2
|
-
*
|
|
13
|
+
* Status discriminator on the response payload sent back from the Rust side.
|
|
3
14
|
*
|
|
4
|
-
*
|
|
5
|
-
* It provides WebSocket initialization and shared WebSocket state for communicating
|
|
6
|
-
* with a Tauri application.
|
|
15
|
+
* Mirrors the `RpcStatus` enum in `src/models.rs` — keep both in sync.
|
|
7
16
|
*/
|
|
17
|
+
const RpcStatus = {
|
|
18
|
+
Success: 'success',
|
|
19
|
+
Error: 'error',
|
|
20
|
+
};
|
|
21
|
+
/** Returns true if the page is running inside a Tauri webview. */
|
|
22
|
+
function hasTauriRuntime() {
|
|
23
|
+
const w = window;
|
|
24
|
+
return Boolean((w.__TAURI_INTERNALS__ && w.__TAURI_INTERNALS__.invoke) ||
|
|
25
|
+
(w.__TAURI__ && w.__TAURI__.invoke));
|
|
26
|
+
}
|
|
8
27
|
let ws = null;
|
|
9
|
-
|
|
28
|
+
const listenEvent = new EventTarget();
|
|
10
29
|
let wsReady = null;
|
|
11
30
|
const filterCollection = {};
|
|
12
31
|
let latencyMs = 0;
|
|
13
|
-
/**
|
|
14
|
-
* Get the WebSocket URL based on the current window location
|
|
15
|
-
*/
|
|
32
|
+
/** Build the WebSocket URL for the RPC connection. */
|
|
16
33
|
function getWsUrl() {
|
|
17
34
|
const loc = window.location;
|
|
18
35
|
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
19
|
-
|
|
20
|
-
return wsUrl;
|
|
36
|
+
return `${proto}//${loc.host}/remote_ui_ws`;
|
|
21
37
|
}
|
|
22
|
-
|
|
38
|
+
/** Build the disconnect-redirect URL the page navigates to on close. */
|
|
39
|
+
function getDisconnectUrl() {
|
|
23
40
|
const loc = window.location;
|
|
24
|
-
|
|
25
|
-
return wsUrl;
|
|
41
|
+
return `${loc.protocol}//${loc.host}/remote_ui_disconnect`;
|
|
26
42
|
}
|
|
27
43
|
/**
|
|
28
|
-
* Initialize the WebSocket connection
|
|
29
|
-
*
|
|
44
|
+
* Initialize the WebSocket connection on first use. A no-op when running
|
|
45
|
+
* inside Tauri (native IPC is preferred) or when the socket is already open.
|
|
30
46
|
*/
|
|
31
47
|
function initWebSocket() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
throw new Error("Moving to WS backup for Tauri Backend");
|
|
40
|
-
}
|
|
48
|
+
if (hasTauriRuntime()) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (ws) {
|
|
52
|
+
return;
|
|
41
53
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
console.info('Tauri-Remote-UI : Remote RPC Attempting...');
|
|
55
|
+
const wsUrl = getWsUrl();
|
|
56
|
+
try {
|
|
57
|
+
let lastPingTimestamp = Date.now();
|
|
58
|
+
let pingPongTimer;
|
|
59
|
+
const socket = new WebSocket(wsUrl);
|
|
60
|
+
ws = socket;
|
|
61
|
+
wsReady = new Promise((resolve, reject) => {
|
|
62
|
+
socket.onopen = () => {
|
|
63
|
+
console.info('Tauri-Remote-UI : Remote Connected.');
|
|
64
|
+
socket.send(`${VERSION_PREFIX}${PACKAGE_VERSION}`);
|
|
65
|
+
lastPingTimestamp = Date.now();
|
|
66
|
+
socket.send('ping');
|
|
67
|
+
pingPongTimer = setInterval(() => {
|
|
54
68
|
lastPingTimestamp = Date.now();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
socket.send('ping');
|
|
70
|
+
}, 10000);
|
|
71
|
+
resolve();
|
|
72
|
+
};
|
|
73
|
+
socket.onmessage = ({ data }) => {
|
|
74
|
+
var _a;
|
|
75
|
+
if (data === 'pong') {
|
|
76
|
+
latencyMs = Date.now() - lastPingTimestamp;
|
|
77
|
+
if (latencyMs > 200) {
|
|
78
|
+
console.warn(`Tauri-Remote-UI : High latency detected - ${latencyMs}ms`);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (typeof data === 'string' && data.startsWith(VERSION_PREFIX)) {
|
|
83
|
+
const serverVersion = data.slice(VERSION_PREFIX.length);
|
|
84
|
+
if (serverVersion !== PACKAGE_VERSION) {
|
|
85
|
+
console.warn(`Tauri-Remote-UI : Version mismatch — frontend ` +
|
|
86
|
+
`'tauri-remote-ui' npm package is ${PACKAGE_VERSION}, ` +
|
|
87
|
+
`host crate is ${serverVersion}. Behavior is undefined; ` +
|
|
88
|
+
`align both to the same release.`);
|
|
69
89
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
let jsonData;
|
|
93
|
+
try {
|
|
94
|
+
jsonData = JSON.parse(data);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.warn('Tauri-Remote-UI : Failed to parse message', err);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typeof jsonData.id === 'number' && filterCollection[jsonData.id]) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse((_a = jsonData.payload) !== null && _a !== void 0 ? _a : 'null');
|
|
103
|
+
filterCollection[jsonData.id](parsed);
|
|
73
104
|
}
|
|
74
|
-
|
|
75
|
-
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.warn('Tauri-Remote-UI : Failed to parse RPC payload', err);
|
|
76
107
|
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
}
|
|
109
|
+
else if (jsonData.event) {
|
|
110
|
+
listenEvent.dispatchEvent(new MessageEvent(jsonData.event, { data: jsonData }));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
socket.onclose = () => {
|
|
114
|
+
ws = null;
|
|
115
|
+
wsReady = null;
|
|
116
|
+
if (pingPongTimer !== undefined) {
|
|
117
|
+
clearInterval(pingPongTimer);
|
|
118
|
+
}
|
|
119
|
+
console.info('Tauri-Remote-UI : Remote Disconnected.');
|
|
120
|
+
window.location.href = getDisconnectUrl();
|
|
121
|
+
};
|
|
122
|
+
socket.onerror = (e) => {
|
|
123
|
+
reject(e);
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
console.error(e);
|
|
93
129
|
}
|
|
94
130
|
}
|
|
95
131
|
|
|
96
|
-
export { filterCollection, initWebSocket, latencyMs, listenEvent, ws, wsReady };
|
|
132
|
+
export { RpcStatus, filterCollection, hasTauriRuntime, initWebSocket, latencyMs, listenEvent, ws, wsReady };
|
package/version.cjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Source-of-truth for the npm package's version, used in the WebSocket
|
|
5
|
+
* handshake to detect frontend/backend version skew.
|
|
6
|
+
*
|
|
7
|
+
* **Keep this in sync with `package.json` `version` and the Rust crate's
|
|
8
|
+
* `Cargo.toml` `version`.** The release flow (`DEVOPS_BUILD=1`) rewrites this
|
|
9
|
+
* value from the latest git tag.
|
|
10
|
+
*/
|
|
11
|
+
const PACKAGE_VERSION = '1.0.1';
|
|
12
|
+
|
|
13
|
+
exports.PACKAGE_VERSION = PACKAGE_VERSION;
|
package/version.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-of-truth for the npm package's version, used in the WebSocket
|
|
3
|
+
* handshake to detect frontend/backend version skew.
|
|
4
|
+
*
|
|
5
|
+
* **Keep this in sync with `package.json` `version` and the Rust crate's
|
|
6
|
+
* `Cargo.toml` `version`.** The release flow (`DEVOPS_BUILD=1`) rewrites this
|
|
7
|
+
* value from the latest git tag.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PACKAGE_VERSION = "1.0.1";
|
package/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-of-truth for the npm package's version, used in the WebSocket
|
|
3
|
+
* handshake to detect frontend/backend version skew.
|
|
4
|
+
*
|
|
5
|
+
* **Keep this in sync with `package.json` `version` and the Rust crate's
|
|
6
|
+
* `Cargo.toml` `version`.** The release flow (`DEVOPS_BUILD=1`) rewrites this
|
|
7
|
+
* value from the latest git tag.
|
|
8
|
+
*/
|
|
9
|
+
const PACKAGE_VERSION = '1.0.1';
|
|
10
|
+
|
|
11
|
+
export { PACKAGE_VERSION };
|