@mindexec/cli 0.2.44 → 0.2.46
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/codex-runtime.js +75 -3
- package/package.json +1 -1
- package/remote-hub.js +119 -12
- package/scripts/remote-http-smoke.mjs +54 -0
- package/scripts/remote-hub-smoke.mjs +42 -0
- package/server.js +115 -23
- package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
- package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +19 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +249 -22
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +2 -0
- package/wwwroot/_framework/MindExecution.Core.xg9yy9l5dz.dll +0 -0
- package/wwwroot/_framework/{MindExecution.Kernel.z56elxihok.dll → MindExecution.Kernel.erg96341xf.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Admin.p5cs4ap87v.dll → MindExecution.Plugins.Admin.11j9vpdm9u.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Business.s35og5uz44.dll → MindExecution.Plugins.Business.oyskf08knn.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.ueuo23qx6f.dll → MindExecution.Plugins.Concept.keia4ox68c.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Directory.y74f55e8x3.dll → MindExecution.Plugins.Directory.7pus9p63ym.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.lhbyievfnk.dll → MindExecution.Plugins.PlanMaster.wr3pupzfyo.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.y87u77w5nn.dll → MindExecution.Plugins.YouTube.kpfew1eggc.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.3kkptsi9lw.dll → MindExecution.Shared.kzibxbai3y.dll} +0 -0
- package/wwwroot/_framework/MindExecution.Web.6fjnkr9ty4.dll +0 -0
- package/wwwroot/_framework/blazor.boot.json +21 -21
- package/wwwroot/service-worker-assets.js +26 -26
- package/wwwroot/service-worker.js +1 -1
- package/wwwroot/_framework/MindExecution.Core.6rfnfdndxq.dll +0 -0
- package/wwwroot/_framework/MindExecution.Web.4ddj83yo5w.dll +0 -0
package/codex-runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs, readFileSync } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import { exec, spawn } from 'child_process';
|
|
4
5
|
import { promisify } from 'util';
|
|
5
6
|
import crypto from 'crypto';
|
|
@@ -36,6 +37,7 @@ const DEFAULT_TIMEOUT_MS = 8 * 60 * 1000;
|
|
|
36
37
|
const MAX_LOG_CHARS = 4000;
|
|
37
38
|
const MAX_EVENT_LOG = 120;
|
|
38
39
|
const TEMP_DIR = '.ai/codex';
|
|
40
|
+
const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
|
|
39
41
|
|
|
40
42
|
let cachedSdkModule = null;
|
|
41
43
|
let cachedSdkLoadError = null;
|
|
@@ -88,6 +90,75 @@ function normalizeProviderKind(value) {
|
|
|
88
90
|
return '';
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
function isEnabledEnv(value) {
|
|
94
|
+
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function unquoteTomlKey(value) {
|
|
98
|
+
const text = String(value || '').trim();
|
|
99
|
+
if (text.length >= 2 && ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'")))) {
|
|
100
|
+
return text.slice(1, -1).replace(/\\(["\\])/g, '$1').trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readConfiguredMcpServerNames() {
|
|
107
|
+
if (isEnabledEnv(process.env.MINDEXEC_CODEX_INHERIT_MCP)) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const names = new Set();
|
|
112
|
+
try {
|
|
113
|
+
const content = readFileSync(CODEX_CONFIG_PATH, 'utf8');
|
|
114
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
115
|
+
const line = rawLine.trim();
|
|
116
|
+
const match = line.match(/^\[mcp_servers\.((?:"(?:[^"\\]|\\.)+"|'(?:[^'\\]|\\.)+'|[^\]\s]+))\]$/);
|
|
117
|
+
if (!match) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const name = unquoteTomlKey(match[1]);
|
|
122
|
+
if (/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
123
|
+
names.add(name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Missing Codex config is fine; SDK auth still reports its own error if login is required.
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const name of String(process.env.MINDEXEC_CODEX_DISABLED_MCP_SERVERS || 'cloudflare,supabase')
|
|
131
|
+
.split(',')
|
|
132
|
+
.map(item => item.trim())
|
|
133
|
+
.filter(Boolean)) {
|
|
134
|
+
if (/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
135
|
+
names.add(name);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Array.from(names).sort();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildCodexSdkConfigOverrides() {
|
|
143
|
+
const mcpServerNames = readConfiguredMcpServerNames();
|
|
144
|
+
if (mcpServerNames.length === 0) {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const mcpServers = {};
|
|
149
|
+
for (const name of mcpServerNames) {
|
|
150
|
+
mcpServers[name] = { enabled: false };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { mcp_servers: mcpServers };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function appendCodexIsolationConfigArgs(args) {
|
|
157
|
+
for (const name of readConfiguredMcpServerNames()) {
|
|
158
|
+
args.push('--config', `mcp_servers.${name}.enabled=false`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
91
162
|
function normalizeReasoningEffort(value) {
|
|
92
163
|
const normalized = String(value || '').trim().toLowerCase();
|
|
93
164
|
return ['minimal', 'low', 'medium', 'high', 'xhigh'].includes(normalized)
|
|
@@ -491,7 +562,7 @@ export function createCodexRuntime(options) {
|
|
|
491
562
|
|
|
492
563
|
if (providerKind === PROVIDER_KIND.typeScriptSdk) {
|
|
493
564
|
const sdk = await loadCodexSdk();
|
|
494
|
-
const codex = new sdk.Codex();
|
|
565
|
+
const codex = new sdk.Codex({ config: buildCodexSdkConfigOverrides() });
|
|
495
566
|
const thread = codex.startThread(threadOptions);
|
|
496
567
|
const localThreadId = `local_${crypto.randomUUID()}`;
|
|
497
568
|
threads.set(localThreadId, {
|
|
@@ -553,7 +624,7 @@ export function createCodexRuntime(options) {
|
|
|
553
624
|
}
|
|
554
625
|
|
|
555
626
|
const sdk = await loadCodexSdk();
|
|
556
|
-
const codex = new sdk.Codex();
|
|
627
|
+
const codex = new sdk.Codex({ config: buildCodexSdkConfigOverrides() });
|
|
557
628
|
const officialId = requestedThreadId && !requestedThreadId.startsWith('local_')
|
|
558
629
|
? requestedThreadId
|
|
559
630
|
: '';
|
|
@@ -708,6 +779,7 @@ export function createCodexRuntime(options) {
|
|
|
708
779
|
'--config',
|
|
709
780
|
`model_reasoning_effort=${threadOptions.modelReasoningEffort}`
|
|
710
781
|
];
|
|
782
|
+
appendCodexIsolationConfigArgs(args);
|
|
711
783
|
|
|
712
784
|
if (threadOptions.model) {
|
|
713
785
|
args.push('-m', threadOptions.model);
|
|
@@ -863,7 +935,7 @@ export function createCodexRuntime(options) {
|
|
|
863
935
|
const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
|
|
864
936
|
const threadOptions = buildThreadOptions(body, workingDirectory);
|
|
865
937
|
const sdk = await loadCodexSdk();
|
|
866
|
-
const codex = new sdk.Codex();
|
|
938
|
+
const codex = new sdk.Codex({ config: buildCodexSdkConfigOverrides() });
|
|
867
939
|
const thread = codex.resumeThread(threadId, threadOptions);
|
|
868
940
|
threads.set(threadId, {
|
|
869
941
|
providerKind,
|
package/package.json
CHANGED
package/remote-hub.js
CHANGED
|
@@ -19,6 +19,7 @@ const REMOTE_PROTOCOL_VERSION = 1;
|
|
|
19
19
|
const MAX_SYNTHETIC_DEVICES = 1000;
|
|
20
20
|
const DEFAULT_HOST_TARGET_LEASE_MS = 30000;
|
|
21
21
|
const SYNTHETIC_FRAME_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+KAAAADElEQVR42mP8z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC';
|
|
22
|
+
const SYNTHETIC_FRAME_PAYLOAD = Buffer.from(SYNTHETIC_FRAME_DATA_URL.split(',')[1], 'base64');
|
|
22
23
|
|
|
23
24
|
function isEnabledValue(value, fallback = true) {
|
|
24
25
|
if (value === undefined || value === null || value === '') {
|
|
@@ -122,7 +123,52 @@ function parseJsonLine(line) {
|
|
|
122
123
|
return JSON.parse(text);
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
function
|
|
126
|
+
function createFrameAccessToken() {
|
|
127
|
+
return crypto.randomBytes(18).toString('base64url');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildRemoteFramePath(deviceId, frameKind, frame) {
|
|
131
|
+
const token = safeString(frame?.accessToken, 128);
|
|
132
|
+
const frameSeq = Number(frame?.frameSeq);
|
|
133
|
+
if (!deviceId || !token || !Number.isFinite(frameSeq)) {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const endpoint = frameKind === 'thumbnail' ? 'thumbnail' : 'live/frame';
|
|
138
|
+
const params = new URLSearchParams({
|
|
139
|
+
format: 'binary',
|
|
140
|
+
seq: String(Math.floor(frameSeq)),
|
|
141
|
+
token
|
|
142
|
+
});
|
|
143
|
+
return `/api/remote/devices/${encodeURIComponent(deviceId)}/${endpoint}?${params.toString()}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function serializeRemoteFrame(frame, deviceId, frameKind, options = {}) {
|
|
147
|
+
if (!frame) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const {
|
|
152
|
+
payload,
|
|
153
|
+
accessToken,
|
|
154
|
+
dataUrl,
|
|
155
|
+
...publicFrame
|
|
156
|
+
} = frame;
|
|
157
|
+
|
|
158
|
+
const framePath = buildRemoteFramePath(deviceId, frameKind, frame);
|
|
159
|
+
const serialized = {
|
|
160
|
+
...publicFrame,
|
|
161
|
+
framePath,
|
|
162
|
+
frameUrl: framePath
|
|
163
|
+
};
|
|
164
|
+
if (options.includeDataUrl !== false) {
|
|
165
|
+
serialized.dataUrl = dataUrl;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return serialized;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function serializeDevice(device, options = {}) {
|
|
126
172
|
if (!device) {
|
|
127
173
|
return null;
|
|
128
174
|
}
|
|
@@ -145,8 +191,8 @@ function serializeDevice(device) {
|
|
|
145
191
|
lastDisconnectReason: device.lastDisconnectReason,
|
|
146
192
|
remoteAddress: device.remoteAddress,
|
|
147
193
|
remotePort: device.remotePort,
|
|
148
|
-
latestThumbnail: device.latestThumbnail
|
|
149
|
-
latestLiveFrame: device.latestLiveFrame
|
|
194
|
+
latestThumbnail: serializeRemoteFrame(device.latestThumbnail, device.deviceId, 'thumbnail', options),
|
|
195
|
+
latestLiveFrame: serializeRemoteFrame(device.latestLiveFrame, device.deviceId, 'live', options),
|
|
150
196
|
activeLiveStream: device.activeLiveStream ? { ...device.activeLiveStream } : null,
|
|
151
197
|
latestTask: device.latestTask ? { ...device.latestTask } : null,
|
|
152
198
|
synthetic: device.synthetic === true,
|
|
@@ -376,9 +422,12 @@ export function createRemoteHub(options = {}) {
|
|
|
376
422
|
};
|
|
377
423
|
}
|
|
378
424
|
|
|
379
|
-
function listDevices() {
|
|
425
|
+
function listDevices(options = {}) {
|
|
426
|
+
const serializeOptions = {
|
|
427
|
+
includeDataUrl: options.includeDataUrl !== false
|
|
428
|
+
};
|
|
380
429
|
return [...devices.values()]
|
|
381
|
-
.map(serializeDevice)
|
|
430
|
+
.map(device => serializeDevice(device, serializeOptions))
|
|
382
431
|
.filter(Boolean)
|
|
383
432
|
.sort((a, b) => {
|
|
384
433
|
const nameCompare = String(a.deviceName || '').localeCompare(String(b.deviceName || ''));
|
|
@@ -427,7 +476,10 @@ export function createRemoteHub(options = {}) {
|
|
|
427
476
|
capturedAt: now,
|
|
428
477
|
receivedAt: now,
|
|
429
478
|
byteLength: 68,
|
|
430
|
-
|
|
479
|
+
transport: 'synthetic',
|
|
480
|
+
dataUrl: SYNTHETIC_FRAME_DATA_URL,
|
|
481
|
+
payload: SYNTHETIC_FRAME_PAYLOAD,
|
|
482
|
+
accessToken: createFrameAccessToken()
|
|
431
483
|
};
|
|
432
484
|
}
|
|
433
485
|
|
|
@@ -1095,6 +1147,19 @@ export function createRemoteHub(options = {}) {
|
|
|
1095
1147
|
: `data:${mimeType};base64,${frameData}`;
|
|
1096
1148
|
}
|
|
1097
1149
|
|
|
1150
|
+
function buildFramePayloadBuffer(framePayload, frameData) {
|
|
1151
|
+
if (Buffer.isBuffer(framePayload)) {
|
|
1152
|
+
return Buffer.from(framePayload);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const raw = String(frameData || '');
|
|
1156
|
+
const commaIndex = raw.indexOf(',');
|
|
1157
|
+
const base64 = raw.startsWith('data:') && commaIndex >= 0
|
|
1158
|
+
? raw.slice(commaIndex + 1)
|
|
1159
|
+
: raw;
|
|
1160
|
+
return Buffer.from(base64, 'base64');
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1098
1163
|
function applyThumbnailFrame(device, message, framePayload, transport = 'json-base64') {
|
|
1099
1164
|
const frameData = Buffer.isBuffer(framePayload)
|
|
1100
1165
|
? ''
|
|
@@ -1115,6 +1180,7 @@ export function createRemoteHub(options = {}) {
|
|
|
1115
1180
|
|
|
1116
1181
|
const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
|
|
1117
1182
|
const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
|
|
1183
|
+
const payload = buildFramePayloadBuffer(framePayload, frameData);
|
|
1118
1184
|
device.latestThumbnail = {
|
|
1119
1185
|
streamId: safeString(message.streamId, 128) || 'thumbnail',
|
|
1120
1186
|
frameSeq,
|
|
@@ -1127,7 +1193,9 @@ export function createRemoteHub(options = {}) {
|
|
|
1127
1193
|
receivedAt: device.lastSeenAt,
|
|
1128
1194
|
byteLength,
|
|
1129
1195
|
transport,
|
|
1130
|
-
dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType)
|
|
1196
|
+
dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType),
|
|
1197
|
+
payload,
|
|
1198
|
+
accessToken: createFrameAccessToken()
|
|
1131
1199
|
};
|
|
1132
1200
|
device.counters.thumbnailFramesReceived += 1;
|
|
1133
1201
|
emitRemoteEvent('RemoteFrameReceived', device, {
|
|
@@ -1173,6 +1241,7 @@ export function createRemoteHub(options = {}) {
|
|
|
1173
1241
|
|
|
1174
1242
|
const mimeType = safeString(message.mimeType || message.format || 'image/jpeg', 80) || 'image/jpeg';
|
|
1175
1243
|
const capturedAt = safeString(message.capturedAt, 80) || device.lastSeenAt;
|
|
1244
|
+
const payload = buildFramePayloadBuffer(framePayload, frameData);
|
|
1176
1245
|
device.latestLiveFrame = {
|
|
1177
1246
|
streamId,
|
|
1178
1247
|
frameSeq,
|
|
@@ -1187,7 +1256,9 @@ export function createRemoteHub(options = {}) {
|
|
|
1187
1256
|
receivedAt: device.lastSeenAt,
|
|
1188
1257
|
byteLength,
|
|
1189
1258
|
transport,
|
|
1190
|
-
dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType)
|
|
1259
|
+
dataUrl: buildFrameDataUrl(framePayload, frameData, mimeType),
|
|
1260
|
+
payload,
|
|
1261
|
+
accessToken: createFrameAccessToken()
|
|
1191
1262
|
};
|
|
1192
1263
|
device.activeLiveStream.lastFrameAt = device.lastSeenAt;
|
|
1193
1264
|
device.activeLiveStream.lastFrameSeq = frameSeq;
|
|
@@ -1904,14 +1975,49 @@ export function createRemoteHub(options = {}) {
|
|
|
1904
1975
|
return { ok: true, commandId, streamId };
|
|
1905
1976
|
}
|
|
1906
1977
|
|
|
1907
|
-
function getDeviceLiveFrame(deviceId) {
|
|
1978
|
+
function getDeviceLiveFrame(deviceId, options = {}) {
|
|
1908
1979
|
const device = devices.get(String(deviceId || ''));
|
|
1909
|
-
return device?.latestLiveFrame
|
|
1980
|
+
return serializeRemoteFrame(device?.latestLiveFrame, device?.deviceId, 'live', {
|
|
1981
|
+
includeDataUrl: options.includeDataUrl !== false
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function getDeviceThumbnail(deviceId, options = {}) {
|
|
1986
|
+
const device = devices.get(String(deviceId || ''));
|
|
1987
|
+
return serializeRemoteFrame(device?.latestThumbnail, device?.deviceId, 'thumbnail', {
|
|
1988
|
+
includeDataUrl: options.includeDataUrl !== false
|
|
1989
|
+
});
|
|
1910
1990
|
}
|
|
1911
1991
|
|
|
1912
|
-
function
|
|
1992
|
+
function getFramePayload(deviceId, frameKind, options = {}) {
|
|
1913
1993
|
const device = devices.get(String(deviceId || ''));
|
|
1914
|
-
|
|
1994
|
+
const frame = frameKind === 'thumbnail'
|
|
1995
|
+
? device?.latestThumbnail
|
|
1996
|
+
: device?.latestLiveFrame;
|
|
1997
|
+
if (!frame || !Buffer.isBuffer(frame.payload)) {
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
const requestedSeq = Number(options.frameSeq);
|
|
2002
|
+
if (Number.isFinite(requestedSeq) && Math.floor(requestedSeq) !== frame.frameSeq) {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
const token = safeString(options.token, 128);
|
|
2007
|
+
if (token && !timingSafeStringEqual(token, frame.accessToken)) {
|
|
2008
|
+
return null;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
if (options.requireToken === true && !token) {
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
return {
|
|
2016
|
+
frame: serializeRemoteFrame(frame, device.deviceId, frameKind, { includeDataUrl: false }),
|
|
2017
|
+
payload: frame.payload,
|
|
2018
|
+
mimeType: safeString(frame.mimeType || frame.format || 'application/octet-stream', 120) || 'application/octet-stream',
|
|
2019
|
+
byteLength: frame.payload.length
|
|
2020
|
+
};
|
|
1915
2021
|
}
|
|
1916
2022
|
|
|
1917
2023
|
return {
|
|
@@ -1931,6 +2037,7 @@ export function createRemoteHub(options = {}) {
|
|
|
1931
2037
|
stopLiveStream,
|
|
1932
2038
|
getDeviceLiveFrame,
|
|
1933
2039
|
getDeviceThumbnail,
|
|
2040
|
+
getFramePayload,
|
|
1934
2041
|
seedSyntheticFleet,
|
|
1935
2042
|
clearSyntheticFleet,
|
|
1936
2043
|
getPairToken: () => pairToken
|
|
@@ -54,6 +54,26 @@ async function fetchJson(url, options = {}) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
async function fetchBinary(url, options = {}) {
|
|
58
|
+
const response = await fetch(url, {
|
|
59
|
+
...options,
|
|
60
|
+
headers: {
|
|
61
|
+
Accept: 'image/png,image/jpeg,image/webp,image/*',
|
|
62
|
+
...(options.token ? { 'X-Bridge-Token': options.token } : {}),
|
|
63
|
+
...(options.headers || {})
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
status: response.status,
|
|
69
|
+
ok: response.ok,
|
|
70
|
+
contentType: response.headers.get('content-type') || '',
|
|
71
|
+
frameSeq: response.headers.get('x-mindexec-remote-frame-seq') || '',
|
|
72
|
+
streamId: response.headers.get('x-mindexec-remote-stream-id') || '',
|
|
73
|
+
payload: Buffer.from(await response.arrayBuffer())
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
async function waitForBridge(baseUrl, getFailureDetails, bridgeToken = BRIDGE_TOKEN) {
|
|
58
78
|
const startedAt = Date.now();
|
|
59
79
|
while (Date.now() - startedAt < 30000) {
|
|
@@ -339,6 +359,23 @@ async function runSyntheticEnabledSmoke() {
|
|
|
339
359
|
assert.equal(thumbnail.ok, true, JSON.stringify(thumbnail.payload));
|
|
340
360
|
assert.equal(thumbnail.payload?.thumbnail?.streamId, 'http-smoke-thumb');
|
|
341
361
|
assert.ok(String(thumbnail.payload?.thumbnail?.dataUrl || '').startsWith('data:image/png;base64,'));
|
|
362
|
+
assert.ok(String(thumbnail.payload?.thumbnail?.framePath || '').includes(`/api/remote/devices/${encodeURIComponent(thumbnailTarget.deviceId)}/thumbnail?`));
|
|
363
|
+
assert.equal(thumbnail.payload?.thumbnail?.frameUrl, thumbnail.payload?.thumbnail?.framePath);
|
|
364
|
+
|
|
365
|
+
const thumbnailBinary = await fetchBinary(`${baseUrl}${thumbnail.payload.thumbnail.framePath}`);
|
|
366
|
+
assert.equal(thumbnailBinary.ok, true, `${thumbnailBinary.status} ${thumbnailBinary.contentType}`);
|
|
367
|
+
assert.equal(thumbnailBinary.contentType, 'image/png');
|
|
368
|
+
assert.equal(thumbnailBinary.streamId, 'http-smoke-thumb');
|
|
369
|
+
assert.ok(thumbnailBinary.payload.length > 0);
|
|
370
|
+
|
|
371
|
+
const badThumbnailBinary = await fetchBinary(`${baseUrl}${thumbnail.payload.thumbnail.framePath.replace(/token=[^&]+/, 'token=wrong-token')}`);
|
|
372
|
+
assert.equal(badThumbnailBinary.status, 401);
|
|
373
|
+
|
|
374
|
+
const devicesUrlOnlyResult = await fetchJson(`${baseUrl}/api/remote/devices?frameData=url`, { token: BRIDGE_TOKEN });
|
|
375
|
+
assert.equal(devicesUrlOnlyResult.ok, true, JSON.stringify(devicesUrlOnlyResult.payload));
|
|
376
|
+
const urlOnlyThumbnailDevice = devicesUrlOnlyResult.payload?.devices?.find(device => device.deviceId === thumbnailTarget.deviceId);
|
|
377
|
+
assert.ok(urlOnlyThumbnailDevice?.latestThumbnail?.framePath);
|
|
378
|
+
assert.ok(!('dataUrl' in urlOnlyThumbnailDevice.latestThumbnail));
|
|
342
379
|
|
|
343
380
|
const liveTarget = devicesResult.payload.devices.find(device =>
|
|
344
381
|
device.connected && device.capabilities?.liveStream);
|
|
@@ -364,6 +401,23 @@ async function runSyntheticEnabledSmoke() {
|
|
|
364
401
|
assert.equal(liveFrame.payload?.frame?.streamId, 'http-smoke-live');
|
|
365
402
|
assert.equal(liveFrame.payload?.frame?.mode, 'remote-fast');
|
|
366
403
|
assert.equal(liveFrame.payload?.frame?.fps, 20);
|
|
404
|
+
assert.ok(String(liveFrame.payload?.frame?.dataUrl || '').startsWith('data:image/png;base64,'));
|
|
405
|
+
assert.ok(String(liveFrame.payload?.frame?.framePath || '').includes(`/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/frame?`));
|
|
406
|
+
|
|
407
|
+
const liveBinary = await fetchBinary(`${baseUrl}${liveFrame.payload.frame.framePath}`);
|
|
408
|
+
assert.equal(liveBinary.ok, true, `${liveBinary.status} ${liveBinary.contentType}`);
|
|
409
|
+
assert.equal(liveBinary.contentType, 'image/png');
|
|
410
|
+
assert.equal(liveBinary.streamId, 'http-smoke-live');
|
|
411
|
+
assert.ok(liveBinary.payload.length > 0);
|
|
412
|
+
|
|
413
|
+
const liveBinaryWithBridgeToken = await fetchBinary(
|
|
414
|
+
`${baseUrl}/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/frame?format=binary`,
|
|
415
|
+
{ token: BRIDGE_TOKEN });
|
|
416
|
+
assert.equal(liveBinaryWithBridgeToken.ok, true, `${liveBinaryWithBridgeToken.status} ${liveBinaryWithBridgeToken.contentType}`);
|
|
417
|
+
assert.equal(liveBinaryWithBridgeToken.contentType, 'image/png');
|
|
418
|
+
|
|
419
|
+
const badLiveSeq = await fetchBinary(`${baseUrl}${liveFrame.payload.frame.framePath.replace(/seq=\d+/, 'seq=999999')}`);
|
|
420
|
+
assert.equal(badLiveSeq.status, 401);
|
|
367
421
|
|
|
368
422
|
const liveStop = await fetchJson(`${baseUrl}/api/remote/devices/${encodeURIComponent(liveTarget.deviceId)}/live/stop`, {
|
|
369
423
|
method: 'POST',
|
|
@@ -152,6 +152,27 @@ try {
|
|
|
152
152
|
assert.equal(binaryThumbnailDevice.latestThumbnail.transport, 'binary');
|
|
153
153
|
assert.equal(binaryThumbnailDevice.latestThumbnail.byteLength, smokePngFrame.length);
|
|
154
154
|
assert.equal(binaryThumbnailDevice.counters.thumbnailFramesReceived, 2);
|
|
155
|
+
const serializedBinaryThumbnail = hub.getDeviceThumbnail('smoke-device', { includeDataUrl: false });
|
|
156
|
+
assert.equal(serializedBinaryThumbnail.streamId, 'smoke-thumb-binary');
|
|
157
|
+
assert.equal(serializedBinaryThumbnail.frameSeq, 4);
|
|
158
|
+
assert.ok(serializedBinaryThumbnail.framePath.includes('/api/remote/devices/smoke-device/thumbnail?'));
|
|
159
|
+
assert.ok(!('dataUrl' in serializedBinaryThumbnail));
|
|
160
|
+
assert.ok(!('payload' in serializedBinaryThumbnail));
|
|
161
|
+
assert.ok(!('accessToken' in serializedBinaryThumbnail));
|
|
162
|
+
const serializedThumbnailUrl = new URL(serializedBinaryThumbnail.framePath, 'http://127.0.0.1');
|
|
163
|
+
const thumbnailPayload = hub.getFramePayload('smoke-device', 'thumbnail', {
|
|
164
|
+
token: serializedThumbnailUrl.searchParams.get('token'),
|
|
165
|
+
frameSeq: serializedThumbnailUrl.searchParams.get('seq'),
|
|
166
|
+
requireToken: true
|
|
167
|
+
});
|
|
168
|
+
assert.equal(thumbnailPayload?.mimeType, 'image/png');
|
|
169
|
+
assert.equal(thumbnailPayload?.byteLength, smokePngFrame.length);
|
|
170
|
+
assert.equal(Buffer.compare(thumbnailPayload.payload, smokePngFrame), 0);
|
|
171
|
+
assert.equal(hub.getFramePayload('smoke-device', 'thumbnail', {
|
|
172
|
+
token: 'wrong-token',
|
|
173
|
+
frameSeq: 4,
|
|
174
|
+
requireToken: true
|
|
175
|
+
}), null);
|
|
155
176
|
|
|
156
177
|
const liveCommand = hub.startLiveStream('smoke-device', {
|
|
157
178
|
streamId: 'smoke-live',
|
|
@@ -204,6 +225,27 @@ try {
|
|
|
204
225
|
assert.equal(binaryLiveDevice.latestLiveFrame.transport, 'binary');
|
|
205
226
|
assert.equal(binaryLiveDevice.latestLiveFrame.byteLength, smokePngFrame.length);
|
|
206
227
|
assert.equal(binaryLiveDevice.counters.liveFramesReceived, 2);
|
|
228
|
+
const serializedBinaryLiveFrame = hub.getDeviceLiveFrame('smoke-device', { includeDataUrl: false });
|
|
229
|
+
assert.equal(serializedBinaryLiveFrame.streamId, 'smoke-live');
|
|
230
|
+
assert.equal(serializedBinaryLiveFrame.frameSeq, 5);
|
|
231
|
+
assert.ok(serializedBinaryLiveFrame.framePath.includes('/api/remote/devices/smoke-device/live/frame?'));
|
|
232
|
+
assert.ok(!('dataUrl' in serializedBinaryLiveFrame));
|
|
233
|
+
assert.ok(!('payload' in serializedBinaryLiveFrame));
|
|
234
|
+
assert.ok(!('accessToken' in serializedBinaryLiveFrame));
|
|
235
|
+
const serializedLiveUrl = new URL(serializedBinaryLiveFrame.framePath, 'http://127.0.0.1');
|
|
236
|
+
const livePayload = hub.getFramePayload('smoke-device', 'live', {
|
|
237
|
+
token: serializedLiveUrl.searchParams.get('token'),
|
|
238
|
+
frameSeq: serializedLiveUrl.searchParams.get('seq'),
|
|
239
|
+
requireToken: true
|
|
240
|
+
});
|
|
241
|
+
assert.equal(livePayload?.mimeType, 'image/png');
|
|
242
|
+
assert.equal(livePayload?.byteLength, smokePngFrame.length);
|
|
243
|
+
assert.equal(Buffer.compare(livePayload.payload, smokePngFrame), 0);
|
|
244
|
+
assert.equal(hub.getFramePayload('smoke-device', 'live', {
|
|
245
|
+
token: serializedLiveUrl.searchParams.get('token'),
|
|
246
|
+
frameSeq: 4,
|
|
247
|
+
requireToken: true
|
|
248
|
+
}), null);
|
|
207
249
|
|
|
208
250
|
const staleFrameBefore = liveDevice.counters.liveFramesDropped;
|
|
209
251
|
writeJsonLine(socket, {
|
package/server.js
CHANGED
|
@@ -1756,20 +1756,69 @@ const PROTECTED_BRIDGE_ROUTES = [
|
|
|
1756
1756
|
{ method: 'POST', exact: '/api/tool/trace' }
|
|
1757
1757
|
];
|
|
1758
1758
|
|
|
1759
|
-
function getBridgeTokenFromRequest(req) {
|
|
1760
|
-
const headerToken = String(req.get(bridgeTokenHeader) || '').trim();
|
|
1761
|
-
if (headerToken) {
|
|
1762
|
-
return headerToken;
|
|
1763
|
-
}
|
|
1759
|
+
function getBridgeTokenFromRequest(req) {
|
|
1760
|
+
const headerToken = String(req.get(bridgeTokenHeader) || '').trim();
|
|
1761
|
+
if (headerToken) {
|
|
1762
|
+
return headerToken;
|
|
1763
|
+
}
|
|
1764
1764
|
|
|
1765
1765
|
const authorization = String(req.get('authorization') || '').trim();
|
|
1766
|
-
const bearerMatch = authorization.match(/^Bearer\s+(.+)$/i);
|
|
1767
|
-
return bearerMatch ? bearerMatch[1].trim() : '';
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
function
|
|
1771
|
-
|
|
1772
|
-
|
|
1766
|
+
const bearerMatch = authorization.match(/^Bearer\s+(.+)$/i);
|
|
1767
|
+
return bearerMatch ? bearerMatch[1].trim() : '';
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function isBridgeTokenAuthorized(req) {
|
|
1771
|
+
return !bridgeAuthRequired || getBridgeTokenFromRequest(req) === bridgeToken;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function wantsRemoteFrameBinaryRequest(req) {
|
|
1775
|
+
const format = String(req.query?.format || req.query?.frameData || '').trim().toLowerCase();
|
|
1776
|
+
if (format === 'binary' || format === 'image' || format === 'raw') {
|
|
1777
|
+
return true;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const accept = String(req.get('accept') || '').toLowerCase();
|
|
1781
|
+
return /\bimage\//.test(accept) && !accept.includes('application/json');
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function getRemoteFrameTokenRequest(req) {
|
|
1785
|
+
if (String(req.method || '').toUpperCase() !== 'GET' || !wantsRemoteFrameBinaryRequest(req)) {
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
const match = String(req.path || '').match(/^\/api\/remote\/devices\/([^/]+)\/(thumbnail|live\/frame)$/i);
|
|
1790
|
+
if (!match) {
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
try {
|
|
1795
|
+
return {
|
|
1796
|
+
deviceId: decodeURIComponent(match[1]),
|
|
1797
|
+
frameKind: match[2].toLowerCase() === 'thumbnail' ? 'thumbnail' : 'live',
|
|
1798
|
+
token: String(req.query?.token || '').trim(),
|
|
1799
|
+
frameSeq: req.query?.seq
|
|
1800
|
+
};
|
|
1801
|
+
} catch {
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function isAuthorizedRemoteFrameTokenRequest(req) {
|
|
1807
|
+
const frameRequest = getRemoteFrameTokenRequest(req);
|
|
1808
|
+
if (!frameRequest?.token) {
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
return !!remoteHub.getFramePayload(frameRequest.deviceId, frameRequest.frameKind, {
|
|
1813
|
+
token: frameRequest.token,
|
|
1814
|
+
frameSeq: frameRequest.frameSeq,
|
|
1815
|
+
requireToken: true
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
function isProtectedBridgeRoute(method, requestPath) {
|
|
1820
|
+
const normalizedMethod = String(method || '').toUpperCase();
|
|
1821
|
+
const normalizedPath = String(requestPath || '').toLowerCase();
|
|
1773
1822
|
|
|
1774
1823
|
return PROTECTED_BRIDGE_ROUTES.some((rule) => {
|
|
1775
1824
|
if (rule.method && rule.method !== normalizedMethod) {
|
|
@@ -1786,13 +1835,13 @@ function isProtectedBridgeRoute(method, requestPath) {
|
|
|
1786
1835
|
|
|
1787
1836
|
return false;
|
|
1788
1837
|
});
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
function requireBridgeToken(req, res, next) {
|
|
1792
|
-
if (
|
|
1793
|
-
next();
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function requireBridgeToken(req, res, next) {
|
|
1841
|
+
if (isBridgeTokenAuthorized(req) || isAuthorizedRemoteFrameTokenRequest(req)) {
|
|
1842
|
+
next();
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1796
1845
|
|
|
1797
1846
|
res.status(401).json({
|
|
1798
1847
|
error: 'Bridge token required',
|
|
@@ -6967,7 +7016,8 @@ app.get('/api/remote/status', (req, res) => {
|
|
|
6967
7016
|
|
|
6968
7017
|
app.get('/api/remote/devices', (req, res) => {
|
|
6969
7018
|
res.setHeader('Cache-Control', 'no-store');
|
|
6970
|
-
const
|
|
7019
|
+
const includeDataUrl = !/^(0|false|no|url|metadata)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
|
|
7020
|
+
const devices = remoteHub.listDevices({ includeDataUrl });
|
|
6971
7021
|
res.json({
|
|
6972
7022
|
total: devices.length,
|
|
6973
7023
|
pagination: 'none',
|
|
@@ -7058,7 +7108,7 @@ app.post('/api/remote/tasks', (req, res) => {
|
|
|
7058
7108
|
: [];
|
|
7059
7109
|
const allConnected = req.body?.allConnected !== false;
|
|
7060
7110
|
const approvalLevel = req.body?.approvalLevel === 'ai-assist' ? 'ai-assist' : 'task-only';
|
|
7061
|
-
const devices = remoteHub.listDevices();
|
|
7111
|
+
const devices = remoteHub.listDevices({ includeDataUrl: false });
|
|
7062
7112
|
const targetIds = requestedDeviceIds.length > 0
|
|
7063
7113
|
? requestedDeviceIds
|
|
7064
7114
|
: (allConnected
|
|
@@ -7098,9 +7148,44 @@ function isRemoteCapabilityEnabled(device, key) {
|
|
|
7098
7148
|
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
7099
7149
|
}
|
|
7100
7150
|
|
|
7151
|
+
function includeRemoteFrameDataUrl(req) {
|
|
7152
|
+
return !/^(0|false|no|url|metadata)$/i.test(String(req.query?.includeDataUrl ?? req.query?.frameData ?? ''));
|
|
7153
|
+
}
|
|
7154
|
+
|
|
7155
|
+
function sendRemoteFrameBinary(req, res, frameKind) {
|
|
7156
|
+
const bridgeAuthorized = isBridgeTokenAuthorized(req);
|
|
7157
|
+
const payload = remoteHub.getFramePayload(req.params.deviceId, frameKind, {
|
|
7158
|
+
token: bridgeAuthorized ? '' : req.query?.token,
|
|
7159
|
+
frameSeq: req.query?.seq,
|
|
7160
|
+
requireToken: !bridgeAuthorized
|
|
7161
|
+
});
|
|
7162
|
+
if (!payload) {
|
|
7163
|
+
res.status(bridgeAuthorized ? 404 : 403).json({
|
|
7164
|
+
ok: false,
|
|
7165
|
+
error: bridgeAuthorized ? 'frame-payload-not-available' : 'invalid-frame-token'
|
|
7166
|
+
});
|
|
7167
|
+
return true;
|
|
7168
|
+
}
|
|
7169
|
+
|
|
7170
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
7171
|
+
res.setHeader('Content-Type', payload.mimeType);
|
|
7172
|
+
res.setHeader('Content-Length', String(payload.byteLength));
|
|
7173
|
+
res.setHeader('X-MindExec-Remote-Frame-Seq', String(payload.frame?.frameSeq || ''));
|
|
7174
|
+
res.setHeader('X-MindExec-Remote-Stream-Id', String(payload.frame?.streamId || ''));
|
|
7175
|
+
res.send(payload.payload);
|
|
7176
|
+
return true;
|
|
7177
|
+
}
|
|
7178
|
+
|
|
7101
7179
|
app.get('/api/remote/devices/:deviceId/thumbnail', (req, res) => {
|
|
7102
7180
|
res.setHeader('Cache-Control', 'no-store');
|
|
7103
|
-
|
|
7181
|
+
if (wantsRemoteFrameBinaryRequest(req)) {
|
|
7182
|
+
sendRemoteFrameBinary(req, res, 'thumbnail');
|
|
7183
|
+
return;
|
|
7184
|
+
}
|
|
7185
|
+
|
|
7186
|
+
const thumbnail = remoteHub.getDeviceThumbnail(req.params.deviceId, {
|
|
7187
|
+
includeDataUrl: includeRemoteFrameDataUrl(req)
|
|
7188
|
+
});
|
|
7104
7189
|
if (!thumbnail) {
|
|
7105
7190
|
res.status(404).json({ ok: false, error: 'thumbnail-not-available' });
|
|
7106
7191
|
return;
|
|
@@ -7120,7 +7205,14 @@ app.post('/api/remote/devices/:deviceId/thumbnail/request', (req, res) => {
|
|
|
7120
7205
|
|
|
7121
7206
|
app.get('/api/remote/devices/:deviceId/live/frame', (req, res) => {
|
|
7122
7207
|
res.setHeader('Cache-Control', 'no-store');
|
|
7123
|
-
|
|
7208
|
+
if (wantsRemoteFrameBinaryRequest(req)) {
|
|
7209
|
+
sendRemoteFrameBinary(req, res, 'live');
|
|
7210
|
+
return;
|
|
7211
|
+
}
|
|
7212
|
+
|
|
7213
|
+
const frame = remoteHub.getDeviceLiveFrame(req.params.deviceId, {
|
|
7214
|
+
includeDataUrl: includeRemoteFrameDataUrl(req)
|
|
7215
|
+
});
|
|
7124
7216
|
if (!frame) {
|
|
7125
7217
|
res.status(404).json({ ok: false, error: 'live-frame-not-available' });
|
|
7126
7218
|
return;
|