@livedesk/hub 0.1.32 → 0.1.33

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.
@@ -1,239 +1,251 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
-
4
- export const SETTINGS_SCHEMA_VERSION = 2;
5
-
6
- export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
7
- settingsSchemaVersion: SETTINGS_SCHEMA_VERSION,
8
- connection: {},
9
- security: {
10
- accessMode: 'trusted-only',
11
- allowInternetConnections: false,
12
- allowLanConnections: true,
13
- lockOnControlEnd: true,
14
- idleControlMinutes: 30
15
- },
16
- control: {
17
- allowKeyboardMouse: true,
18
- allowSystemShortcuts: true,
19
- allowClipboardText: true,
20
- allowRemoteRestart: false,
21
- reconnectAfterRemoteRestart: true,
22
- allowSwitchingMonitors: true,
23
- showConnectionToolbar: true,
24
- showRemoteCursor: true,
25
- openControlOnDoubleClick: true,
26
- startRemoteAudioWithControl: false,
27
- fitRemoteScreen: true,
28
- rememberLastMonitor: true,
29
- keepControlReadyBetweenPages: true
30
- },
31
- wall: {
32
- performanceMode: 'auto',
33
- autoStart: true,
34
- connectedOnly: false,
35
- keepEmptySlots: true,
36
- showDeviceStatus: true,
37
- showPerformanceDetails: false,
38
- pauseHiddenTiles: true,
39
- reduceWhenHidden: true,
40
- autoAdjustTileQuality: true,
41
- rememberDevicePositions: true
42
- },
43
- filesAudio: {
44
- allowFileTransfer: true,
45
- allowFolderSync: false,
46
- openReceivedFolder: false,
47
- notifyTransferComplete: true,
48
- defaultReceiveFolder: 'Desktop/LiveDeskFiles',
49
- maxFileSizeBytes: 1024 * 1024 * 1024,
50
- allowRemoteAudio: true,
51
- startAudioMuted: false,
52
- rememberVolume: true,
53
- automaticallyRecoverAudio: true,
54
- showAudioTroubleshooting: false,
55
- rollingBufferEnabled: false,
56
- recordingQuality: 'standard',
57
- timelapseIntervalSeconds: 10,
58
- includeRemoteCursor: true,
59
- captureSaveLocation: 'LiveDesk Captures',
60
- captureAutoDelete: 'never'
61
- },
62
- agent: {
63
- enabled: false,
64
- defaultPermissionMode: 'safe-auto',
65
- askBeforeDestructive: true,
66
- allowProcessManagement: true,
67
- allowServiceManagement: true,
68
- allowFileChanges: true,
69
- allowCommandExecution: false,
70
- allowSoftwareInstallation: false,
71
- allowPowerActions: false,
72
- keepAuditHistory: true,
73
- timeoutMinutes: 10
74
- },
75
- advanced: {
76
- wallFrameMode: 'auto',
77
- wallFps: 20,
78
- wallMaxWidth: 960,
79
- wallMaxHeight: 540,
80
- wallQuality: 55,
81
- controlFrameMode: 'mode3-h264-hw',
82
- controlFps: 30,
83
- controlMaxWidth: 1920,
84
- controlMaxHeight: 1080,
85
- controlQuality: 60,
86
- transport: 'auto',
87
- verboseLogs: false,
88
- frameStatistics: false
89
- }
90
- });
91
-
92
- const ENUMS = {
93
- accessMode: new Set(['trusted-only', 'ask-every-time', 'view-only', 'block-remote-access']),
94
- performanceMode: new Set(['auto', 'responsive', 'quality', 'custom']),
95
- permissionMode: new Set(['ask', 'safe-auto', 'full-access', 'custom']),
96
- wallFrameMode: new Set(['auto', 'mode2-lzo', 'mode3-h264-hw', 'mode4-h264-atlas']),
97
- controlFrameMode: new Set(['mode3-h264-hw', 'mode5-lzo-delta']),
98
- transport: new Set(['auto', 'encrypted']),
99
- recordingQuality: new Set(['standard', 'high']),
100
- captureAutoDelete: new Set(['never', '7-days', '30-days'])
101
- };
102
-
103
- function booleanValue(value, fallback) {
104
- return typeof value === 'boolean' ? value : fallback;
105
- }
106
-
107
- function numberValue(value, min, max, fallback) {
108
- const number = Number(value);
109
- return Number.isFinite(number) ? Math.max(min, Math.min(max, Math.round(number))) : fallback;
110
- }
111
-
112
- function enumValue(value, allowed, fallback) {
113
- const normalized = String(value ?? fallback).trim().toLowerCase();
114
- return allowed.has(normalized) ? normalized : fallback;
115
- }
116
-
117
- function stringValue(value, fallback, maxLength = 600) {
118
- return typeof value === 'string' ? value.replace(/[\0\r\n]/g, ' ').trim().slice(0, maxLength) : fallback;
119
- }
120
-
121
- function normalizeSection(source, defaults, rules = {}) {
122
- const input = source && typeof source === 'object' && !Array.isArray(source) ? source : {};
123
- const result = {};
124
- for (const [key, fallback] of Object.entries(defaults)) {
125
- const rule = rules[key];
126
- if (rule?.type === 'boolean') result[key] = booleanValue(input[key], fallback);
127
- else if (rule?.type === 'number') result[key] = numberValue(input[key], rule.min, rule.max, fallback);
128
- else if (rule?.type === 'enum') result[key] = enumValue(input[key], rule.values, fallback);
129
- else if (rule?.type === 'string') result[key] = stringValue(input[key], fallback, rule.maxLength);
130
- else result[key] = input[key] === undefined ? fallback : fallback;
131
- }
132
- return result;
133
- }
134
-
135
- const bools = keys => Object.fromEntries(keys.map(key => [key, { type: 'boolean' }]));
136
- const numbers = (entries) => Object.fromEntries(entries.map(([key, min, max]) => [key, { type: 'number', min, max }]));
137
-
138
- const RULES = {
139
- connection: {},
140
- security: {
141
- accessMode: { type: 'enum', values: ENUMS.accessMode },
142
- ...bools(['allowInternetConnections', 'allowLanConnections', 'lockOnControlEnd']),
143
- ...numbers([['idleControlMinutes', 5, 1440]])
144
- },
145
- control: {
146
- ...bools(['allowKeyboardMouse', 'allowSystemShortcuts', 'allowClipboardText', 'allowRemoteRestart', 'reconnectAfterRemoteRestart', 'allowSwitchingMonitors', 'showConnectionToolbar', 'showRemoteCursor', 'openControlOnDoubleClick', 'startRemoteAudioWithControl', 'fitRemoteScreen', 'rememberLastMonitor', 'keepControlReadyBetweenPages'])
147
- },
148
- wall: {
149
- performanceMode: { type: 'enum', values: ENUMS.performanceMode },
150
- ...bools(['autoStart', 'connectedOnly', 'keepEmptySlots', 'showDeviceStatus', 'showPerformanceDetails', 'pauseHiddenTiles', 'reduceWhenHidden', 'autoAdjustTileQuality', 'rememberDevicePositions'])
151
- },
152
- filesAudio: {
153
- ...bools(['allowFileTransfer', 'allowFolderSync', 'openReceivedFolder', 'notifyTransferComplete', 'allowRemoteAudio', 'startAudioMuted', 'rememberVolume', 'automaticallyRecoverAudio', 'showAudioTroubleshooting', 'rollingBufferEnabled', 'includeRemoteCursor']),
154
- recordingQuality: { type: 'enum', values: ENUMS.recordingQuality },
155
- captureAutoDelete: { type: 'enum', values: ENUMS.captureAutoDelete },
156
- timelapseIntervalSeconds: { type: 'number', min: 5, max: 60 },
157
- defaultReceiveFolder: { type: 'string', maxLength: 600 },
158
- captureSaveLocation: { type: 'string', maxLength: 120 },
159
- maxFileSizeBytes: { type: 'number', min: 1, max: Number.MAX_SAFE_INTEGER }
160
- },
161
- agent: {
162
- defaultPermissionMode: { type: 'enum', values: ENUMS.permissionMode },
163
- ...bools(['enabled', 'askBeforeDestructive', 'allowProcessManagement', 'allowServiceManagement', 'allowFileChanges', 'allowCommandExecution', 'allowSoftwareInstallation', 'allowPowerActions', 'keepAuditHistory']),
164
- timeoutMinutes: { type: 'number', min: 1, max: 120 }
165
- },
166
- advanced: {
167
- wallFrameMode: { type: 'enum', values: ENUMS.wallFrameMode },
168
- controlFrameMode: { type: 'enum', values: ENUMS.controlFrameMode },
169
- transport: { type: 'enum', values: ENUMS.transport },
170
- ...bools(['verboseLogs', 'frameStatistics']),
171
- ...numbers([['wallFps', 1, 60], ['wallMaxWidth', 320, 3840], ['wallMaxHeight', 180, 2160], ['wallQuality', 20, 95], ['controlFps', 20, 60], ['controlMaxWidth', 640, 3840], ['controlMaxHeight', 360, 2160], ['controlQuality', 20, 95]])
172
- }
173
- };
174
-
175
- export function normalizeLiveDeskSettings(value = {}) {
176
- const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
177
- const settings = {
178
- settingsSchemaVersion: SETTINGS_SCHEMA_VERSION,
179
- connection: normalizeSection(source.connection, DEFAULT_LIVEDESK_SETTINGS.connection, RULES.connection),
180
- security: normalizeSection(source.security, DEFAULT_LIVEDESK_SETTINGS.security, RULES.security),
181
- control: normalizeSection(source.control, DEFAULT_LIVEDESK_SETTINGS.control, RULES.control),
182
- wall: normalizeSection(source.wall, DEFAULT_LIVEDESK_SETTINGS.wall, RULES.wall),
183
- filesAudio: normalizeSection(source.filesAudio, DEFAULT_LIVEDESK_SETTINGS.filesAudio, RULES.filesAudio),
184
- agent: normalizeSection(source.agent, DEFAULT_LIVEDESK_SETTINGS.agent, RULES.agent),
185
- advanced: normalizeSection(source.advanced, DEFAULT_LIVEDESK_SETTINGS.advanced, RULES.advanced)
186
- };
187
- if (!settings.security.allowInternetConnections) {
188
- settings.advanced.transport = settings.advanced.transport === 'encrypted' ? 'encrypted' : 'auto';
189
- }
190
- if (settings.agent.defaultPermissionMode === 'safe-auto') {
191
- settings.agent.askBeforeDestructive = true;
192
- }
193
- if (![5, 10, 30, 60].includes(settings.filesAudio.timelapseIntervalSeconds)) {
194
- settings.filesAudio.timelapseIntervalSeconds = 10;
195
- }
196
- return settings;
197
- }
198
-
199
- export function defaultSettingsPath(dataDir = undefined) {
200
- return path.join(dataDir || path.join(os.homedir(), '.livedesk'), 'settings.json');
201
- }
202
-
203
- export function publicSettings(settings) {
204
- return normalizeLiveDeskSettings(settings);
205
- }
206
-
207
- export function effectiveDevicePolicy(settings = DEFAULT_LIVEDESK_SETTINGS) {
208
- const normalized = normalizeLiveDeskSettings(settings);
209
- // The retired ask-every-time surface has no interactive approval authority,
210
- // so a persisted legacy value must fail closed. View-only remains able to
211
- // receive video/audio while every mutating operation is blocked.
212
- const accessMode = normalized.security.accessMode === 'ask-every-time'
213
- ? 'block-remote-access'
214
- : normalized.security.accessMode;
215
- const allowMutation = accessMode === 'trusted-only';
216
- return {
217
- settingsSchemaVersion: normalized.settingsSchemaVersion,
218
- policyRevision: Number(settings?.revision || 0),
219
- accessMode,
220
- allowInternetConnections: normalized.security.allowInternetConnections,
221
- allowLanConnections: normalized.security.allowLanConnections,
222
- // Encryption and no-overwrite are structural invariants. They are not
223
- // user preferences and cannot be disabled through a legacy settings file.
224
- requireEncryptedConnections: true,
225
- allowUnencryptedLanFallback: false,
226
- visibleControlIndicator: true,
227
- lockOnControlEnd: normalized.security.lockOnControlEnd,
228
- disconnectIdleControlSessions: true,
229
- idleControlMinutes: normalized.security.idleControlMinutes,
230
- allowFileOverwrite: false,
231
- maxFileSizeBytes: normalized.filesAudio.maxFileSizeBytes,
232
- allowControl: allowMutation && normalized.control.allowKeyboardMouse,
233
- allowClipboardText: allowMutation && normalized.control.allowClipboardText,
234
- allowPowerActions: allowMutation && normalized.control.allowRemoteRestart,
235
- allowFileTransfer: allowMutation && normalized.filesAudio.allowFileTransfer,
236
- allowRemoteAudio: accessMode !== 'block-remote-access' && normalized.filesAudio.allowRemoteAudio,
237
- allowAgent: allowMutation && normalized.agent.enabled
238
- };
239
- }
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export const SETTINGS_SCHEMA_VERSION = 3;
5
+
6
+ export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
7
+ settingsSchemaVersion: SETTINGS_SCHEMA_VERSION,
8
+ connection: {},
9
+ security: {
10
+ accessMode: 'trusted-only',
11
+ allowInternetConnections: true,
12
+ allowLanConnections: true,
13
+ lockOnControlEnd: true,
14
+ idleControlMinutes: 30
15
+ },
16
+ control: {
17
+ allowKeyboardMouse: true,
18
+ allowSystemShortcuts: true,
19
+ allowClipboardText: true,
20
+ allowRemoteRestart: false,
21
+ reconnectAfterRemoteRestart: true,
22
+ allowSwitchingMonitors: true,
23
+ showConnectionToolbar: true,
24
+ showRemoteCursor: true,
25
+ openControlOnDoubleClick: true,
26
+ startRemoteAudioWithControl: false,
27
+ fitRemoteScreen: true,
28
+ rememberLastMonitor: true,
29
+ keepControlReadyBetweenPages: true
30
+ },
31
+ wall: {
32
+ performanceMode: 'auto',
33
+ autoStart: true,
34
+ connectedOnly: false,
35
+ keepEmptySlots: true,
36
+ showDeviceStatus: true,
37
+ showPerformanceDetails: false,
38
+ pauseHiddenTiles: true,
39
+ reduceWhenHidden: true,
40
+ autoAdjustTileQuality: true,
41
+ rememberDevicePositions: true
42
+ },
43
+ filesAudio: {
44
+ allowFileTransfer: true,
45
+ allowFolderSync: false,
46
+ openReceivedFolder: false,
47
+ notifyTransferComplete: true,
48
+ defaultReceiveFolder: 'Desktop/LiveDeskFiles',
49
+ maxFileSizeBytes: 1024 * 1024 * 1024,
50
+ allowRemoteAudio: true,
51
+ startAudioMuted: false,
52
+ rememberVolume: true,
53
+ automaticallyRecoverAudio: true,
54
+ showAudioTroubleshooting: false,
55
+ rollingBufferEnabled: false,
56
+ recordingQuality: 'standard',
57
+ timelapseIntervalSeconds: 10,
58
+ includeRemoteCursor: true,
59
+ captureSaveLocation: 'LiveDesk Captures',
60
+ captureAutoDelete: 'never'
61
+ },
62
+ agent: {
63
+ enabled: false,
64
+ defaultPermissionMode: 'safe-auto',
65
+ askBeforeDestructive: true,
66
+ allowProcessManagement: true,
67
+ allowServiceManagement: true,
68
+ allowFileChanges: true,
69
+ allowCommandExecution: false,
70
+ allowSoftwareInstallation: false,
71
+ allowPowerActions: false,
72
+ keepAuditHistory: true,
73
+ timeoutMinutes: 10
74
+ },
75
+ advanced: {
76
+ wallFrameMode: 'auto',
77
+ wallFps: 20,
78
+ wallMaxWidth: 960,
79
+ wallMaxHeight: 540,
80
+ wallQuality: 55,
81
+ controlFrameMode: 'mode3-h264-hw',
82
+ controlFps: 30,
83
+ controlMaxWidth: 1920,
84
+ controlMaxHeight: 1080,
85
+ controlQuality: 60,
86
+ transport: 'auto',
87
+ verboseLogs: false,
88
+ frameStatistics: false
89
+ }
90
+ });
91
+
92
+ const ENUMS = {
93
+ accessMode: new Set(['trusted-only', 'ask-every-time', 'view-only', 'block-remote-access']),
94
+ performanceMode: new Set(['auto', 'responsive', 'quality', 'custom']),
95
+ permissionMode: new Set(['ask', 'safe-auto', 'full-access', 'custom']),
96
+ wallFrameMode: new Set(['auto', 'mode2-lzo', 'mode3-h264-hw', 'mode4-h264-atlas']),
97
+ controlFrameMode: new Set(['mode3-h264-hw', 'mode5-lzo-delta']),
98
+ transport: new Set(['auto', 'encrypted']),
99
+ recordingQuality: new Set(['standard', 'high']),
100
+ captureAutoDelete: new Set(['never', '7-days', '30-days'])
101
+ };
102
+
103
+ function booleanValue(value, fallback) {
104
+ return typeof value === 'boolean' ? value : fallback;
105
+ }
106
+
107
+ function numberValue(value, min, max, fallback) {
108
+ const number = Number(value);
109
+ return Number.isFinite(number) ? Math.max(min, Math.min(max, Math.round(number))) : fallback;
110
+ }
111
+
112
+ function enumValue(value, allowed, fallback) {
113
+ const normalized = String(value ?? fallback).trim().toLowerCase();
114
+ return allowed.has(normalized) ? normalized : fallback;
115
+ }
116
+
117
+ function stringValue(value, fallback, maxLength = 600) {
118
+ return typeof value === 'string' ? value.replace(/[\0\r\n]/g, ' ').trim().slice(0, maxLength) : fallback;
119
+ }
120
+
121
+ function normalizeSection(source, defaults, rules = {}) {
122
+ const input = source && typeof source === 'object' && !Array.isArray(source) ? source : {};
123
+ const result = {};
124
+ for (const [key, fallback] of Object.entries(defaults)) {
125
+ const rule = rules[key];
126
+ if (rule?.type === 'boolean') result[key] = booleanValue(input[key], fallback);
127
+ else if (rule?.type === 'number') result[key] = numberValue(input[key], rule.min, rule.max, fallback);
128
+ else if (rule?.type === 'enum') result[key] = enumValue(input[key], rule.values, fallback);
129
+ else if (rule?.type === 'string') result[key] = stringValue(input[key], fallback, rule.maxLength);
130
+ else result[key] = input[key] === undefined ? fallback : fallback;
131
+ }
132
+ return result;
133
+ }
134
+
135
+ const bools = keys => Object.fromEntries(keys.map(key => [key, { type: 'boolean' }]));
136
+ const numbers = (entries) => Object.fromEntries(entries.map(([key, min, max]) => [key, { type: 'number', min, max }]));
137
+
138
+ const RULES = {
139
+ connection: {},
140
+ security: {
141
+ accessMode: { type: 'enum', values: ENUMS.accessMode },
142
+ ...bools(['allowInternetConnections', 'allowLanConnections', 'lockOnControlEnd']),
143
+ ...numbers([['idleControlMinutes', 5, 1440]])
144
+ },
145
+ control: {
146
+ ...bools(['allowKeyboardMouse', 'allowSystemShortcuts', 'allowClipboardText', 'allowRemoteRestart', 'reconnectAfterRemoteRestart', 'allowSwitchingMonitors', 'showConnectionToolbar', 'showRemoteCursor', 'openControlOnDoubleClick', 'startRemoteAudioWithControl', 'fitRemoteScreen', 'rememberLastMonitor', 'keepControlReadyBetweenPages'])
147
+ },
148
+ wall: {
149
+ performanceMode: { type: 'enum', values: ENUMS.performanceMode },
150
+ ...bools(['autoStart', 'connectedOnly', 'keepEmptySlots', 'showDeviceStatus', 'showPerformanceDetails', 'pauseHiddenTiles', 'reduceWhenHidden', 'autoAdjustTileQuality', 'rememberDevicePositions'])
151
+ },
152
+ filesAudio: {
153
+ ...bools(['allowFileTransfer', 'allowFolderSync', 'openReceivedFolder', 'notifyTransferComplete', 'allowRemoteAudio', 'startAudioMuted', 'rememberVolume', 'automaticallyRecoverAudio', 'showAudioTroubleshooting', 'rollingBufferEnabled', 'includeRemoteCursor']),
154
+ recordingQuality: { type: 'enum', values: ENUMS.recordingQuality },
155
+ captureAutoDelete: { type: 'enum', values: ENUMS.captureAutoDelete },
156
+ timelapseIntervalSeconds: { type: 'number', min: 5, max: 60 },
157
+ defaultReceiveFolder: { type: 'string', maxLength: 600 },
158
+ captureSaveLocation: { type: 'string', maxLength: 120 },
159
+ maxFileSizeBytes: { type: 'number', min: 1, max: Number.MAX_SAFE_INTEGER }
160
+ },
161
+ agent: {
162
+ defaultPermissionMode: { type: 'enum', values: ENUMS.permissionMode },
163
+ ...bools(['enabled', 'askBeforeDestructive', 'allowProcessManagement', 'allowServiceManagement', 'allowFileChanges', 'allowCommandExecution', 'allowSoftwareInstallation', 'allowPowerActions', 'keepAuditHistory']),
164
+ timeoutMinutes: { type: 'number', min: 1, max: 120 }
165
+ },
166
+ advanced: {
167
+ wallFrameMode: { type: 'enum', values: ENUMS.wallFrameMode },
168
+ controlFrameMode: { type: 'enum', values: ENUMS.controlFrameMode },
169
+ transport: { type: 'enum', values: ENUMS.transport },
170
+ ...bools(['verboseLogs', 'frameStatistics']),
171
+ ...numbers([['wallFps', 1, 60], ['wallMaxWidth', 320, 3840], ['wallMaxHeight', 180, 2160], ['wallQuality', 20, 95], ['controlFps', 20, 60], ['controlMaxWidth', 640, 3840], ['controlMaxHeight', 360, 2160], ['controlQuality', 20, 95]])
172
+ }
173
+ };
174
+
175
+ export function migrateLiveDeskSettings(value = {}) {
176
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? structuredClone(value) : {};
177
+ const sourceVersion = Math.max(0, Number(source.settingsSchemaVersion) || 0);
178
+ if (sourceVersion < 3) {
179
+ source.security = {
180
+ ...(source.security && typeof source.security === 'object' && !Array.isArray(source.security) ? source.security : {}),
181
+ allowInternetConnections: true
182
+ };
183
+ }
184
+ return normalizeLiveDeskSettings(source);
185
+ }
186
+
187
+ export function normalizeLiveDeskSettings(value = {}) {
188
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
189
+ const settings = {
190
+ settingsSchemaVersion: SETTINGS_SCHEMA_VERSION,
191
+ connection: normalizeSection(source.connection, DEFAULT_LIVEDESK_SETTINGS.connection, RULES.connection),
192
+ security: normalizeSection(source.security, DEFAULT_LIVEDESK_SETTINGS.security, RULES.security),
193
+ control: normalizeSection(source.control, DEFAULT_LIVEDESK_SETTINGS.control, RULES.control),
194
+ wall: normalizeSection(source.wall, DEFAULT_LIVEDESK_SETTINGS.wall, RULES.wall),
195
+ filesAudio: normalizeSection(source.filesAudio, DEFAULT_LIVEDESK_SETTINGS.filesAudio, RULES.filesAudio),
196
+ agent: normalizeSection(source.agent, DEFAULT_LIVEDESK_SETTINGS.agent, RULES.agent),
197
+ advanced: normalizeSection(source.advanced, DEFAULT_LIVEDESK_SETTINGS.advanced, RULES.advanced)
198
+ };
199
+ if (!settings.security.allowInternetConnections) {
200
+ settings.advanced.transport = settings.advanced.transport === 'encrypted' ? 'encrypted' : 'auto';
201
+ }
202
+ if (settings.agent.defaultPermissionMode === 'safe-auto') {
203
+ settings.agent.askBeforeDestructive = true;
204
+ }
205
+ if (![5, 10, 30, 60].includes(settings.filesAudio.timelapseIntervalSeconds)) {
206
+ settings.filesAudio.timelapseIntervalSeconds = 10;
207
+ }
208
+ return settings;
209
+ }
210
+
211
+ export function defaultSettingsPath(dataDir = undefined) {
212
+ return path.join(dataDir || path.join(os.homedir(), '.livedesk'), 'settings.json');
213
+ }
214
+
215
+ export function publicSettings(settings) {
216
+ return normalizeLiveDeskSettings(settings);
217
+ }
218
+
219
+ export function effectiveDevicePolicy(settings = DEFAULT_LIVEDESK_SETTINGS) {
220
+ const normalized = normalizeLiveDeskSettings(settings);
221
+ // The retired ask-every-time surface has no interactive approval authority,
222
+ // so a persisted legacy value must fail closed. View-only remains able to
223
+ // receive video/audio while every mutating operation is blocked.
224
+ const accessMode = normalized.security.accessMode === 'ask-every-time'
225
+ ? 'block-remote-access'
226
+ : normalized.security.accessMode;
227
+ const allowMutation = accessMode === 'trusted-only';
228
+ return {
229
+ settingsSchemaVersion: normalized.settingsSchemaVersion,
230
+ policyRevision: Number(settings?.revision || 0),
231
+ accessMode,
232
+ allowInternetConnections: normalized.security.allowInternetConnections,
233
+ allowLanConnections: normalized.security.allowLanConnections,
234
+ // Encryption and no-overwrite are structural invariants. They are not
235
+ // user preferences and cannot be disabled through a legacy settings file.
236
+ requireEncryptedConnections: true,
237
+ allowUnencryptedLanFallback: false,
238
+ visibleControlIndicator: true,
239
+ lockOnControlEnd: normalized.security.lockOnControlEnd,
240
+ disconnectIdleControlSessions: true,
241
+ idleControlMinutes: normalized.security.idleControlMinutes,
242
+ allowFileOverwrite: false,
243
+ maxFileSizeBytes: normalized.filesAudio.maxFileSizeBytes,
244
+ allowControl: allowMutation && normalized.control.allowKeyboardMouse,
245
+ allowClipboardText: allowMutation && normalized.control.allowClipboardText,
246
+ allowPowerActions: allowMutation && normalized.control.allowRemoteRestart,
247
+ allowFileTransfer: allowMutation && normalized.filesAudio.allowFileTransfer,
248
+ allowRemoteAudio: accessMode !== 'block-remote-access' && normalized.filesAudio.allowRemoteAudio,
249
+ allowAgent: allowMutation && normalized.agent.enabled
250
+ };
251
+ }