livedesk 0.1.701 → 0.1.702
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/electron/after-pack.cjs +5 -0
- package/electron/electron-builder.linux-x64.yml +11 -2
- package/electron/linux-signed-updater.mjs +107 -0
- package/electron/linux-updater/com.vuvodesk.update.policy +21 -0
- package/electron/linux-updater/release-public-key.json +5 -0
- package/electron/linux-updater/vuvodesk-update-helper +264 -0
- package/electron/update-manager.mjs +32 -8
- package/package.json +1 -1
- package/web/dist/app.html +1 -1
- package/web/dist/app.webmanifest +1 -1
- package/web/dist/assets/{AgentSettingsTab-D0oebRk-.js → AgentSettingsTab-BOUun4gQ.js} +1 -1
- package/web/dist/assets/{AgentsPage-BBj64SpA.js → AgentsPage-Cx7U-mYh.js} +1 -1
- package/web/dist/assets/{CaptureGallery-DF6gZHy0.js → CaptureGallery-DNRbC4Nm.js} +1 -1
- package/web/dist/assets/{HubApp-DJsiVqWP.js → HubApp-CbKj-EB_.js} +3 -3
- package/web/dist/assets/{MonitorStackIcon-BD7Yn-Z9.js → MonitorStackIcon-QQhyrZEW.js} +1 -1
- package/web/dist/assets/{SettingsPage-CXmk161K.js → SettingsPage-ZAduWG1M.js} +1 -1
- package/web/dist/assets/{ShareFilesPage-Dx5yYsbk.js → ShareFilesPage-R8N1yFBD.js} +1 -1
- package/web/dist/assets/{SupportPage-DAsms8Kr.js → SupportPage-B4DfcVAu.js} +1 -1
- package/web/dist/assets/{VuvoDeskApp-9sUnuvyS.js → VuvoDeskApp-CerByFHO.js} +1 -1
- package/web/dist/assets/{app-CUM2wTSb.js → app-CgvQQda5.js} +2 -2
- package/web/dist/assets/{main-BbK4NG31.js → main-Cx9AZE78.js} +2 -2
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +2 -2
- package/web/dist/vuvodesk-build-evidence.json +39 -39
package/electron/after-pack.cjs
CHANGED
|
@@ -106,4 +106,9 @@ module.exports = async function afterPack(context) {
|
|
|
106
106
|
`Packaged ${platform} RemoteFast must be executable before immutable assembly; mode=${executableMode.toString(8)}`
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
|
+
if (platform === 'linux') {
|
|
110
|
+
chmodSync(join(__dirname, 'linux-updater', 'vuvodesk-update-helper'), 0o755);
|
|
111
|
+
chmodSync(join(__dirname, 'linux-updater', 'release-public-key.json'), 0o644);
|
|
112
|
+
chmodSync(join(__dirname, 'linux-updater', 'com.vuvodesk.update.policy'), 0o644);
|
|
113
|
+
}
|
|
109
114
|
};
|
|
@@ -43,8 +43,8 @@ deb:
|
|
|
43
43
|
packageCategory: utils
|
|
44
44
|
description: >-
|
|
45
45
|
View your computers together and control a selected screen with VuvoDesk.
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
After the first administrator-approved installation, verified VuvoDesk
|
|
47
|
+
updates install automatically without another password prompt.
|
|
48
48
|
fpm:
|
|
49
49
|
- --replaces
|
|
50
50
|
- livedesk
|
|
@@ -52,7 +52,16 @@ deb:
|
|
|
52
52
|
- livedesk
|
|
53
53
|
- --provides
|
|
54
54
|
- livedesk
|
|
55
|
+
- --depends
|
|
56
|
+
- python3
|
|
57
|
+
- --depends
|
|
58
|
+
- openssl
|
|
59
|
+
- --depends
|
|
60
|
+
- pkexec | policykit-1
|
|
55
61
|
- packages/vuvodesk/electron/vuvodesk.metainfo.xml=/usr/share/metainfo/com.livedesk.desktop.metainfo.xml
|
|
62
|
+
- packages/vuvodesk/electron/linux-updater/vuvodesk-update-helper=/usr/lib/vuvodesk-updater/vuvodesk-update-helper
|
|
63
|
+
- packages/vuvodesk/electron/linux-updater/release-public-key.json=/usr/lib/vuvodesk-updater/release-public-key.json
|
|
64
|
+
- packages/vuvodesk/electron/linux-updater/com.vuvodesk.update.policy=/usr/share/polkit-1/actions/com.vuvodesk.update.policy
|
|
56
65
|
extraMetadata:
|
|
57
66
|
homepage: https://vuvodesk.com
|
|
58
67
|
publish:
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
|
|
7
|
+
const execute = promisify(execFile);
|
|
8
|
+
const HELPER = '/usr/lib/vuvodesk-updater/vuvodesk-update-helper';
|
|
9
|
+
const VERSION = /^(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})$/;
|
|
10
|
+
const FEED = /^https:\/\/livedesk-desktop-updates\.lovecrdm\.workers\.dev\/(dev|stable)\/linux\/x64\/?$/;
|
|
11
|
+
|
|
12
|
+
async function runHelper(args) {
|
|
13
|
+
let output;
|
|
14
|
+
try {
|
|
15
|
+
output = await execute('/usr/bin/pkexec', ['--disable-internal-agent', HELPER, ...args], {
|
|
16
|
+
encoding: 'utf8', maxBuffer: 8192, timeout: args[0] === 'install' ? 340_000 : 130_000,
|
|
17
|
+
windowsHide: true, shell: false
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
let code = 'linux-update-helper-unavailable';
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(error.stdout);
|
|
23
|
+
if (/^linux-update-[a-z-]+$/.test(parsed.error)) code = parsed.error;
|
|
24
|
+
} catch { /* Never forward paths, command lines, or arbitrary helper output. */ }
|
|
25
|
+
throw new Error(code);
|
|
26
|
+
}
|
|
27
|
+
const result = JSON.parse(output.stdout);
|
|
28
|
+
if (result.state === 'error') throw new Error('linux-update-helper-failed');
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readEnvelope(url) {
|
|
33
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15_000), redirect: 'error', cache: 'no-store' });
|
|
34
|
+
if (!response.ok || Number(response.headers.get('content-length') || 0) > 8192) {
|
|
35
|
+
await response.body?.cancel();
|
|
36
|
+
throw new Error('linux-update-signature-unavailable');
|
|
37
|
+
}
|
|
38
|
+
const reader = response.body.getReader();
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let size = 0;
|
|
41
|
+
try {
|
|
42
|
+
for (;;) {
|
|
43
|
+
const { done, value } = await reader.read();
|
|
44
|
+
if (done) break;
|
|
45
|
+
size += value.byteLength;
|
|
46
|
+
if (size > 8192) throw new Error('linux-update-signature-too-large');
|
|
47
|
+
chunks.push(value);
|
|
48
|
+
}
|
|
49
|
+
return Buffer.concat(chunks, size);
|
|
50
|
+
} finally {
|
|
51
|
+
await reader.cancel().catch(() => undefined);
|
|
52
|
+
reader.releaseLock();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Injection is confined to this unprivileged adapter. The installed root helper
|
|
57
|
+
// has no test flag, environment-selected key, URL, command, or staging root.
|
|
58
|
+
export function signedDebUpdaterClass(DebUpdater, { helper = runHelper, envelope = readEnvelope,
|
|
59
|
+
noteQuit = () => undefined } = {}) {
|
|
60
|
+
return class SignedDebUpdater extends DebUpdater {
|
|
61
|
+
preparedRelease = null;
|
|
62
|
+
// Install only through the manager's exact runtime-drain owner. The base
|
|
63
|
+
// quit handler would invoke synchronous dpkg outside that lifecycle.
|
|
64
|
+
addQuitHandler() {}
|
|
65
|
+
|
|
66
|
+
async prepareVerifiedInstall({ version, feedUrl }) {
|
|
67
|
+
if (!VERSION.test(version) || !FEED.test(feedUrl)) throw new Error('linux-update-target-invalid');
|
|
68
|
+
if (this.preparedRelease) throw new Error('linux-update-already-prepared');
|
|
69
|
+
const channel = feedUrl.match(FEED)[1];
|
|
70
|
+
const file = `VuvoDesk-${version}${channel === 'dev' ? '-dev' : ''}-amd64.deb`;
|
|
71
|
+
const installer = this.installerPath;
|
|
72
|
+
if (!installer || basename(installer) !== file) throw new Error('linux-update-installer-mismatch');
|
|
73
|
+
const directory = await mkdtemp(join(tmpdir(), 'vuvodesk-update-signature-'));
|
|
74
|
+
try {
|
|
75
|
+
const documentPath = join(directory, 'release.json');
|
|
76
|
+
await writeFile(documentPath, await envelope(`${feedUrl.replace(/\/$/, '')}/${file}.update.json`), { mode: 0o600, flag: 'wx' });
|
|
77
|
+
const prepared = await helper(['prepare', version, installer, documentPath]);
|
|
78
|
+
if (prepared.state !== 'prepared' || prepared.version !== version || !/^[a-f0-9]{32}$/.test(prepared.token)) {
|
|
79
|
+
throw new Error('linux-update-prepared-proof-invalid');
|
|
80
|
+
}
|
|
81
|
+
this.preparedRelease = { token: prepared.token, version, installer };
|
|
82
|
+
} finally {
|
|
83
|
+
await rm(directory, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async cancelVerifiedInstall() {
|
|
88
|
+
const prepared = this.preparedRelease;
|
|
89
|
+
this.preparedRelease = null;
|
|
90
|
+
if (prepared) await helper(['cancel', prepared.token]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async quitAndInstall(_silent = true, forceRunAfter = true) {
|
|
94
|
+
const prepared = this.preparedRelease;
|
|
95
|
+
if (!prepared || prepared.installer !== this.installerPath) throw new Error('linux-update-prepared-owner-missing');
|
|
96
|
+
// Clear before awaiting so no second caller can install the same owner.
|
|
97
|
+
this.preparedRelease = null;
|
|
98
|
+
const installed = await helper(['install', prepared.token]);
|
|
99
|
+
if (installed.state !== 'installed' || installed.version !== prepared.version) {
|
|
100
|
+
throw new Error('linux-update-installed-proof-invalid');
|
|
101
|
+
}
|
|
102
|
+
if (forceRunAfter) this.app.relaunch();
|
|
103
|
+
noteQuit();
|
|
104
|
+
this.app.quit();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD polkit Policy Configuration 1.0//EN"
|
|
3
|
+
"http://www.freedesktop.org/software/polkit/policyconfig-1.dtd">
|
|
4
|
+
<policyconfig>
|
|
5
|
+
<vendor>VuvoDesk</vendor>
|
|
6
|
+
<vendor_url>https://vuvodesk.com</vendor_url>
|
|
7
|
+
<icon_name>livedesk</icon_name>
|
|
8
|
+
<action id="com.vuvodesk.update.install-signed-release">
|
|
9
|
+
<description>Install a verified VuvoDesk update</description>
|
|
10
|
+
<message>Install a newer VuvoDesk release signed by its publisher</message>
|
|
11
|
+
<!-- This exact root-owned helper accepts only signed, newer VuvoDesk debs.
|
|
12
|
+
It cannot run a command, URL, installer, or script chosen by the caller.
|
|
13
|
+
Remote and inactive sessions need updates too; no general sudo rule. -->
|
|
14
|
+
<defaults>
|
|
15
|
+
<allow_any>yes</allow_any>
|
|
16
|
+
<allow_inactive>yes</allow_inactive>
|
|
17
|
+
<allow_active>yes</allow_active>
|
|
18
|
+
</defaults>
|
|
19
|
+
<annotate key="org.freedesktop.policykit.exec.path">/usr/lib/vuvodesk-updater/vuvodesk-update-helper</annotate>
|
|
20
|
+
</action>
|
|
21
|
+
</policyconfig>
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/python3 -I
|
|
2
|
+
"""Install only signed, newer VuvoDesk debs. No caller-selected command or URL."""
|
|
3
|
+
import base64
|
|
4
|
+
import fcntl
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
import resource
|
|
11
|
+
import secrets
|
|
12
|
+
import signal
|
|
13
|
+
import shutil
|
|
14
|
+
import stat
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
BASE = Path('/var/lib/vuvodesk-updater')
|
|
21
|
+
KEY = Path('/usr/lib/vuvodesk-updater/release-public-key.json')
|
|
22
|
+
FEED = Path('/opt/VuvoDesk/resources/app-update.yml')
|
|
23
|
+
MAX_PACKAGE = 1024 * 1024 * 1024
|
|
24
|
+
MAX_DOCUMENT = 8192
|
|
25
|
+
PREPARED_TTL = 600
|
|
26
|
+
VERSION = re.compile(r'(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})\Z')
|
|
27
|
+
SAFE_ENV = {'PATH': '/usr/sbin:/usr/bin:/sbin:/bin', 'HOME': '/root',
|
|
28
|
+
'LANG': 'C', 'LC_ALL': 'C', 'DEBIAN_FRONTEND': 'noninteractive'}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def require(condition, reason):
|
|
32
|
+
if not condition:
|
|
33
|
+
raise ValueError('linux-update-' + reason)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def version_tuple(value):
|
|
37
|
+
require(isinstance(value, str) and VERSION.fullmatch(value), 'version-invalid')
|
|
38
|
+
return tuple(map(int, value.split('.')))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def safe_owned_file(path, uid, limit):
|
|
42
|
+
require(os.path.isabs(path), 'path-invalid')
|
|
43
|
+
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
44
|
+
try:
|
|
45
|
+
info = os.fstat(fd)
|
|
46
|
+
require(stat.S_ISREG(info.st_mode) and info.st_uid == uid, 'file-owner-invalid')
|
|
47
|
+
require(uid != 0 or info.st_mode & 0o022 == 0, 'trusted-file-writable')
|
|
48
|
+
require(0 < info.st_size <= limit and info.st_nlink == 1, 'file-size-or-links-invalid')
|
|
49
|
+
return os.fdopen(fd, 'rb'), info.st_size
|
|
50
|
+
except BaseException:
|
|
51
|
+
os.close(fd)
|
|
52
|
+
raise
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_document(path, uid=0):
|
|
56
|
+
source, size = safe_owned_file(str(path), uid, MAX_DOCUMENT)
|
|
57
|
+
with source:
|
|
58
|
+
data = source.read(MAX_DOCUMENT + 1)
|
|
59
|
+
require(len(data) == size, 'document-changed')
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_checked(args, timeout=15):
|
|
64
|
+
# Fixed system tools only; never execute a caller path or preserve its env.
|
|
65
|
+
capture = args[0] != '/usr/bin/dpkg'
|
|
66
|
+
def bound_query_output():
|
|
67
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (MAX_DOCUMENT, MAX_DOCUMENT))
|
|
68
|
+
with tempfile.TemporaryFile() as output_file:
|
|
69
|
+
child = subprocess.Popen(args, stdin=subprocess.DEVNULL,
|
|
70
|
+
stdout=output_file if capture else subprocess.DEVNULL,
|
|
71
|
+
stderr=subprocess.DEVNULL, env=SAFE_ENV, cwd='/', start_new_session=True,
|
|
72
|
+
preexec_fn=bound_query_output if capture else None)
|
|
73
|
+
try:
|
|
74
|
+
child.wait(timeout=timeout)
|
|
75
|
+
except BaseException:
|
|
76
|
+
# Even a timed-out maintainer script must not survive its exact owner.
|
|
77
|
+
try:
|
|
78
|
+
os.killpg(child.pid, signal.SIGKILL)
|
|
79
|
+
except ProcessLookupError:
|
|
80
|
+
pass
|
|
81
|
+
child.wait()
|
|
82
|
+
raise
|
|
83
|
+
require(child.returncode == 0, 'system-check-failed')
|
|
84
|
+
output_file.seek(0)
|
|
85
|
+
output = output_file.read(MAX_DOCUMENT + 1)
|
|
86
|
+
# A writer may accept a short write at RLIMIT_FSIZE and exit zero.
|
|
87
|
+
# Reaching the cap is therefore unconfirmed/truncated output, not proof.
|
|
88
|
+
require(len(output) < MAX_DOCUMENT, 'system-output-too-large')
|
|
89
|
+
return output.decode('utf8').strip()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def installed_version():
|
|
93
|
+
record = run_checked(['/usr/bin/dpkg-query', '-W', '-f=${Status}\n${Version}', 'vuvodesk'])
|
|
94
|
+
status, version = record.split('\n')
|
|
95
|
+
require(status == 'install ok installed', 'package-state-invalid')
|
|
96
|
+
version_tuple(version)
|
|
97
|
+
return version
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def installed_channel():
|
|
101
|
+
raw = read_document(FEED).decode('utf8')
|
|
102
|
+
matches = re.findall(r'^url: https://livedesk-desktop-updates\.lovecrdm\.workers\.dev/(dev|stable)/linux/x64\s*$', raw, re.M)
|
|
103
|
+
require(len(matches) == 1, 'installed-feed-invalid')
|
|
104
|
+
return matches[0]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def verify_envelope(raw, scratch, channel, expected_version):
|
|
108
|
+
require(len(raw) <= MAX_DOCUMENT, 'document-too-large')
|
|
109
|
+
envelope = json.loads(raw)
|
|
110
|
+
require(set(envelope) == {'payload', 'signature'}, 'envelope-invalid')
|
|
111
|
+
payload = base64.b64decode(envelope['payload'], validate=True)
|
|
112
|
+
signature = base64.b64decode(envelope['signature'], validate=True)
|
|
113
|
+
require(len(signature) == 64 and 0 < len(payload) <= 4096, 'signature-invalid')
|
|
114
|
+
trust = json.loads(read_document(KEY))
|
|
115
|
+
require(trust['algorithm'] == 'Ed25519' and trust['schema'] == 1, 'trust-invalid')
|
|
116
|
+
(scratch / 'public.pem').write_text(trust['publicKey'], encoding='ascii')
|
|
117
|
+
(scratch / 'payload').write_bytes(payload)
|
|
118
|
+
(scratch / 'signature').write_bytes(signature)
|
|
119
|
+
run_checked(['/usr/bin/openssl', 'pkeyutl', '-verify', '-pubin', '-rawin',
|
|
120
|
+
'-inkey', str(scratch / 'public.pem'), '-in', str(scratch / 'payload'),
|
|
121
|
+
'-sigfile', str(scratch / 'signature')])
|
|
122
|
+
document = json.loads(payload)
|
|
123
|
+
require(set(document) == {'schema', 'package', 'version', 'arch', 'channel', 'file', 'size', 'sha256'}, 'payload-invalid')
|
|
124
|
+
require(document['schema'] == 1 and document['package'] == 'vuvodesk'
|
|
125
|
+
and document['arch'] == 'amd64' and document['channel'] == channel, 'release-scope-invalid')
|
|
126
|
+
version_tuple(document['version'])
|
|
127
|
+
require(document['version'] == expected_version, 'target-mismatch')
|
|
128
|
+
suffix = '-dev' if channel == 'dev' else ''
|
|
129
|
+
require(document['file'] == f'VuvoDesk-{expected_version}{suffix}-amd64.deb', 'filename-invalid')
|
|
130
|
+
require(type(document['size']) is int and 0 < document['size'] <= MAX_PACKAGE, 'size-invalid')
|
|
131
|
+
require(isinstance(document['sha256'], str) and re.fullmatch('[a-f0-9]{64}', document['sha256']), 'hash-invalid')
|
|
132
|
+
return document
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def verify_package(path, document):
|
|
136
|
+
digest = hashlib.sha256()
|
|
137
|
+
with path.open('rb') as source:
|
|
138
|
+
for chunk in iter(lambda: source.read(1024 * 1024), b''):
|
|
139
|
+
digest.update(chunk)
|
|
140
|
+
require(path.stat().st_size == document['size'] and digest.hexdigest() == document['sha256'], 'package-hash-mismatch')
|
|
141
|
+
for field, expected in [('Package', 'vuvodesk'), ('Version', document['version']), ('Architecture', 'amd64')]:
|
|
142
|
+
require(run_checked(['/usr/bin/dpkg-deb', '-f', str(path), field]) == expected, 'package-metadata-mismatch')
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def prepare(uid, version, package_path, envelope_path):
|
|
146
|
+
version_tuple(version)
|
|
147
|
+
previous = installed_version()
|
|
148
|
+
require(version_tuple(version) > version_tuple(previous), 'not-newer')
|
|
149
|
+
channel = installed_channel()
|
|
150
|
+
pending = BASE / 'pending'
|
|
151
|
+
if pending.exists():
|
|
152
|
+
require(not pending.is_symlink() and pending.is_dir(), 'staging-invalid')
|
|
153
|
+
state = json.loads(read_document(pending / 'state.json'))
|
|
154
|
+
require(time.time() - state['created'] > PREPARED_TTL, 'busy')
|
|
155
|
+
shutil.rmtree(pending)
|
|
156
|
+
# A failed request never leaves a prepared owner or an unbounded cache.
|
|
157
|
+
scratch = BASE / 'work'
|
|
158
|
+
if scratch.exists():
|
|
159
|
+
require(not scratch.is_symlink() and scratch.is_dir(), 'staging-invalid')
|
|
160
|
+
shutil.rmtree(scratch)
|
|
161
|
+
scratch.mkdir(mode=0o700)
|
|
162
|
+
try:
|
|
163
|
+
raw = read_document(envelope_path, uid)
|
|
164
|
+
document = verify_envelope(raw, scratch, channel, version)
|
|
165
|
+
source, size = safe_owned_file(package_path, uid, MAX_PACKAGE)
|
|
166
|
+
with source:
|
|
167
|
+
require(size == document['size'], 'package-size-mismatch')
|
|
168
|
+
require(shutil.disk_usage(BASE).free > size + 256 * 1024 * 1024, 'disk-space-low')
|
|
169
|
+
total = 0
|
|
170
|
+
with (scratch / 'package.deb').open('xb') as destination:
|
|
171
|
+
while True:
|
|
172
|
+
chunk = source.read(min(1024 * 1024, size - total + 1))
|
|
173
|
+
if not chunk:
|
|
174
|
+
break
|
|
175
|
+
total += len(chunk)
|
|
176
|
+
require(total <= size, 'package-changed')
|
|
177
|
+
destination.write(chunk)
|
|
178
|
+
destination.flush()
|
|
179
|
+
os.fsync(destination.fileno())
|
|
180
|
+
require(total == size, 'package-changed')
|
|
181
|
+
verify_package(scratch / 'package.deb', document)
|
|
182
|
+
# Only this signed package is inspected; do not invoke apt repair or scripts here.
|
|
183
|
+
run_checked(['/usr/bin/dpkg', '--no-act', '--install', str(scratch / 'package.deb')], 30)
|
|
184
|
+
token = secrets.token_hex(16)
|
|
185
|
+
state = {'token': token, 'uid': uid, 'created': time.time(), 'previous': previous,
|
|
186
|
+
'document': document}
|
|
187
|
+
(scratch / 'state.json').write_text(json.dumps(state), encoding='utf8')
|
|
188
|
+
os.rename(scratch, pending)
|
|
189
|
+
finally:
|
|
190
|
+
if scratch.exists():
|
|
191
|
+
shutil.rmtree(scratch)
|
|
192
|
+
return {'state': 'prepared', 'token': token, 'version': version}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def apply_prepared(uid, token):
|
|
196
|
+
require(re.fullmatch('[a-f0-9]{32}', token), 'token-invalid')
|
|
197
|
+
pending = BASE / 'pending'
|
|
198
|
+
state = json.loads(read_document(pending / 'state.json'))
|
|
199
|
+
require(state['uid'] == uid and secrets.compare_digest(state['token'], token), 'owner-mismatch')
|
|
200
|
+
require(0 <= time.time() - state['created'] <= PREPARED_TTL, 'prepared-expired')
|
|
201
|
+
document = state['document']
|
|
202
|
+
require(installed_channel() == document['channel'], 'channel-changed')
|
|
203
|
+
require(installed_version() == state['previous'], 'installed-version-changed')
|
|
204
|
+
verify_package(pending / 'package.deb', document)
|
|
205
|
+
# Lock ownership stays with this process throughout dpkg. No restart command,
|
|
206
|
+
# shell, arbitrary args, dependency repair, downgrade, or auth data access.
|
|
207
|
+
try:
|
|
208
|
+
run_checked(['/usr/bin/dpkg', '--force-confdef', '--force-confold', '--install',
|
|
209
|
+
str(pending / 'package.deb')], 300)
|
|
210
|
+
require(installed_version() == document['version'], 'installed-proof-missing')
|
|
211
|
+
finally:
|
|
212
|
+
shutil.rmtree(pending)
|
|
213
|
+
return {'state': 'installed', 'version': document['version']}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def cancel(uid, token):
|
|
217
|
+
require(re.fullmatch('[a-f0-9]{32}', token), 'token-invalid')
|
|
218
|
+
pending = BASE / 'pending'
|
|
219
|
+
if pending.exists():
|
|
220
|
+
state = json.loads(read_document(pending / 'state.json'))
|
|
221
|
+
require(state['uid'] == uid and secrets.compare_digest(state['token'], token), 'owner-mismatch')
|
|
222
|
+
shutil.rmtree(pending)
|
|
223
|
+
return {'state': 'cancelled'}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
require(os.geteuid() == 0, 'administrator-helper-required')
|
|
228
|
+
uid = int(os.environ.get('PKEXEC_UID', '-1'))
|
|
229
|
+
require(uid > 0, 'caller-invalid')
|
|
230
|
+
os.environ.clear()
|
|
231
|
+
os.environ.update(SAFE_ENV)
|
|
232
|
+
os.umask(0o077)
|
|
233
|
+
os.chdir('/')
|
|
234
|
+
require(len(sys.argv) in (3, 5), 'arguments-invalid')
|
|
235
|
+
action = sys.argv[1]
|
|
236
|
+
require((action == 'prepare' and len(sys.argv) == 5)
|
|
237
|
+
or (action in ('install', 'cancel') and len(sys.argv) == 3), 'arguments-invalid')
|
|
238
|
+
BASE.mkdir(mode=0o700, exist_ok=True)
|
|
239
|
+
info = BASE.lstat()
|
|
240
|
+
require(stat.S_ISDIR(info.st_mode) and info.st_uid == 0 and info.st_mode & 0o077 == 0, 'staging-owner-invalid')
|
|
241
|
+
fd = os.open(BASE / 'lock', os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
|
|
242
|
+
with os.fdopen(fd, 'w') as lock:
|
|
243
|
+
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
244
|
+
def deadline(_signal, _frame):
|
|
245
|
+
raise ValueError('linux-update-deadline')
|
|
246
|
+
signal.signal(signal.SIGALRM, deadline)
|
|
247
|
+
signal.alarm(325 if action == 'install' else 120)
|
|
248
|
+
if action == 'prepare':
|
|
249
|
+
result = prepare(uid, *sys.argv[2:])
|
|
250
|
+
elif action == 'install':
|
|
251
|
+
result = apply_prepared(uid, sys.argv[2])
|
|
252
|
+
else:
|
|
253
|
+
result = cancel(uid, sys.argv[2])
|
|
254
|
+
print(json.dumps(result), flush=True)
|
|
255
|
+
signal.alarm(0)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == '__main__':
|
|
259
|
+
try:
|
|
260
|
+
main()
|
|
261
|
+
except BaseException as error:
|
|
262
|
+
code = str(error) if isinstance(error, ValueError) and str(error).startswith('linux-update-') else 'linux-update-helper-failed'
|
|
263
|
+
print(json.dumps({'state': 'error', 'error': code}), flush=True)
|
|
264
|
+
sys.exit(1)
|
|
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import { resolveDesktopUpdateFeed } from './update-feed-resolver.mjs';
|
|
6
|
+
import { signedDebUpdaterClass } from './linux-signed-updater.mjs';
|
|
6
7
|
import {
|
|
7
8
|
installBoundedMultipartRangeBatching,
|
|
8
9
|
markUpdaterWithBoundedMultipartRangeBatching,
|
|
@@ -36,14 +37,20 @@ export function createPlatformProductUpdater(platform = process.platform) {
|
|
|
36
37
|
return updater;
|
|
37
38
|
}
|
|
38
39
|
if (platform === 'darwin') return new module.MacUpdater();
|
|
40
|
+
let packageType = '';
|
|
39
41
|
try {
|
|
40
|
-
|
|
41
|
-
if (packageType === 'deb') return new module.DebUpdater();
|
|
42
|
-
if (packageType === 'rpm') return new module.RpmUpdater();
|
|
43
|
-
if (packageType === 'pacman') return new module.PacmanUpdater();
|
|
42
|
+
packageType = String(readFileSync(join(process.resourcesPath, 'package-type'), 'utf8')).trim();
|
|
44
43
|
} catch {
|
|
45
44
|
// AppImage is the Linux fallback used by electron-updater itself.
|
|
46
45
|
}
|
|
46
|
+
if (packageType === 'deb') {
|
|
47
|
+
const SignedDebUpdater = signedDebUpdaterClass(module.DebUpdater, {
|
|
48
|
+
noteQuit: () => require('electron').autoUpdater.emit('before-quit-for-update')
|
|
49
|
+
});
|
|
50
|
+
return new SignedDebUpdater();
|
|
51
|
+
}
|
|
52
|
+
if (packageType === 'rpm') return new module.RpmUpdater();
|
|
53
|
+
if (packageType === 'pacman') return new module.PacmanUpdater();
|
|
47
54
|
return new module.AppImageUpdater();
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -883,6 +890,7 @@ export function createProductUpdateManager({
|
|
|
883
890
|
}
|
|
884
891
|
installOperationPending = true;
|
|
885
892
|
const operation = (async () => {
|
|
893
|
+
let drainStarted = false;
|
|
886
894
|
try {
|
|
887
895
|
if (status.downloaded
|
|
888
896
|
&& (!status.downloadedVersion || status.downloadedVersion !== status.availableVersion)) {
|
|
@@ -912,22 +920,38 @@ export function createProductUpdateManager({
|
|
|
912
920
|
automaticInstallFailure = null;
|
|
913
921
|
updater.autoInstallOnAppQuit = true;
|
|
914
922
|
}
|
|
923
|
+
// Reject an unsigned/wrong-owner Linux package while the current
|
|
924
|
+
// runtime is still healthy. Only a root-owned verified copy may
|
|
925
|
+
// cross the subsequent drain/install boundary.
|
|
926
|
+
await updater.prepareVerifiedInstall?.({ version: installTargetVersion, feedUrl: feed.url });
|
|
915
927
|
// The updater may launch its installer synchronously from
|
|
916
928
|
// quitAndInstall. Prove the exact runtime descendant tree is already
|
|
917
|
-
// zero before allowing that replacement process to exist.
|
|
918
|
-
|
|
929
|
+
// zero before allowing that replacement process to exist.
|
|
930
|
+
drainStarted = true;
|
|
931
|
+
await beforeInstall();
|
|
919
932
|
installRuntimePrepared = true;
|
|
920
933
|
publish({ state: 'installing', error: '', errorCode: '' });
|
|
921
934
|
// Long-running desktop Clients must not stop at the assisted NSIS
|
|
922
935
|
// wizard after the runtime has already shut down for replacement.
|
|
923
|
-
updater.quitAndInstall(true, true);
|
|
936
|
+
await updater.quitAndInstall(true, true);
|
|
924
937
|
// BaseUpdater can emit a synchronous error and return instead of
|
|
925
938
|
// throwing. Keep this install owner locked until runtime recovery
|
|
926
939
|
// completes, so Retry cannot begin a second pre-install drain.
|
|
927
940
|
if (installRecoveryPromise) await installRecoveryPromise;
|
|
928
941
|
return snapshot();
|
|
929
942
|
} catch (error) {
|
|
930
|
-
|
|
943
|
+
try {
|
|
944
|
+
await updater.cancelVerifiedInstall?.();
|
|
945
|
+
} catch {
|
|
946
|
+
publish({ preparedCleanupUnconfirmed: true });
|
|
947
|
+
}
|
|
948
|
+
// A signature fetch or package-manager busy check before drain is
|
|
949
|
+
// safe to retry on the existing recurring cadence. Only a started
|
|
950
|
+
// drain/installer blocks repeated automatic shutdown of this target.
|
|
951
|
+
const failure = drainStarted ? rememberInstallerFailure(error) : {
|
|
952
|
+
state: 'error', error: String(error?.message || error),
|
|
953
|
+
errorCode: 'desktop-update-preparation-failed'
|
|
954
|
+
};
|
|
931
955
|
await recoverPreparedRuntime(error);
|
|
932
956
|
if (!status.reopenRequired) {
|
|
933
957
|
const message = String(error?.message || error);
|
package/package.json
CHANGED
package/web/dist/app.html
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
.vuvodesk-static-boot [hidden] { display: none; }
|
|
53
53
|
@keyframes vuvodesk-static-boot-spin { to { transform: rotate(360deg); } }
|
|
54
54
|
</style>
|
|
55
|
-
<script type="module" crossorigin src="/assets/app-
|
|
55
|
+
<script type="module" crossorigin src="/assets/app-CgvQQda5.js" data-vuvodesk-entry></script>
|
|
56
56
|
<link rel="modulepreload" crossorigin href="/assets/public-site-routing-pddZbLCj.js">
|
|
57
57
|
<link rel="modulepreload" crossorigin href="/assets/pwa-bootstrap-ljOQTt2e.js">
|
|
58
58
|
</head>
|
package/web/dist/app.webmanifest
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as t}from"./pwa-startup-C8kDd3Vd.js";import{r as c,d as A,S as q,T as I,e as z}from"./icons-BwhnWKHR.js";import{h as x}from"./MonitorStackIcon-
|
|
1
|
+
import{j as t}from"./pwa-startup-C8kDd3Vd.js";import{r as c,d as A,S as q,T as I,e as z}from"./icons-BwhnWKHR.js";import{h as x}from"./MonitorStackIcon-QQhyrZEW.js";import"./pwa-bootstrap-ljOQTt2e.js";const E={enabled:!0};function w(i){return{enabled:i?.enabled===!0}}async function L(i,p,f){let l=null;for(let s=0;s<2;s+=1){const g=await x("/api/settings",{signal:f},p);if(g.settings.agent?.enabled===i)return;try{await x("/api/settings",{method:"PATCH",signal:f,body:JSON.stringify({revision:g.revision,agent:{enabled:i}})},p);return}catch(a){if(l=a,s!==0||!String(a instanceof Error?a.message:a).startsWith("409 "))throw a}}throw l}function H({hubUrl:i,showToast:p}){const[f,l]=c.useState(E),[s,g]=c.useState(null),[a,m]=c.useState(""),[j,v]=c.useState(""),[y,d]=c.useState(""),b=c.useRef(0),u=c.useRef(null);c.useEffect(()=>{let e=!0;u.current?.abort();const n=new AbortController;u.current=n;const h=++b.current;return g(null),l(E),m(""),d(""),v(""),x("/api/settings/agent",{signal:n.signal},i).then(o=>{!e||n.signal.aborted||h!==b.current||(g(o.settings),l(w(o.settings)))}).catch(o=>{e&&!n.signal.aborted&&h===b.current&&d(o instanceof Error?o.message:String(o))}),()=>{e=!1,n.abort(),u.current===n&&(u.current=null)}},[i]);const k=(e,n)=>l(h=>({...h,[e]:n})),C=e=>{g(e),l(w(e))},S=()=>({generation:b.current,controller:u.current}),r=e=>e.generation===b.current&&e.controller!==null&&e.controller===u.current&&!e.controller.signal.aborted,R=async()=>{if(!s){d("Agent settings are not available yet.");return}const e=S();if(r(e)){m("save"),d(""),v("");try{if(await L(f.enabled,i,e.controller.signal),!r(e))return;const n=await x("/api/settings/agent",{signal:e.controller.signal},i);if(!r(e))return;C(n.settings),v("Codex Agent settings saved.")}catch(n){r(e)&&d(n instanceof Error?n.message:String(n))}finally{r(e)&&m("")}}},T=async()=>{const e=S();if(r(e)){m("test"),d(""),v("");try{const n=await x("/api/settings/agent/test",{method:"POST",signal:e.controller.signal,body:JSON.stringify({})},i);if(!r(e))return;const h=await x("/api/settings/agent",{signal:e.controller.signal},i);if(!r(e))return;C(h.settings);const o=Number.isFinite(n.connection?.latencyMs)?Math.max(0,Math.round(n.connection.latencyMs)):null;p(`Codex Agent connection verified${o!==null?` in ${o} ms.`:"."}`,"success")}catch(n){r(e)&&d(n instanceof Error?n.message:String(n))}finally{r(e)&&m("")}}},D=s?.codexInstallation==="installed"&&s.codexAuth==="signed-in",N=s!==null;return t.jsxs("div",{className:"settings-tab-content agent-settings-content",children:[t.jsxs("section",{className:"settings-section-card agent-enable-card",children:[t.jsxs("label",{className:"settings-toggle agent-enable-toggle",children:[t.jsx("input",{type:"checkbox",checked:f.enabled,onChange:e=>k("enabled",e.target.checked),disabled:!N||a!==""}),t.jsx("span",{children:"Enable Codex Agent"})]}),t.jsxs("p",{className:"settings-help",children:[t.jsx(A,{size:12})," Enabled by default. Agent commands use only the Codex account signed in on this Hub."]})]}),y&&t.jsx("div",{className:"agent-settings-alert error",role:"alert",children:y}),j&&t.jsx("div",{className:"agent-settings-alert success",role:"status",children:j}),t.jsxs("section",{className:"settings-section-card",children:[t.jsxs("div",{className:"settings-section-heading",children:[t.jsxs("div",{children:[t.jsx("span",{children:"Connection"}),t.jsx("h3",{children:"Codex SDK / CLI"})]}),t.jsx(q,{size:20})]}),t.jsxs("div",{className:"agent-connection-row",children:[t.jsx("span",{className:`agent-connection-dot ${D?"connected":s?.codexAuth==="not-signed-in"?"unavailable":""}`}),t.jsx("span",{children:s?.codexInstallation==="not-installed"?"Codex SDK not installed":s?.codexAuth==="signed-in"?"Codex CLI signed in":s?.codexAuth==="not-signed-in"?"Codex CLI sign-in required":"Codex status not tested"}),t.jsxs("button",{className:"settings-reset",onClick:()=>{T()},disabled:a!=="",type:"button",children:[t.jsx(I,{size:15})," ",a==="test"?"Testing":"Test connection"]})]}),t.jsxs("p",{className:"settings-help",children:[t.jsx(A,{size:12})," The Hub uses the installed Codex SDK / CLI to plan approved commands and run them through VuvoDesk Agent tools on selected computers. VuvoDesk never asks for separate credentials."]})]}),t.jsx("div",{className:"settings-action-row agent-settings-actions",children:t.jsxs("button",{className:"text-button primary",onClick:()=>{R()},disabled:!N||a!=="",type:"button",children:[t.jsx(z,{size:15})," ",a==="save"?"Saving":"Save Agent settings"]})})]})}export{H as AgentSettingsTab};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{j as t}from"./pwa-startup-C8kDd3Vd.js";import{r as d,ae as ht,af as Ce,a3 as yt,y as ee,X as xe,ag as vt,ah as wt,ai as Ze,aj as kt,ak as bt,al as Se,b as H,am as Ct,O as xt,an as _e,m as St,ao as jt,S as Je,ap as At,N as Rt,c as Nt,aq as Pt,ar as It,A as Tt,as as ue,M as Xe}from"./icons-BwhnWKHR.js";import{r as Et,h as F}from"./MonitorStackIcon-
|
|
1
|
+
import{j as t}from"./pwa-startup-C8kDd3Vd.js";import{r as d,ae as ht,af as Ce,a3 as yt,y as ee,X as xe,ag as vt,ah as wt,ai as Ze,aj as kt,ak as bt,al as Se,b as H,am as Ct,O as xt,an as _e,m as St,ao as jt,S as Je,ap as At,N as Rt,c as Nt,aq as Pt,ar as It,A as Tt,as as ue,M as Xe}from"./icons-BwhnWKHR.js";import{r as Et,h as F}from"./MonitorStackIcon-QQhyrZEW.js";import"./pwa-bootstrap-ljOQTt2e.js";const Ot=d.forwardRef(function({value:s,onChange:n,onSubmit:r,onOpenSuggestedTasks:i,suggestedOpen:m=!1,suggestedTaskCount:c,disabled:v=!1,placeholder:S="Ask VuvoDesk to work across your machines...",compact:g=!1},x){return t.jsxs("form",{className:"agents-composer",onSubmit:w=>{w.preventDefault(),r()},children:[t.jsx("button",{className:`agents-suggested-trigger ${m?"active":""}`,type:"button","aria-label":c===void 0?"Open Suggested tasks":`Open Suggested tasks (${c} available)`,"aria-expanded":m,onClick:i,disabled:v,children:t.jsx(ht,{size:17})}),t.jsx("textarea",{ref:x,value:s,onChange:w=>n(w.target.value.slice(0,4e3)),placeholder:S,"aria-label":"Ask VuvoDesk",rows:g?1:2,disabled:v}),t.jsxs("button",{className:"agents-run-button",type:"submit",disabled:v||!s.trim(),children:[v?t.jsx(Ce,{className:"spin",size:16}):t.jsx(yt,{size:16}),v?"Running":"Run"]})]})}),$t=["read","processControl","serviceControl","applicationControl","fileRead","fileWrite","fileDelete","shell","script","softwareInstall","network","systemPower","systemConfiguration","userAccount"];function de(e){return e.deviceName?.trim()||e.hostname?.trim()||e.deviceId}function pe(e){return e.connected!==!0?!1:e.synthetic===!0?!0:!e.channels||e.channels.control===!0}function Dt(e){return e==="running"||e==="completed"||e==="failed"||e==="cancelled"?e:"queued"}function _t(e){switch(e){case"process.list":return"Process list";case"system.health":return"System health";case"gpu.status":return"GPU status";case"disk.status":return"Disk status";case"service.status":return"Service status";case"diagnostics.collect":return"Diagnostics";case"process.control":return"Process control";case"service.control":return"Service control";case"application.launch":return"Launch application";case"application.close":return"Close application";case"file.read":return"Read file";case"file.write":return"Write file";case"file.delete":return"Delete file";case"file.list":return"List directory";case"command.run":return"Run command";case"script.run":return"Run script";case"software.install":return"Install software";case"network.status":return"Network status";case"system.power":return"Power action";case"system.configure":return"System configuration";case"logs.collect":return"Collect logs"}}const Mt=64*1024,Me="[Earlier log output hidden by PWA display limit.]",Qe=new TextEncoder,zt=new TextDecoder("utf-8",{fatal:!0}),ze=16*1024,Lt=200,Le="(?:access[_-]?(?:token|key)|refresh[_-]?token|id[_-]?token|pair[_-]?(?:token|key)|authorization|api[_-]?key|private[_-]?key|connection[_-]?string|credential|password|passwd|secret|cookie|set-cookie)",Ft="(?:access[_-]?(?:token|key)|refresh[_-]?token|id[_-]?token|pair[_-]?(?:token|key)|token|authorization|api[_-]?key|private[_-]?key|connection[_-]?string|credential|password|passwd|secret|cookie|set-cookie)",Bt="(?:access[_-]?(?:token|key)|refresh[_-]?token|id[_-]?token|pair[_-]?(?:token|key)|token|authorization|api[_-]?key|credential|password|secret|cookie)",qt=/\b(?:sk-(?:proj-|ant-)?[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|(?:AKIA|ASIA)[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{35}|npm_[A-Za-z0-9]{8,}|xox[a-z]-[A-Za-z0-9-]{8,}|(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,})\b/g;function Ut(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:null}function Fe(e){return Qe.encode(e).byteLength}function Be(e,s){const n=Qe.encode(e);if(n.byteLength<=s)return{value:e,truncated:!1};let r=n.byteLength-s;for(;r<n.byteLength&&(n[r]&192)===128;)r+=1;let i=zt.decode(n.subarray(r));const m=i.indexOf(`
|
|
2
2
|
`);return m>=0&&m<i.length-1&&(i=i.slice(m+1)),{value:i,truncated:!0}}function et(e){return e.replace(/-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/gi,"[REDACTED PRIVATE KEY]").replace(/\b(?:proxy-)?authorization\s*[:=]\s*[^\r\n]*|\b(?:set-)?cookie\s*[:=]\s*[^\r\n]*/gi,"credential-header=[REDACTED]").replace(/\bBearer\s+(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi,"Bearer [REDACTED]").replace(new RegExp(`("${Le}"\\s*:\\s*")[^"\\r\\n]*(")`,"gi"),"$1[REDACTED]$2").replace(new RegExp(`('${Le}'\\s*:\\s*')[^'\\r\\n]*(')`,"gi"),"$1[REDACTED]$2").replace(new RegExp(`((?:[A-Za-z0-9.-]+[_-])?${Ft}\\s*[:=]\\s*)(?!\\[REDACTED\\])[^\\s,;&]+`,"gi"),"$1[REDACTED]").replace(new RegExp(`([?&]${Bt}=)[^&\\s]+`,"gi"),"$1[REDACTED]").replace(/\b(?:eyJ|[A-Za-z0-9_-]{8,}\.)[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,"[REDACTED TOKEN]").replace(qt,"[REDACTED KEY]")}function Gt(e){return typeof e!="string"?"source unknown":et(e).replace(/[\u0000-\u001f\u007f]/g," ").trim().slice(0,64)||"source unknown"}function qe(e){return typeof e=="number"&&Number.isSafeInteger(e)&&e>=0?Math.floor(e):null}function Vt(e){if(typeof e!="string"||e.length===0)return{output:"",displayTruncated:!1};const s=Be(e.replace(/\r\n?/g,`
|
|
3
3
|
`),Mt);let n=et(s.value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g," ").trim(),r=s.truncated;const i=n?n.split(`
|
|
4
4
|
`):[];if(i.length>Lt-1&&(n=i.slice(-199).join(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as e}from"./pwa-startup-C8kDd3Vd.js";import{r as i,R as x,E as j,D as u,f as g,h as f}from"./icons-BwhnWKHR.js";import{l as b,c as d,d as y}from"./HubApp-
|
|
1
|
+
import{j as e}from"./pwa-startup-C8kDd3Vd.js";import{r as i,R as x,E as j,D as u,f as g,h as f}from"./icons-BwhnWKHR.js";import{l as b,c as d,d as y}from"./HubApp-CbKj-EB_.js";import"./pwa-bootstrap-ljOQTt2e.js";import"./public-site-routing-pddZbLCj.js";import"./MonitorStackIcon-QQhyrZEW.js";import"./supabase-C7qjtLN3.js";import"./frame-lifecycle-D-BxeTQV.js";function z({hubUrl:s,onToast:n}){const[o,m]=i.useState([]),[h,c]=i.useState(!0),r=i.useCallback(async()=>{c(!0);try{m((await b(s)).captures||[])}catch(t){n(t instanceof Error?t.message:String(t))}finally{c(!1)}},[s,n]);i.useEffect(()=>{r()},[r]);const p=async t=>{const a=d(t.id,s);if(navigator.share)try{await navigator.share({title:t.fileName,url:a});return}catch{}window.open(a,"_blank","noopener,noreferrer")};return e.jsxs("section",{className:"capture-gallery",children:[e.jsxs("header",{children:[e.jsxs("div",{children:[e.jsx("span",{className:"settings-eyebrow",children:"Capture Gallery"}),e.jsx("h2",{children:"Saved captures"})]}),e.jsxs("button",{className:"text-button",onClick:()=>{r()},type:"button",children:[e.jsx(x,{size:15})," Refresh"]})]}),h?e.jsx("p",{className:"settings-help",children:"Loading captures..."}):o.length===0?e.jsx("p",{className:"settings-help",children:"No captures saved yet."}):e.jsx("div",{className:"capture-gallery-grid",children:o.map(t=>{const a=d(t.id,s);return e.jsxs("article",{className:"capture-gallery-card",children:[t.mimeType.startsWith("image/")?e.jsx("img",{src:a,alt:t.target.label}):e.jsx("video",{src:a,controls:!0,preload:"metadata"}),e.jsxs("div",{className:"capture-gallery-card-body",children:[e.jsx("strong",{children:t.fileName}),e.jsxs("small",{children:[t.target.label," · ",new Date(t.createdAt).toLocaleString()," · ",Math.ceil(t.sizeBytes/1024)," KB"]}),e.jsxs("div",{children:[e.jsx("button",{className:"icon-button",title:"Open","aria-label":"Open",onClick:()=>window.open(a,"_blank","noopener,noreferrer"),type:"button",children:e.jsx(j,{size:15})}),e.jsx("a",{className:"icon-button",title:"Download","aria-label":"Download",href:`${a}?download=1`,download:t.fileName,children:e.jsx(u,{size:15})}),e.jsx("button",{className:"icon-button",title:"Share","aria-label":"Share",onClick:()=>{p(t)},type:"button",children:e.jsx(g,{size:15})}),e.jsx("button",{className:"icon-button danger",title:"Delete","aria-label":"Delete",onClick:()=>{window.confirm(`Delete ${t.fileName}?`)&&y(t.id,s).then(()=>r()).catch(l=>n(l instanceof Error?l.message:String(l)))},type:"button",children:e.jsx(f,{size:15})})]})]})]},t.id)})})]})}export{z as CaptureGallery};
|