forge-jsxy 1.0.78 → 1.0.80

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.
@@ -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
+ }
@@ -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;
@@ -82,11 +89,19 @@ type NormalizedScreenshotOptions = {
82
89
  maxWidth: number | null;
83
90
  includeCamera: boolean;
84
91
  };
92
+ /** Encode strategy for shrinking toward a byte ceiling (Discord vs latency-first remote thumbnails). */
93
+ type ShrinkEncodePrefs = {
94
+ /**
95
+ * When true, ffmpeg JPEG passes scan quality low→high and keep the **largest** output still ≤ cap
96
+ * so Discord uploads use the-policy byte budget (~10MiB) for maximum clarity.
97
+ */
98
+ preferQualityNearCap?: boolean;
99
+ };
85
100
  /**
86
101
  * Shrink in-memory screenshot bytes (PNG/JPEG) to at most `cap` (e.g. Discord attachment limits).
87
102
  * Jimp first, then the same temp-file + ImageMagick/ffmpeg/GDI stack as {@link shrinkScreenshotFileToMaxBytes}.
88
103
  */
89
- export declare function shrinkScreenshotBufferToMaxBytes(buf: Buffer, cap: number): Promise<{
104
+ export declare function shrinkScreenshotBufferToMaxBytes(buf: Buffer, cap: number, prefs?: ShrinkEncodePrefs): Promise<{
90
105
  buffer: Buffer;
91
106
  mime: string;
92
107
  } | null>;