@livedesk/hub 0.1.47 → 0.1.48
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/package.json +2 -2
- package/src/filesystem/roots.js +20 -2
- package/src/remote-clipboard-contract.mjs +482 -0
- package/src/remote-hub.js +1140 -221
- package/src/server.js +137 -9
- package/src/settings/effective-device-policy.js +31 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "LiveDesk local Hub API and browser frame bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "node src/server.js",
|
|
13
13
|
"start": "node src/server.js",
|
|
14
|
-
"check": "node --check src/server.js && node --check src/remote-hub.js && node --check src/console-relay.js",
|
|
14
|
+
"check": "node --check src/server.js && node --check src/remote-hub.js && node --check src/remote-clipboard-contract.mjs && node --check src/console-relay.js",
|
|
15
15
|
"prepublishOnly": "node ../../scripts/livedesk-release-git-gate.mjs"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
package/src/filesystem/roots.js
CHANGED
|
@@ -98,8 +98,26 @@ async function mountedRoots() {
|
|
|
98
98
|
return roots;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
export async function discoverFilesystemRoots() {
|
|
102
|
-
const
|
|
101
|
+
export async function discoverFilesystemRoots() {
|
|
102
|
+
const isolatedTestRoot = process.env.LIVEDESK_TEST_MODE === '1'
|
|
103
|
+
? normalizeRootPath(process.env.LIVEDESK_TEST_FILESYSTEM_ROOT)
|
|
104
|
+
: '';
|
|
105
|
+
if (isolatedTestRoot) {
|
|
106
|
+
const rootStat = await fs.stat(isolatedTestRoot);
|
|
107
|
+
if (!rootStat.isDirectory()) {
|
|
108
|
+
throw new Error('LIVEDESK_TEST_FILESYSTEM_ROOT must be a directory.');
|
|
109
|
+
}
|
|
110
|
+
return [{
|
|
111
|
+
path: isolatedTestRoot,
|
|
112
|
+
name: 'Test drive',
|
|
113
|
+
displayPath: isolatedTestRoot,
|
|
114
|
+
type: 'drive',
|
|
115
|
+
driveType: 'fixed',
|
|
116
|
+
...await statfsCapacity(isolatedTestRoot),
|
|
117
|
+
hasChildren: true
|
|
118
|
+
}];
|
|
119
|
+
}
|
|
120
|
+
const roots = process.platform === 'win32' ? await windowsRoots() : await mountedRoots();
|
|
103
121
|
if (roots.length > 0) return roots;
|
|
104
122
|
const fallback = process.platform === 'win32' ? `${process.env.SystemDrive || 'C:'}\\` : os.homedir();
|
|
105
123
|
return [{
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
const MIB = 1024 * 1024;
|
|
2
|
+
|
|
3
|
+
export const REMOTE_CLIPBOARD_PROTOCOL = 'livedesk.clipboard.v1';
|
|
4
|
+
|
|
5
|
+
export const REMOTE_CLIPBOARD_LIMITS = Object.freeze({
|
|
6
|
+
maxTextBytes: 1 * MIB,
|
|
7
|
+
maxImageBytes: 24 * MIB,
|
|
8
|
+
maxTotalBytes: 256 * MIB,
|
|
9
|
+
maxFiles: 24,
|
|
10
|
+
// 384,000 raw bytes encode to 512,000 base64 characters, leaving bounded
|
|
11
|
+
// room for the command envelope below Relay's 512 KiB JSON-message limit.
|
|
12
|
+
maxChunkBytes: 384_000,
|
|
13
|
+
maxOperationsPerDevice: 2,
|
|
14
|
+
operationIdleTimeoutMs: 2 * 60 * 1000,
|
|
15
|
+
commandTimeoutMs: 15 * 1000
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const REMOTE_CLIPBOARD_INPUT_ACTIONS = Object.freeze(new Set([
|
|
19
|
+
'clipboard.prepare',
|
|
20
|
+
'clipboard.copy',
|
|
21
|
+
'clipboard.paste'
|
|
22
|
+
]));
|
|
23
|
+
|
|
24
|
+
export const REMOTE_CLIPBOARD_FILE_ACTIONS = Object.freeze(new Set([
|
|
25
|
+
'clipboard.copy.begin',
|
|
26
|
+
'clipboard.copy.chunk',
|
|
27
|
+
'clipboard.copy.end',
|
|
28
|
+
'clipboard.paste.begin',
|
|
29
|
+
'clipboard.paste.chunk',
|
|
30
|
+
'clipboard.paste.cancel',
|
|
31
|
+
'clipboard.operation.status'
|
|
32
|
+
]));
|
|
33
|
+
|
|
34
|
+
const ACTION_ALIASES = new Map([
|
|
35
|
+
['prepare', 'clipboard.prepare'],
|
|
36
|
+
['copy', 'clipboard.copy'],
|
|
37
|
+
['paste', 'clipboard.paste'],
|
|
38
|
+
['copy.begin', 'clipboard.copy.begin'],
|
|
39
|
+
['copy.chunk', 'clipboard.copy.chunk'],
|
|
40
|
+
['copy.end', 'clipboard.copy.end'],
|
|
41
|
+
['paste.begin', 'clipboard.paste.begin'],
|
|
42
|
+
['paste.chunk', 'clipboard.paste.chunk'],
|
|
43
|
+
['paste.cancel', 'clipboard.paste.cancel'],
|
|
44
|
+
['operation.status', 'clipboard.operation.status']
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const SAFE_STATUS_VALUES = new Set([
|
|
48
|
+
'preparing',
|
|
49
|
+
'copying',
|
|
50
|
+
'receiving',
|
|
51
|
+
'ready',
|
|
52
|
+
'completed',
|
|
53
|
+
'cancelled',
|
|
54
|
+
'cleaned',
|
|
55
|
+
'failed',
|
|
56
|
+
'expired',
|
|
57
|
+
'not-found',
|
|
58
|
+
'already-ended',
|
|
59
|
+
'complete',
|
|
60
|
+
'applied',
|
|
61
|
+
'pasted'
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
export class RemoteClipboardContractError extends Error {
|
|
65
|
+
constructor(code, status = 400) {
|
|
66
|
+
super(code);
|
|
67
|
+
this.name = 'RemoteClipboardContractError';
|
|
68
|
+
this.code = code;
|
|
69
|
+
this.status = status;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fail(code, status = 400) {
|
|
74
|
+
throw new RemoteClipboardContractError(code, status);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function boundedString(value, maxLength, code, { required = false } = {}) {
|
|
78
|
+
const text = String(value ?? '').trim();
|
|
79
|
+
if ((required && !text) || text.length > maxLength) {
|
|
80
|
+
fail(code);
|
|
81
|
+
}
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function exactInteger(value, { min = 0, max = Number.MAX_SAFE_INTEGER, code }) {
|
|
86
|
+
const number = Number(value);
|
|
87
|
+
if (!Number.isSafeInteger(number) || number < min || number > max) {
|
|
88
|
+
fail(code);
|
|
89
|
+
}
|
|
90
|
+
return number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function optionalInteger(value, options) {
|
|
94
|
+
if (value === undefined || value === null || value === '') {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return exactInteger(value, options);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeContentKind(value) {
|
|
101
|
+
const kind = String(value || '').trim().toLowerCase();
|
|
102
|
+
if (!['text', 'image', 'files'].includes(kind)) {
|
|
103
|
+
fail('clipboard-manifest-kind-invalid');
|
|
104
|
+
}
|
|
105
|
+
return kind;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeItemKind(value) {
|
|
109
|
+
const kind = String(value || '').trim().toLowerCase();
|
|
110
|
+
if (!['text', 'image', 'file'].includes(kind)) {
|
|
111
|
+
fail('clipboard-manifest-item-kind-invalid');
|
|
112
|
+
}
|
|
113
|
+
return kind;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeMimeType(value, itemKind) {
|
|
117
|
+
const fallback = itemKind === 'text'
|
|
118
|
+
? 'text/plain;charset=utf-8'
|
|
119
|
+
: itemKind === 'image'
|
|
120
|
+
? 'image/png'
|
|
121
|
+
: 'application/octet-stream';
|
|
122
|
+
const mimeType = boundedString(value || fallback, 160, 'clipboard-manifest-mime-invalid');
|
|
123
|
+
if (itemKind === 'text' && !mimeType.toLowerCase().startsWith('text/plain')) {
|
|
124
|
+
fail('clipboard-text-mime-required');
|
|
125
|
+
}
|
|
126
|
+
if (itemKind === 'image' && mimeType.toLowerCase() !== 'image/png') {
|
|
127
|
+
fail('clipboard-image-png-required');
|
|
128
|
+
}
|
|
129
|
+
return mimeType;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function normalizeRemoteClipboardOperationId(value) {
|
|
133
|
+
const operationId = boundedString(value, 128, 'clipboard-operation-id-invalid', { required: true });
|
|
134
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(operationId)) {
|
|
135
|
+
fail('clipboard-operation-id-invalid');
|
|
136
|
+
}
|
|
137
|
+
return operationId;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function normalizeRemoteClipboardAction(value) {
|
|
141
|
+
const requested = String(value || '').trim().toLowerCase();
|
|
142
|
+
const action = ACTION_ALIASES.get(requested) || requested;
|
|
143
|
+
if (!REMOTE_CLIPBOARD_INPUT_ACTIONS.has(action) && !REMOTE_CLIPBOARD_FILE_ACTIONS.has(action)) {
|
|
144
|
+
fail('clipboard-action-invalid');
|
|
145
|
+
}
|
|
146
|
+
return action;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function normalizeRemoteClipboardManifest(value, expectedOperationId = '') {
|
|
150
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
151
|
+
fail('clipboard-manifest-required');
|
|
152
|
+
}
|
|
153
|
+
const operationId = normalizeRemoteClipboardOperationId(
|
|
154
|
+
value.operationId || value.clipboardOperationId || expectedOperationId);
|
|
155
|
+
if (expectedOperationId && operationId !== expectedOperationId) {
|
|
156
|
+
fail('clipboard-operation-id-mismatch');
|
|
157
|
+
}
|
|
158
|
+
const contentKind = normalizeContentKind(value.contentKind || value.kind);
|
|
159
|
+
const rawItems = Array.isArray(value.items) ? value.items : [];
|
|
160
|
+
if (rawItems.length === 0 || rawItems.length > REMOTE_CLIPBOARD_LIMITS.maxFiles) {
|
|
161
|
+
fail('clipboard-manifest-item-count-invalid');
|
|
162
|
+
}
|
|
163
|
+
const expectedItemKind = contentKind === 'files' ? 'file' : contentKind;
|
|
164
|
+
if (contentKind !== 'files' && rawItems.length !== 1) {
|
|
165
|
+
fail('clipboard-single-item-required');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let totalBytes = 0;
|
|
169
|
+
const items = rawItems.map((rawItem, itemIndex) => {
|
|
170
|
+
const item = rawItem && typeof rawItem === 'object' && !Array.isArray(rawItem) ? rawItem : {};
|
|
171
|
+
const normalizedIndex = exactInteger(item.itemIndex, {
|
|
172
|
+
min: 0,
|
|
173
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxFiles - 1,
|
|
174
|
+
code: 'clipboard-manifest-item-index-invalid'
|
|
175
|
+
});
|
|
176
|
+
if (normalizedIndex !== itemIndex) {
|
|
177
|
+
fail('clipboard-manifest-item-index-invalid');
|
|
178
|
+
}
|
|
179
|
+
const kind = normalizeItemKind(item.kind);
|
|
180
|
+
if (kind !== expectedItemKind) {
|
|
181
|
+
fail('clipboard-manifest-item-kind-mismatch');
|
|
182
|
+
}
|
|
183
|
+
const size = exactInteger(item.size, {
|
|
184
|
+
min: 0,
|
|
185
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxTotalBytes,
|
|
186
|
+
code: 'clipboard-manifest-item-size-invalid'
|
|
187
|
+
});
|
|
188
|
+
if (kind !== 'file' && size === 0) {
|
|
189
|
+
fail('clipboard-manifest-item-size-invalid');
|
|
190
|
+
}
|
|
191
|
+
if (kind === 'text' && size > REMOTE_CLIPBOARD_LIMITS.maxTextBytes) {
|
|
192
|
+
fail('clipboard-text-too-large', 413);
|
|
193
|
+
}
|
|
194
|
+
if (kind === 'image' && size > REMOTE_CLIPBOARD_LIMITS.maxImageBytes) {
|
|
195
|
+
fail('clipboard-image-too-large', 413);
|
|
196
|
+
}
|
|
197
|
+
totalBytes += size;
|
|
198
|
+
if (totalBytes > REMOTE_CLIPBOARD_LIMITS.maxTotalBytes) {
|
|
199
|
+
fail('clipboard-total-too-large', 413);
|
|
200
|
+
}
|
|
201
|
+
const storageName = boundedString(
|
|
202
|
+
item.storageName,
|
|
203
|
+
64,
|
|
204
|
+
'clipboard-manifest-storage-name-invalid',
|
|
205
|
+
{ required: true });
|
|
206
|
+
if (storageName !== `item-${itemIndex}.bin`) {
|
|
207
|
+
fail('clipboard-manifest-storage-name-invalid');
|
|
208
|
+
}
|
|
209
|
+
const sha256 = boundedString(item.sha256, 64, 'clipboard-manifest-sha256-invalid', { required: true }).toLowerCase();
|
|
210
|
+
if (!/^[a-f0-9]{64}$/.test(sha256)) {
|
|
211
|
+
fail('clipboard-manifest-sha256-invalid');
|
|
212
|
+
}
|
|
213
|
+
const name = boundedString(
|
|
214
|
+
item.name || (kind === 'text' ? 'clipboard.txt' : kind === 'image' ? 'clipboard.png' : ''),
|
|
215
|
+
240,
|
|
216
|
+
'clipboard-manifest-name-invalid',
|
|
217
|
+
{ required: true });
|
|
218
|
+
if (/[/\\\u0000-\u001f]/.test(name) || name === '.' || name === '..') {
|
|
219
|
+
fail('clipboard-manifest-name-invalid');
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
itemIndex,
|
|
223
|
+
kind,
|
|
224
|
+
name,
|
|
225
|
+
mimeType: normalizeMimeType(item.mimeType, kind),
|
|
226
|
+
size,
|
|
227
|
+
lastModified: optionalInteger(item.lastModified, {
|
|
228
|
+
min: 0,
|
|
229
|
+
max: 8_640_000_000_000_000,
|
|
230
|
+
code: 'clipboard-manifest-last-modified-invalid'
|
|
231
|
+
}) || 0,
|
|
232
|
+
sha256,
|
|
233
|
+
storageName
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const declaredTotalBytes = optionalInteger(value.totalBytes, {
|
|
238
|
+
min: 0,
|
|
239
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxTotalBytes,
|
|
240
|
+
code: 'clipboard-manifest-total-invalid'
|
|
241
|
+
});
|
|
242
|
+
if (declaredTotalBytes !== undefined && declaredTotalBytes !== totalBytes) {
|
|
243
|
+
fail('clipboard-manifest-total-mismatch');
|
|
244
|
+
}
|
|
245
|
+
return { operationId, contentKind, totalBytes, items };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function normalizeRemoteClipboardBase64(value, maxBytes = REMOTE_CLIPBOARD_LIMITS.maxChunkBytes) {
|
|
249
|
+
const encoded = String(value || '').trim();
|
|
250
|
+
if (encoded.length > Math.ceil(maxBytes / 3) * 4 + 4 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) {
|
|
251
|
+
fail('clipboard-chunk-base64-invalid');
|
|
252
|
+
}
|
|
253
|
+
const bytes = Buffer.from(encoded, 'base64');
|
|
254
|
+
if (bytes.length > maxBytes) {
|
|
255
|
+
fail('clipboard-chunk-too-large', 413);
|
|
256
|
+
}
|
|
257
|
+
if (bytes.toString('base64') !== encoded) {
|
|
258
|
+
fail('clipboard-chunk-base64-invalid');
|
|
259
|
+
}
|
|
260
|
+
return { dataBase64: encoded, byteLength: bytes.length };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function normalizeBinding(body) {
|
|
264
|
+
const binding = body?.binding && typeof body.binding === 'object' && !Array.isArray(body.binding)
|
|
265
|
+
? body.binding
|
|
266
|
+
: {};
|
|
267
|
+
return {
|
|
268
|
+
controlSessionId: boundedString(
|
|
269
|
+
binding.sessionId || binding.controlSessionId || body.controlSessionId || body.sessionId,
|
|
270
|
+
160,
|
|
271
|
+
'clipboard-control-session-required',
|
|
272
|
+
{ required: true }),
|
|
273
|
+
controlCommandId: boundedString(
|
|
274
|
+
binding.commandId || binding.controlCommandId || body.controlCommandId || body.commandId,
|
|
275
|
+
128,
|
|
276
|
+
'clipboard-control-command-required',
|
|
277
|
+
{ required: true }),
|
|
278
|
+
captureGeneration: exactInteger(
|
|
279
|
+
binding.captureGeneration ?? body.captureGeneration,
|
|
280
|
+
{ min: 1, max: Number.MAX_SAFE_INTEGER, code: 'clipboard-capture-generation-required' }),
|
|
281
|
+
monitorIndex: exactInteger(
|
|
282
|
+
binding.monitorIndex ?? body.monitorIndex,
|
|
283
|
+
{ min: 0, max: 63, code: 'clipboard-monitor-index-required' })
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeClipboardPayload(action, rawPayload, operationId) {
|
|
288
|
+
const payload = rawPayload && typeof rawPayload === 'object' && !Array.isArray(rawPayload)
|
|
289
|
+
? rawPayload
|
|
290
|
+
: {};
|
|
291
|
+
if (action === 'clipboard.paste.begin') {
|
|
292
|
+
return { manifest: normalizeRemoteClipboardManifest(payload.manifest || payload, operationId) };
|
|
293
|
+
}
|
|
294
|
+
if (action === 'clipboard.copy.chunk') {
|
|
295
|
+
return {
|
|
296
|
+
itemIndex: exactInteger(payload.itemIndex, {
|
|
297
|
+
min: 0,
|
|
298
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxFiles - 1,
|
|
299
|
+
code: 'clipboard-item-index-invalid'
|
|
300
|
+
}),
|
|
301
|
+
offset: exactInteger(payload.offset, {
|
|
302
|
+
min: 0,
|
|
303
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxTotalBytes,
|
|
304
|
+
code: 'clipboard-chunk-offset-invalid'
|
|
305
|
+
}),
|
|
306
|
+
maxBytes: exactInteger(payload.maxBytes ?? REMOTE_CLIPBOARD_LIMITS.maxChunkBytes, {
|
|
307
|
+
min: 1,
|
|
308
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxChunkBytes,
|
|
309
|
+
code: 'clipboard-chunk-max-bytes-invalid'
|
|
310
|
+
})
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (action === 'clipboard.paste.chunk') {
|
|
314
|
+
const chunk = normalizeRemoteClipboardBase64(payload.dataBase64);
|
|
315
|
+
return {
|
|
316
|
+
itemIndex: exactInteger(payload.itemIndex, {
|
|
317
|
+
min: 0,
|
|
318
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxFiles - 1,
|
|
319
|
+
code: 'clipboard-item-index-invalid'
|
|
320
|
+
}),
|
|
321
|
+
offset: exactInteger(payload.offset, {
|
|
322
|
+
min: 0,
|
|
323
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxTotalBytes,
|
|
324
|
+
code: 'clipboard-chunk-offset-invalid'
|
|
325
|
+
}),
|
|
326
|
+
dataBase64: chunk.dataBase64,
|
|
327
|
+
final: payload.final === true
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const normalized = {};
|
|
331
|
+
const manifestValue = payload.manifest;
|
|
332
|
+
if (manifestValue) {
|
|
333
|
+
normalized.manifest = normalizeRemoteClipboardManifest(manifestValue, operationId);
|
|
334
|
+
}
|
|
335
|
+
const requestedKind = payload.contentKind || payload.kind;
|
|
336
|
+
if (requestedKind) {
|
|
337
|
+
normalized.contentKind = normalizeContentKind(requestedKind);
|
|
338
|
+
}
|
|
339
|
+
return normalized;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function normalizeRemoteClipboardRequest(value) {
|
|
343
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
344
|
+
fail('clipboard-request-invalid');
|
|
345
|
+
}
|
|
346
|
+
const action = normalizeRemoteClipboardAction(value.action || value.type);
|
|
347
|
+
const operationId = normalizeRemoteClipboardOperationId(
|
|
348
|
+
value.operationId || value.clipboardOperationId || value.op?.operationId || value.op);
|
|
349
|
+
const hubConnectionId = boundedString(
|
|
350
|
+
value.hubConnectionId || value.binding?.hubConnectionId,
|
|
351
|
+
128,
|
|
352
|
+
'clipboard-input-owner-required',
|
|
353
|
+
{ required: true });
|
|
354
|
+
const clipboardDirection = action.includes('.paste')
|
|
355
|
+
? 'paste'
|
|
356
|
+
: action.includes('.copy')
|
|
357
|
+
? 'copy'
|
|
358
|
+
: String(value.clipboardDirection || value.payload?.clipboardDirection || '').trim().toLowerCase();
|
|
359
|
+
if (action === 'clipboard.prepare' && !['copy', 'paste'].includes(clipboardDirection)) {
|
|
360
|
+
fail('clipboard-direction-required');
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
action,
|
|
364
|
+
operationId,
|
|
365
|
+
hubConnectionId,
|
|
366
|
+
clipboardDirection,
|
|
367
|
+
binding: normalizeBinding(value),
|
|
368
|
+
payload: normalizeClipboardPayload(action, value.payload, operationId)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function remoteClipboardContentNeedsFileTransfer(contentKind) {
|
|
373
|
+
return contentKind === 'image' || contentKind === 'files';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function safeStatus(value) {
|
|
377
|
+
const status = String(value || '').trim().toLowerCase();
|
|
378
|
+
return SAFE_STATUS_VALUES.has(status) ? status : '';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function normalizeRemoteClipboardCommandResult(action, operationId, value) {
|
|
382
|
+
const result = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
383
|
+
const returnedOperationId = result.operationId || result.clipboardOperationId;
|
|
384
|
+
if (returnedOperationId && normalizeRemoteClipboardOperationId(returnedOperationId) !== operationId) {
|
|
385
|
+
fail('clipboard-operation-id-mismatch', 409);
|
|
386
|
+
}
|
|
387
|
+
const response = {
|
|
388
|
+
ok: result.ok !== false,
|
|
389
|
+
action,
|
|
390
|
+
operationId,
|
|
391
|
+
status: safeStatus(result.status) || (result.ok === false ? 'failed' : '')
|
|
392
|
+
};
|
|
393
|
+
if (action === 'clipboard.copy.begin') {
|
|
394
|
+
response.manifest = normalizeRemoteClipboardManifest(result.manifest, operationId);
|
|
395
|
+
response.kind = response.manifest.contentKind;
|
|
396
|
+
response.status ||= 'ready';
|
|
397
|
+
return response;
|
|
398
|
+
}
|
|
399
|
+
if (action === 'clipboard.copy.chunk') {
|
|
400
|
+
const chunk = normalizeRemoteClipboardBase64(result.dataBase64);
|
|
401
|
+
const itemIndex = exactInteger(result.itemIndex, {
|
|
402
|
+
min: 0,
|
|
403
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxFiles - 1,
|
|
404
|
+
code: 'clipboard-item-index-invalid'
|
|
405
|
+
});
|
|
406
|
+
const offset = exactInteger(result.offset, {
|
|
407
|
+
min: 0,
|
|
408
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxTotalBytes,
|
|
409
|
+
code: 'clipboard-chunk-offset-invalid'
|
|
410
|
+
});
|
|
411
|
+
const bytes = exactInteger(result.bytes ?? chunk.byteLength, {
|
|
412
|
+
min: 0,
|
|
413
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxChunkBytes,
|
|
414
|
+
code: 'clipboard-chunk-byte-count-invalid'
|
|
415
|
+
});
|
|
416
|
+
if (bytes !== chunk.byteLength) {
|
|
417
|
+
fail('clipboard-chunk-byte-count-mismatch');
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
ok: result.ok !== false,
|
|
421
|
+
action,
|
|
422
|
+
operationId,
|
|
423
|
+
itemIndex,
|
|
424
|
+
offset,
|
|
425
|
+
bytes,
|
|
426
|
+
totalBytes: exactInteger(result.totalBytes, {
|
|
427
|
+
min: 0,
|
|
428
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxTotalBytes,
|
|
429
|
+
code: 'clipboard-chunk-total-invalid'
|
|
430
|
+
}),
|
|
431
|
+
final: result.final === true,
|
|
432
|
+
dataBase64: chunk.dataBase64
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (action === 'clipboard.paste.chunk') {
|
|
436
|
+
return {
|
|
437
|
+
ok: result.ok !== false,
|
|
438
|
+
action,
|
|
439
|
+
operationId,
|
|
440
|
+
status: safeStatus(result.status) || '',
|
|
441
|
+
itemIndex: exactInteger(result.itemIndex, {
|
|
442
|
+
min: 0,
|
|
443
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxFiles - 1,
|
|
444
|
+
code: 'clipboard-item-index-invalid'
|
|
445
|
+
}),
|
|
446
|
+
offset: exactInteger(result.offset, {
|
|
447
|
+
min: 0,
|
|
448
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxTotalBytes,
|
|
449
|
+
code: 'clipboard-chunk-offset-invalid'
|
|
450
|
+
}),
|
|
451
|
+
bytes: exactInteger(result.bytes, {
|
|
452
|
+
min: 0,
|
|
453
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxChunkBytes,
|
|
454
|
+
code: 'clipboard-chunk-byte-count-invalid'
|
|
455
|
+
}),
|
|
456
|
+
totalBytes: exactInteger(result.totalBytes, {
|
|
457
|
+
min: 0,
|
|
458
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxTotalBytes,
|
|
459
|
+
code: 'clipboard-chunk-total-invalid'
|
|
460
|
+
}),
|
|
461
|
+
final: result.final === true
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (result.manifest) {
|
|
465
|
+
response.manifest = normalizeRemoteClipboardManifest(result.manifest, operationId);
|
|
466
|
+
response.kind = response.manifest.contentKind;
|
|
467
|
+
} else {
|
|
468
|
+
const kind = result.contentKind || result.kind;
|
|
469
|
+
if (kind) response.kind = normalizeContentKind(kind);
|
|
470
|
+
}
|
|
471
|
+
for (const key of ['receivedBytes', 'totalBytes']) {
|
|
472
|
+
if (result[key] !== undefined) {
|
|
473
|
+
response[key] = exactInteger(result[key], {
|
|
474
|
+
min: 0,
|
|
475
|
+
max: REMOTE_CLIPBOARD_LIMITS.maxTotalBytes,
|
|
476
|
+
code: `clipboard-${key}-invalid`
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (result.ready !== undefined) response.ready = result.ready === true;
|
|
481
|
+
return response;
|
|
482
|
+
}
|