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.
Files changed (24) hide show
  1. package/electron/after-pack.cjs +5 -0
  2. package/electron/electron-builder.linux-x64.yml +11 -2
  3. package/electron/linux-signed-updater.mjs +107 -0
  4. package/electron/linux-updater/com.vuvodesk.update.policy +21 -0
  5. package/electron/linux-updater/release-public-key.json +5 -0
  6. package/electron/linux-updater/vuvodesk-update-helper +264 -0
  7. package/electron/update-manager.mjs +32 -8
  8. package/package.json +1 -1
  9. package/web/dist/app.html +1 -1
  10. package/web/dist/app.webmanifest +1 -1
  11. package/web/dist/assets/{AgentSettingsTab-D0oebRk-.js → AgentSettingsTab-BOUun4gQ.js} +1 -1
  12. package/web/dist/assets/{AgentsPage-BBj64SpA.js → AgentsPage-Cx7U-mYh.js} +1 -1
  13. package/web/dist/assets/{CaptureGallery-DF6gZHy0.js → CaptureGallery-DNRbC4Nm.js} +1 -1
  14. package/web/dist/assets/{HubApp-DJsiVqWP.js → HubApp-CbKj-EB_.js} +3 -3
  15. package/web/dist/assets/{MonitorStackIcon-BD7Yn-Z9.js → MonitorStackIcon-QQhyrZEW.js} +1 -1
  16. package/web/dist/assets/{SettingsPage-CXmk161K.js → SettingsPage-ZAduWG1M.js} +1 -1
  17. package/web/dist/assets/{ShareFilesPage-Dx5yYsbk.js → ShareFilesPage-R8N1yFBD.js} +1 -1
  18. package/web/dist/assets/{SupportPage-DAsms8Kr.js → SupportPage-B4DfcVAu.js} +1 -1
  19. package/web/dist/assets/{VuvoDeskApp-9sUnuvyS.js → VuvoDeskApp-CerByFHO.js} +1 -1
  20. package/web/dist/assets/{app-CUM2wTSb.js → app-CgvQQda5.js} +2 -2
  21. package/web/dist/assets/{main-BbK4NG31.js → main-Cx9AZE78.js} +2 -2
  22. package/web/dist/index.html +2 -2
  23. package/web/dist/sw.js +2 -2
  24. package/web/dist/vuvodesk-build-evidence.json +39 -39
@@ -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
- Updates are checked and downloaded inside VuvoDesk. Installing a deb update
47
- may require your administrator password.
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,5 @@
1
+ {
2
+ "schema": 1,
3
+ "algorithm": "Ed25519",
4
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAQZxBhAsKb23JDzilyvpa1O3SUBiUEn/dil+JJxe3LQ8=\n-----END PUBLIC KEY-----\n"
5
+ }
@@ -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
- const packageType = String(readFileSync(join(process.resourcesPath, 'package-type'), 'utf8')).trim();
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
- await beforeInstall();
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
- const failure = rememberInstallerFailure(error);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.701",
3
+ "version": "0.1.702",
4
4
  "livedeskClientVersion": "0.1.277",
5
5
  "buildFlavor": "production",
6
6
  "description": "VuvoDesk Hub and client launcher",
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-CUM2wTSb.js" data-vuvodesk-entry></script>
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>
@@ -4,7 +4,7 @@
4
4
  "short_name": "VuvoDesk",
5
5
  "description": "Monitor and control your VuvoDesk computers from your phone.",
6
6
  "lang": "en",
7
- "start_url": "/app?pwa=0.1.701",
7
+ "start_url": "/app?pwa=0.1.702",
8
8
  "scope": "/",
9
9
  "display": "standalone",
10
10
  "orientation": "any",
@@ -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-BD7Yn-Z9.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
+ 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-BD7Yn-Z9.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(`
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-DJsiVqWP.js";import"./pwa-bootstrap-ljOQTt2e.js";import"./public-site-routing-pddZbLCj.js";import"./MonitorStackIcon-BD7Yn-Z9.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};
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};