surf-cli 2.7.2 → 2.9.0
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/README.md +208 -13
- package/dist/content/index.js +116 -0
- package/dist/content/index.js.map +1 -0
- package/dist/manifest.json +2 -11
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +261 -61
- package/dist/service-worker/index.js.map +1 -1
- package/native/abort.cjs +65 -0
- package/native/ai-queue.cjs +64 -0
- package/native/aistudio-build.cjs +21 -13
- package/native/aistudio-client.cjs +40 -20
- package/native/browser-lock.cjs +169 -0
- package/native/chatgpt-client.cjs +63 -30
- package/native/cli.cjs +947 -460
- package/native/client-transport.cjs +168 -0
- package/native/config.cjs +2 -2
- package/native/do-executor.cjs +25 -51
- package/native/do-parser.cjs +12 -0
- package/native/doctor.cjs +633 -0
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +244 -88
- package/native/grok-client.cjs +321 -212
- package/native/host-helpers.cjs +88 -16
- package/native/host-sessions.cjs +283 -0
- package/native/host.cjs +811 -616
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -62
- package/native/network-export.cjs +113 -0
- package/native/perplexity-client.cjs +46 -17
- package/native/remote-auth.cjs +279 -0
- package/native/remote-transport.cjs +337 -0
- package/native/request-pending.cjs +148 -0
- package/native/socket-path.cjs +46 -0
- package/package.json +11 -9
- package/scripts/install-native-host.cjs +184 -51
- package/scripts/uninstall-native-host.cjs +93 -15
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +77 -22
- package/dist/content/accessibility-tree.js +0 -11
- package/dist/content/accessibility-tree.js.map +0 -1
- package/dist/content/visual-indicator.js +0 -111
- package/dist/content/visual-indicator.js.map +0 -1
package/native/host-helpers.cjs
CHANGED
|
@@ -2,6 +2,14 @@ const fs = require("fs");
|
|
|
2
2
|
const networkFormatters = require("./formatters/network.cjs");
|
|
3
3
|
const networkStore = require("./network-store.cjs");
|
|
4
4
|
|
|
5
|
+
function buildProviderUploadMessage(provider, tabId, filePaths, id) {
|
|
6
|
+
const normalizedProvider = String(provider || "").toLowerCase();
|
|
7
|
+
if (!["chatgpt", "gemini"].includes(normalizedProvider)) {
|
|
8
|
+
throw new Error(`Unsupported upload provider: ${provider}`);
|
|
9
|
+
}
|
|
10
|
+
return { type: "AI_UPLOAD_FILE_TO_TAB", provider: normalizedProvider, tabId, filePaths, id };
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
function normalizeModelString(model) {
|
|
6
14
|
return String(model || "").trim().toLowerCase();
|
|
7
15
|
}
|
|
@@ -12,7 +20,7 @@ function normalizeModelString(model) {
|
|
|
12
20
|
* @param {Function} log - Logging function (defaults to no-op for testing)
|
|
13
21
|
* @returns {Array} Array of content objects with type and text/data
|
|
14
22
|
*/
|
|
15
|
-
function formatToolContent(result, log = () => {}) {
|
|
23
|
+
function formatToolContent(result, log = () => {}, options = {}) {
|
|
16
24
|
const text = (s) => [{ type: "text", text: s }];
|
|
17
25
|
|
|
18
26
|
if (!result) return text("OK");
|
|
@@ -342,6 +350,19 @@ function formatToolContent(result, log = () => {}) {
|
|
|
342
350
|
}
|
|
343
351
|
return text(output);
|
|
344
352
|
}
|
|
353
|
+
|
|
354
|
+
if (
|
|
355
|
+
result.scrollTop !== undefined &&
|
|
356
|
+
result.scrollHeight !== undefined &&
|
|
357
|
+
result.scrollPercentage === undefined
|
|
358
|
+
) {
|
|
359
|
+
return text(`Scrolled to Y:${result.scrollTop} (page height: ${result.scrollHeight})`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (result.scrollY !== undefined) {
|
|
363
|
+
const pageHeight = result.pageHeight !== undefined ? ` (page height: ${result.pageHeight})` : "";
|
|
364
|
+
return text(`Scrolled to Y:${result.scrollY}${pageHeight}`);
|
|
365
|
+
}
|
|
345
366
|
|
|
346
367
|
if (result.success && result.name && result.tabId !== undefined) {
|
|
347
368
|
return text(`Registered tab ${result.tabId} as "${result.name}"`);
|
|
@@ -369,6 +390,7 @@ function formatToolContent(result, log = () => {}) {
|
|
|
369
390
|
|
|
370
391
|
if (result.autoScreenshot) {
|
|
371
392
|
const { path: ssPath, width, height } = result.autoScreenshot;
|
|
393
|
+
if (options.suppressImages) return text(`OK\nScreenshot saved: ${ssPath}`);
|
|
372
394
|
try {
|
|
373
395
|
const imgData = fs.readFileSync(ssPath);
|
|
374
396
|
const base64 = imgData.toString("base64");
|
|
@@ -450,11 +472,16 @@ function mapComputerAction(args, tabId) {
|
|
|
450
472
|
if (ref) return { type: "CLICK_REF", ref, button: "triple", ...baseMsg };
|
|
451
473
|
return { type: "EXECUTE_TRIPLE_CLICK", x: coordinate?.[0], y: coordinate?.[1], modifiers, ...baseMsg };
|
|
452
474
|
|
|
453
|
-
case "type":
|
|
475
|
+
case "type": {
|
|
454
476
|
if (ref) {
|
|
455
477
|
return { type: "FORM_FILL", data: [{ ref, value: text }], ...baseMsg };
|
|
456
478
|
}
|
|
479
|
+
const typeSelector = a.selector || a.into;
|
|
480
|
+
if (typeSelector) {
|
|
481
|
+
return { type: "SMART_TYPE", selector: typeSelector, text, clear: a.clear ?? true, submit: a.submit ?? false, ...baseMsg };
|
|
482
|
+
}
|
|
457
483
|
return { type: "EXECUTE_TYPE", text, ...baseMsg };
|
|
484
|
+
}
|
|
458
485
|
|
|
459
486
|
case "key": {
|
|
460
487
|
const keyValue = a.key || text;
|
|
@@ -478,14 +505,15 @@ function mapComputerAction(args, tabId) {
|
|
|
478
505
|
return { type: "FIND_AND_TYPE", text, submit: a.submit ?? false, submitKey: a.submitKey || "Enter", ...baseMsg };
|
|
479
506
|
|
|
480
507
|
case "scroll": {
|
|
481
|
-
const
|
|
508
|
+
const direction = a.direction || scroll_direction;
|
|
509
|
+
const amount = a.scroll_pixels ?? ((a.amount ?? scroll_amount ?? 3) * 100);
|
|
482
510
|
const deltas = {
|
|
483
511
|
up: { deltaX: 0, deltaY: -amount },
|
|
484
512
|
down: { deltaX: 0, deltaY: amount },
|
|
485
513
|
left: { deltaX: -amount, deltaY: 0 },
|
|
486
514
|
right: { deltaX: amount, deltaY: 0 },
|
|
487
515
|
};
|
|
488
|
-
const { deltaX, deltaY } = deltas[
|
|
516
|
+
const { deltaX, deltaY } = deltas[direction] || { deltaX: 0, deltaY: 0 };
|
|
489
517
|
return { type: "EXECUTE_SCROLL", deltaX, deltaY, x: coordinate?.[0], y: coordinate?.[1], ...baseMsg };
|
|
490
518
|
}
|
|
491
519
|
|
|
@@ -575,7 +603,7 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
575
603
|
type: "EXECUTE_SCREENSHOT",
|
|
576
604
|
savePath: a.savePath || a.output, // Accept both savePath (CLI) and output (MCP)
|
|
577
605
|
annotate: a.annotate || false,
|
|
578
|
-
fullpage: a.fullpage || false,
|
|
606
|
+
fullpage: a.fullpage || a["full-page"] || false,
|
|
579
607
|
maxHeight: a["max-height"] || 4000,
|
|
580
608
|
fullRes: a.full || false,
|
|
581
609
|
maxSize: a["max-size"] || 1200,
|
|
@@ -583,6 +611,31 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
583
611
|
};
|
|
584
612
|
case "javascript_tool":
|
|
585
613
|
return { type: "EXECUTE_JAVASCRIPT", code: a.code, ...baseMsg };
|
|
614
|
+
case "animate-audit": {
|
|
615
|
+
if (!a.selector || typeof a.selector !== "string") throw new Error("selector required");
|
|
616
|
+
if (typeof a.duration === "boolean") throw new Error("duration must be a number");
|
|
617
|
+
if (typeof a.fps === "boolean") throw new Error("fps must be a number");
|
|
618
|
+
const durationMs = a.duration !== undefined ? Number(a.duration) : 2000;
|
|
619
|
+
const fps = a.fps !== undefined ? Number(a.fps) : 10;
|
|
620
|
+
if (!Number.isFinite(durationMs) || durationMs < 100 || durationMs > 10000) {
|
|
621
|
+
throw new Error("duration must be between 100 and 10000 ms");
|
|
622
|
+
}
|
|
623
|
+
if (!Number.isFinite(fps) || fps < 1 || fps > 30) {
|
|
624
|
+
throw new Error("fps must be between 1 and 30");
|
|
625
|
+
}
|
|
626
|
+
return { type: "ANIMATE_AUDIT", selector: a.selector, durationMs, fps, ...baseMsg };
|
|
627
|
+
}
|
|
628
|
+
case "perf-audit": {
|
|
629
|
+
if (typeof a.duration === "boolean") throw new Error("duration must be a number");
|
|
630
|
+
if (a.trigger !== undefined && typeof a.trigger !== "string") {
|
|
631
|
+
throw new Error("trigger must be action:target");
|
|
632
|
+
}
|
|
633
|
+
const durationMs = a.duration !== undefined ? Number(a.duration) : 3000;
|
|
634
|
+
if (!Number.isFinite(durationMs) || durationMs < 100 || durationMs > 10000) {
|
|
635
|
+
throw new Error("duration must be between 100 and 10000 ms");
|
|
636
|
+
}
|
|
637
|
+
return { type: "PERF_AUDIT", durationMs, trigger: a.trigger, ...baseMsg };
|
|
638
|
+
}
|
|
586
639
|
case "wait_for_element":
|
|
587
640
|
return {
|
|
588
641
|
type: "WAIT_FOR_ELEMENT",
|
|
@@ -679,7 +732,6 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
679
732
|
type: "EXPORT_NETWORK_REQUESTS",
|
|
680
733
|
har: a.har,
|
|
681
734
|
jsonl: a.jsonl,
|
|
682
|
-
output: a.output,
|
|
683
735
|
...baseMsg
|
|
684
736
|
};
|
|
685
737
|
|
|
@@ -751,6 +803,12 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
751
803
|
}
|
|
752
804
|
return { type: "CLOSE_TAB", tabId: id, tabIds: ids };
|
|
753
805
|
}
|
|
806
|
+
case "tab.move": {
|
|
807
|
+
const id = a.id || a.tab_id || a.tabId;
|
|
808
|
+
const ids = a.ids || a.tab_ids || a.tabIds;
|
|
809
|
+
const windowId = a["to-window"] || a.toWindow || a.window_id || a.windowId;
|
|
810
|
+
return { type: "TAB_MOVE", tabId: id, tabIds: ids, windowId, index: a.index };
|
|
811
|
+
}
|
|
754
812
|
case "tab.name":
|
|
755
813
|
return { type: "TABS_REGISTER", name: a.name, ...baseMsg };
|
|
756
814
|
case "tab.unname":
|
|
@@ -843,18 +901,32 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
843
901
|
case "upload":
|
|
844
902
|
const files = a.files ? (typeof a.files === "string" ? a.files.split(",").map(f => f.trim()) : a.files) : [];
|
|
845
903
|
return { type: "UPLOAD_FILE", ref: a.ref, files, ...baseMsg };
|
|
846
|
-
case "page.read":
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
904
|
+
case "page.read": {
|
|
905
|
+
let maxBytes;
|
|
906
|
+
if (a["max-bytes"] !== undefined) {
|
|
907
|
+
const raw = String(a["max-bytes"]).trim();
|
|
908
|
+
if (!/^\d+$/.test(raw) || raw === "0") {
|
|
909
|
+
throw new Error("max-bytes must be a positive integer");
|
|
910
|
+
}
|
|
911
|
+
maxBytes = parseInt(raw, 10);
|
|
912
|
+
if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
|
|
913
|
+
throw new Error("max-bytes must be a positive integer");
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
type: "READ_PAGE",
|
|
918
|
+
options: {
|
|
919
|
+
filter: a.filter || "interactive",
|
|
920
|
+
refId: a.ref,
|
|
852
921
|
includeText: a["no-text"] !== true,
|
|
853
922
|
depth: a.depth !== undefined ? parseInt(a.depth, 10) : undefined,
|
|
854
923
|
compact: a.compact || false,
|
|
855
|
-
|
|
856
|
-
|
|
924
|
+
maxBytes,
|
|
925
|
+
forceFullSnapshot: a.compact === true || maxBytes !== undefined,
|
|
926
|
+
},
|
|
927
|
+
...baseMsg
|
|
857
928
|
};
|
|
929
|
+
}
|
|
858
930
|
case "page.text":
|
|
859
931
|
return { type: "GET_PAGE_TEXT", ...baseMsg };
|
|
860
932
|
case "page.state":
|
|
@@ -1034,7 +1106,7 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
1034
1106
|
return {
|
|
1035
1107
|
type: "GEMINI_QUERY",
|
|
1036
1108
|
query: a.query,
|
|
1037
|
-
model: a.model || "gemini-3-pro",
|
|
1109
|
+
model: a.model || "gemini-3.1-pro",
|
|
1038
1110
|
withPage: a["with-page"],
|
|
1039
1111
|
file: a.file,
|
|
1040
1112
|
generateImage: a["generate-image"],
|
|
@@ -1132,4 +1204,4 @@ function mapToolToMessage(tool, args, tabId) {
|
|
|
1132
1204
|
}
|
|
1133
1205
|
}
|
|
1134
1206
|
|
|
1135
|
-
module.exports = { mapToolToMessage, mapComputerAction, formatToolContent };
|
|
1207
|
+
module.exports = { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
const { abortError } = require("./abort.cjs");
|
|
2
|
+
|
|
3
|
+
const MAX_CONNECTIONS = 32;
|
|
4
|
+
const MAX_REMOTE_CONNECTIONS = 16;
|
|
5
|
+
const MAX_PRINCIPAL_CONNECTIONS = 4;
|
|
6
|
+
const MAX_WAITERS = 16;
|
|
7
|
+
const MAX_STREAMS_PER_PRINCIPAL = 2;
|
|
8
|
+
const MAX_REMOTE_STREAMS = 4;
|
|
9
|
+
const MAX_STREAMS = 4;
|
|
10
|
+
const REQUEST_ID_LIMIT = 128;
|
|
11
|
+
const TOOL_NAME_LIMIT = 128;
|
|
12
|
+
const LEASE_IDLE_MS = 5000;
|
|
13
|
+
const AUTHENTICATED_IDLE_MS = 15000;
|
|
14
|
+
const QUEUE_TIMEOUT_MS = 60000;
|
|
15
|
+
const DEFAULT_DEADLINE_MS = 60000;
|
|
16
|
+
const MAX_DEADLINE_MS = 50 * 60 * 1000;
|
|
17
|
+
const CLEANUP_GRACE_MS = 60000;
|
|
18
|
+
const PROVIDER_DEFAULT_TIMEOUT_SECONDS = {
|
|
19
|
+
ai: 300,
|
|
20
|
+
aistudio: 300,
|
|
21
|
+
"aistudio.build": 600,
|
|
22
|
+
chatgpt: 2700,
|
|
23
|
+
gemini: 300,
|
|
24
|
+
grok: 300,
|
|
25
|
+
perplexity: 120,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function resolveRequestDeadlineMs(tool, args = {}) {
|
|
29
|
+
const defaultSeconds = PROVIDER_DEFAULT_TIMEOUT_SECONDS[tool];
|
|
30
|
+
if (defaultSeconds === undefined) return DEFAULT_DEADLINE_MS;
|
|
31
|
+
const requestedSeconds = Number(
|
|
32
|
+
args && typeof args === "object" && !Array.isArray(args) ? args.timeout : undefined,
|
|
33
|
+
);
|
|
34
|
+
const seconds = Number.isFinite(requestedSeconds) && requestedSeconds > 0
|
|
35
|
+
? requestedSeconds
|
|
36
|
+
: defaultSeconds;
|
|
37
|
+
return Math.min(seconds * 1000 + CLEANUP_GRACE_MS, MAX_DEADLINE_MS);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class HostSessionManager {
|
|
41
|
+
constructor({ audit = () => {}, onTimeout = () => {} } = {}) {
|
|
42
|
+
this.audit = audit;
|
|
43
|
+
this.onTimeout = onTimeout;
|
|
44
|
+
this.contexts = new Set();
|
|
45
|
+
this.principalCounts = new Map();
|
|
46
|
+
this.waiters = [];
|
|
47
|
+
this.leaseOwner = null;
|
|
48
|
+
this.remoteConnections = 0;
|
|
49
|
+
this.streams = new Set();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
admit(socket, isRemote) {
|
|
53
|
+
if (this.contexts.size >= MAX_CONNECTIONS) throw new Error("maximum client connections reached");
|
|
54
|
+
if (isRemote && this.remoteConnections >= MAX_REMOTE_CONNECTIONS) throw new Error("maximum remote connections reached");
|
|
55
|
+
const context = {
|
|
56
|
+
socket,
|
|
57
|
+
isRemote,
|
|
58
|
+
principal: null,
|
|
59
|
+
closed: false,
|
|
60
|
+
activeRequest: null,
|
|
61
|
+
seenRequestIds: new Set(),
|
|
62
|
+
idleTimer: null,
|
|
63
|
+
workTimer: null,
|
|
64
|
+
admitted: true,
|
|
65
|
+
stream: false,
|
|
66
|
+
};
|
|
67
|
+
this.contexts.add(context);
|
|
68
|
+
if (isRemote) this.remoteConnections += 1;
|
|
69
|
+
this.audit({ event: "connection", context, outcome: "accepted" });
|
|
70
|
+
return context;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
authenticate(context, principal) {
|
|
74
|
+
if (context.closed) throw new Error("connection is closed");
|
|
75
|
+
const count = this.principalCounts.get(principal.clientId) || 0;
|
|
76
|
+
if (count >= MAX_PRINCIPAL_CONNECTIONS) throw new Error("maximum connections for remote principal reached");
|
|
77
|
+
context.principal = principal;
|
|
78
|
+
this.principalCounts.set(principal.clientId, count + 1);
|
|
79
|
+
context.workTimer = setTimeout(() => {
|
|
80
|
+
if (!context.closed && !context.activeRequest) {
|
|
81
|
+
this.audit({ event: "connection", context, outcome: "authenticated-idle-timeout" });
|
|
82
|
+
context.socket.destroy();
|
|
83
|
+
}
|
|
84
|
+
}, AUTHENTICATED_IDLE_MS);
|
|
85
|
+
this.audit({ event: "authentication", context, outcome: "success" });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
touch(context) {
|
|
89
|
+
if (!context || context.closed || !context.isRemote || context.activeRequest) return;
|
|
90
|
+
if (context.workTimer) clearTimeout(context.workTimer);
|
|
91
|
+
context.workTimer = setTimeout(() => {
|
|
92
|
+
if (!context.closed && !context.activeRequest) {
|
|
93
|
+
this.audit({ event: "connection", context, outcome: "authenticated-idle-timeout" });
|
|
94
|
+
context.socket.destroy();
|
|
95
|
+
}
|
|
96
|
+
}, AUTHENTICATED_IDLE_MS);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
canStartStream(context) {
|
|
100
|
+
if (context.closed || context.stream || context.activeRequest || this.leaseOwner === context) return false;
|
|
101
|
+
const principalId = context.principal?.clientId || "local";
|
|
102
|
+
const principalStreams = [...this.streams].filter((entry) => entry.principalId === principalId).length;
|
|
103
|
+
if (principalStreams >= MAX_STREAMS_PER_PRINCIPAL) return false;
|
|
104
|
+
if (this.streams.size >= MAX_STREAMS) return false;
|
|
105
|
+
if (context.isRemote && [...this.streams].filter((entry) => entry.isRemote).length >= MAX_REMOTE_STREAMS) return false;
|
|
106
|
+
if (context.workTimer) {
|
|
107
|
+
clearTimeout(context.workTimer);
|
|
108
|
+
context.workTimer = null;
|
|
109
|
+
}
|
|
110
|
+
context.stream = true;
|
|
111
|
+
this.streams.add({ context, principalId, isRemote: context.isRemote });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
stopStream(context) {
|
|
116
|
+
for (const entry of this.streams) if (entry.context === context) this.streams.delete(entry);
|
|
117
|
+
context.stream = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
beginRequest(context, { id, tool, deadlineMs }) {
|
|
121
|
+
if (context.closed) return Promise.reject(new Error("connection is closed"));
|
|
122
|
+
if (context.workTimer) {
|
|
123
|
+
clearTimeout(context.workTimer);
|
|
124
|
+
context.workTimer = null;
|
|
125
|
+
}
|
|
126
|
+
if (context.stream) return Promise.reject(new Error("stream connection cannot execute tools"));
|
|
127
|
+
if (typeof id !== "string" || !id) return Promise.reject(new Error("request id is required"));
|
|
128
|
+
if (id.length > REQUEST_ID_LIMIT) return Promise.reject(new Error("request ID is too long"));
|
|
129
|
+
if (typeof tool !== "string" || !tool) return Promise.reject(new Error("tool name is required"));
|
|
130
|
+
if (tool.length > TOOL_NAME_LIMIT) return Promise.reject(new Error("tool name is too long"));
|
|
131
|
+
if (context.seenRequestIds.has(id)) return Promise.reject(new Error("duplicate request id"));
|
|
132
|
+
if (context.seenRequestIds.size >= REQUEST_ID_LIMIT) return Promise.reject(new Error("request ID limit reached"));
|
|
133
|
+
if (context.activeRequest) return Promise.reject(new Error("one in-flight request per connection"));
|
|
134
|
+
context.seenRequestIds.add(id);
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const request = {
|
|
137
|
+
id,
|
|
138
|
+
tool,
|
|
139
|
+
startedAt: Date.now(),
|
|
140
|
+
deadlineMs: Math.min(Math.max(deadlineMs || DEFAULT_DEADLINE_MS, 1), MAX_DEADLINE_MS),
|
|
141
|
+
queued: true,
|
|
142
|
+
settled: false,
|
|
143
|
+
controller,
|
|
144
|
+
signal: controller.signal,
|
|
145
|
+
tombstoned: false,
|
|
146
|
+
};
|
|
147
|
+
context.activeRequest = request;
|
|
148
|
+
const grant = () => {
|
|
149
|
+
if (context.closed) return Promise.reject(new Error("connection closed while waiting for browser lease"));
|
|
150
|
+
request.queued = false;
|
|
151
|
+
request.timer = setTimeout(() => this.onRequestTimeout(context, request), request.deadlineMs);
|
|
152
|
+
this.leaseOwner = context;
|
|
153
|
+
this.audit({ event: "lease", context, request, outcome: "acquired" });
|
|
154
|
+
return Promise.resolve(request);
|
|
155
|
+
};
|
|
156
|
+
if (!this.leaseOwner || this.leaseOwner === context) {
|
|
157
|
+
if (context.idleTimer) clearTimeout(context.idleTimer);
|
|
158
|
+
context.idleTimer = null;
|
|
159
|
+
return grant();
|
|
160
|
+
}
|
|
161
|
+
if (this.waiters.length >= MAX_WAITERS) {
|
|
162
|
+
context.activeRequest = null;
|
|
163
|
+
return Promise.reject(new Error("browser lease queue is full"));
|
|
164
|
+
}
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const waiter = { context, request, resolve, reject };
|
|
167
|
+
waiter.timer = setTimeout(() => {
|
|
168
|
+
const index = this.waiters.indexOf(waiter);
|
|
169
|
+
if (index !== -1) this.waiters.splice(index, 1);
|
|
170
|
+
if (context.activeRequest === request) context.activeRequest = null;
|
|
171
|
+
this.audit({ event: "lease", context, request, outcome: "queue-timeout" });
|
|
172
|
+
reject(new Error("timed out waiting for browser lease"));
|
|
173
|
+
}, QUEUE_TIMEOUT_MS);
|
|
174
|
+
this.waiters.push(waiter);
|
|
175
|
+
this.audit({ event: "lease", context, request, outcome: "queued" });
|
|
176
|
+
}).then(() => grant());
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
onRequestTimeout(context, request) {
|
|
180
|
+
if (context.activeRequest !== request || request.settled) return;
|
|
181
|
+
request.tombstoned = true;
|
|
182
|
+
if (!request.signal.aborted) request.controller.abort(abortError(null, "Request timed out"));
|
|
183
|
+
this.audit({ event: "request", context, request, outcome: "hard-timeout" });
|
|
184
|
+
this.onTimeout(context, request);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
canRespond(context, id) {
|
|
188
|
+
return Boolean(context && context.activeRequest && context.activeRequest.id === id && !context.activeRequest.settled);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
complete(context, id, outcome = "completed") {
|
|
192
|
+
const request = context?.activeRequest;
|
|
193
|
+
if (!request || request.id !== id || request.settled) return;
|
|
194
|
+
request.settled = true;
|
|
195
|
+
if (request.timer) clearTimeout(request.timer);
|
|
196
|
+
if (request.abortCleanup) request.abortCleanup();
|
|
197
|
+
context.activeRequest = null;
|
|
198
|
+
this.audit({ event: "request", context, request, outcome, elapsedMs: Date.now() - request.startedAt });
|
|
199
|
+
if (this.leaseOwner === context) {
|
|
200
|
+
if (context.closed) this.releaseLease(context);
|
|
201
|
+
else {
|
|
202
|
+
if (context.idleTimer) clearTimeout(context.idleTimer);
|
|
203
|
+
context.idleTimer = setTimeout(() => {
|
|
204
|
+
if (!context.activeRequest && this.leaseOwner === context) this.releaseLease(context);
|
|
205
|
+
}, LEASE_IDLE_MS);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
releaseLease(context) {
|
|
211
|
+
if (this.leaseOwner !== context) return;
|
|
212
|
+
if (context.idleTimer) clearTimeout(context.idleTimer);
|
|
213
|
+
context.idleTimer = null;
|
|
214
|
+
this.leaseOwner = null;
|
|
215
|
+
this.audit({ event: "lease", context, outcome: "released" });
|
|
216
|
+
while (this.waiters.length) {
|
|
217
|
+
const waiter = this.waiters.shift();
|
|
218
|
+
if (!waiter || waiter.context.closed) continue;
|
|
219
|
+
clearTimeout(waiter.timer);
|
|
220
|
+
this.leaseOwner = waiter.context;
|
|
221
|
+
waiter.resolve();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
close(context) {
|
|
227
|
+
if (!context || context.closed) return;
|
|
228
|
+
context.closed = true;
|
|
229
|
+
if (context.workTimer) clearTimeout(context.workTimer);
|
|
230
|
+
context.workTimer = null;
|
|
231
|
+
this.stopStream(context);
|
|
232
|
+
if (context.principal) {
|
|
233
|
+
const count = this.principalCounts.get(context.principal.clientId) || 1;
|
|
234
|
+
if (count <= 1) this.principalCounts.delete(context.principal.clientId);
|
|
235
|
+
else this.principalCounts.set(context.principal.clientId, count - 1);
|
|
236
|
+
}
|
|
237
|
+
for (let index = this.waiters.length - 1; index >= 0; index -= 1) {
|
|
238
|
+
const waiter = this.waiters[index];
|
|
239
|
+
if (waiter.context !== context) continue;
|
|
240
|
+
this.waiters.splice(index, 1);
|
|
241
|
+
clearTimeout(waiter.timer);
|
|
242
|
+
waiter.reject(new Error("connection closed while waiting for browser lease"));
|
|
243
|
+
}
|
|
244
|
+
if (context.activeRequest?.queued) {
|
|
245
|
+
const request = context.activeRequest;
|
|
246
|
+
context.activeRequest = null;
|
|
247
|
+
if (!request.signal.aborted) request.controller.abort(abortError(null, "Request cancelled: queued client disconnected"));
|
|
248
|
+
this.audit({ event: "request", context, request, outcome: "queue-canceled" });
|
|
249
|
+
} else if (context.activeRequest) {
|
|
250
|
+
const request = context.activeRequest;
|
|
251
|
+
request.abandoned = true;
|
|
252
|
+
request.tombstoned = true;
|
|
253
|
+
if (!request.signal.aborted) request.controller.abort(abortError(null, "Request cancelled: client disconnected"));
|
|
254
|
+
this.audit({ event: "request", context, request, outcome: "abort-requested" });
|
|
255
|
+
this.audit({ event: "request", context, request, outcome: "abandoned" });
|
|
256
|
+
} else if (this.leaseOwner === context) {
|
|
257
|
+
this.releaseLease(context);
|
|
258
|
+
}
|
|
259
|
+
this.contexts.delete(context);
|
|
260
|
+
if (context.isRemote) this.remoteConnections -= 1;
|
|
261
|
+
this.audit({ event: "connection", context, outcome: "closed" });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
AUTHENTICATED_IDLE_MS,
|
|
267
|
+
DEFAULT_DEADLINE_MS,
|
|
268
|
+
HostSessionManager,
|
|
269
|
+
MAX_DEADLINE_MS,
|
|
270
|
+
PROVIDER_DEFAULT_TIMEOUT_SECONDS,
|
|
271
|
+
resolveRequestDeadlineMs,
|
|
272
|
+
LEASE_IDLE_MS,
|
|
273
|
+
MAX_CONNECTIONS,
|
|
274
|
+
MAX_PRINCIPAL_CONNECTIONS,
|
|
275
|
+
MAX_REMOTE_CONNECTIONS,
|
|
276
|
+
MAX_REMOTE_STREAMS,
|
|
277
|
+
MAX_STREAMS,
|
|
278
|
+
MAX_STREAMS_PER_PRINCIPAL,
|
|
279
|
+
TOOL_NAME_LIMIT,
|
|
280
|
+
MAX_WAITERS,
|
|
281
|
+
QUEUE_TIMEOUT_MS,
|
|
282
|
+
REQUEST_ID_LIMIT,
|
|
283
|
+
};
|