dsh-builtin-browser 0.4.1 → 0.4.2
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.en.md +61 -23
- package/README.md +59 -23
- package/client.js +60 -5
- package/cordis.patch.yml +21 -0
- package/desktop-bridge/plugin-browser-bridge.js +12 -4
- package/docs/architecture.md +12 -2
- package/docs/tool-reference.md +12 -12
- package/docs/user-guide.md +10 -3
- package/docs/why-browser.md +1 -1
- package/lib/browser/types.d.ts +25 -1
- package/lib/browser-electron/bridge-connection.d.ts +31 -5
- package/lib/browser-electron/bridge-connection.js +143 -21
- package/lib/browser-electron/entry.d.ts +1 -1
- package/lib/browser-electron/entry.js +61 -35
- package/lib/browser-electron/ephemeral-profile.d.ts +37 -0
- package/lib/browser-electron/ephemeral-profile.js +146 -0
- package/lib/browser-electron/history-store.d.ts +25 -0
- package/lib/browser-electron/history-store.js +51 -4
- package/lib/browser-electron/host-main.js +75 -27
- package/lib/browser-electron/provider.d.ts +44 -1
- package/lib/browser-electron/provider.js +379 -48
- package/lib/browser-electron/remote-host.d.ts +52 -3
- package/lib/browser-electron/remote-host.js +244 -23
- package/lib/browser-electron/settings-route.d.ts +28 -0
- package/lib/browser-electron/settings-route.js +28 -0
- package/lib/browser-electron/settings-store.d.ts +40 -0
- package/lib/browser-electron/settings-store.js +107 -11
- package/lib/browser-electron/system-browser.d.ts +71 -3
- package/lib/browser-electron/system-browser.js +311 -40
- package/lib/tool-browser/index.js +142 -33
- package/package.json +5 -3
- package/scripts/verify-host-compat.mjs +195 -0
- package/src/browser/types.ts +25 -1
- package/src/browser-electron/bridge-connection.ts +142 -21
- package/src/browser-electron/entry.ts +58 -33
- package/src/browser-electron/ephemeral-profile.ts +152 -0
- package/src/browser-electron/history-store.ts +51 -4
- package/src/browser-electron/host-main.ts +105 -35
- package/src/browser-electron/provider.ts +401 -51
- package/src/browser-electron/remote-host.ts +261 -36
- package/src/browser-electron/settings-route.ts +46 -0
- package/src/browser-electron/settings-store.ts +121 -11
- package/src/browser-electron/system-browser.ts +302 -26
- package/src/tool-browser/index.ts +143 -35
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
* instead of poisoning the whole file.
|
|
13
13
|
* @module dsh-browser/browser-electron/history-store
|
|
14
14
|
*/
|
|
15
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
17
17
|
import { dirname, join } from 'node:path';
|
|
18
|
+
import { writeFileAtomic } from './settings-store.js';
|
|
18
19
|
/** Default entry cap — mirrors the documented retention rule. */
|
|
19
20
|
const DEFAULT_MAX_ENTRIES = 5_000;
|
|
20
21
|
/** Default age cap in days. */
|
|
@@ -37,6 +38,22 @@ export function visitedHistoryPath() {
|
|
|
37
38
|
*/
|
|
38
39
|
export class HistoryStore {
|
|
39
40
|
file;
|
|
41
|
+
/**
|
|
42
|
+
* File size at which the next rewrite happens.
|
|
43
|
+
*
|
|
44
|
+
* Advanced after each rewrite so the cost is paid once per PRUNE_SLACK appends
|
|
45
|
+
* rather than on every append: asking "is it over the cap?" was true from the first
|
|
46
|
+
* over-cap write onwards, which is what made every later navigation rewrite the file.
|
|
47
|
+
*/
|
|
48
|
+
pruneThreshold;
|
|
49
|
+
/**
|
|
50
|
+
* Appends since the last rewrite.
|
|
51
|
+
*
|
|
52
|
+
* prune() runs after every navigation, and the answer is almost always "nothing to
|
|
53
|
+
* do" — but working that out from the file costs a full parse. Counting instead makes
|
|
54
|
+
* the common case a comparison.
|
|
55
|
+
*/
|
|
56
|
+
appendedSincePrune = 0;
|
|
40
57
|
maxEntries;
|
|
41
58
|
maxAgeMs;
|
|
42
59
|
/**
|
|
@@ -46,6 +63,8 @@ export class HistoryStore {
|
|
|
46
63
|
constructor(file = visitedHistoryPath(), limits = {}) {
|
|
47
64
|
this.file = file;
|
|
48
65
|
this.maxEntries = limits.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
66
|
+
// The first rewrite is due once the file has grown PRUNE_SLACK past the cap.
|
|
67
|
+
this.pruneThreshold = this.maxEntries + this.slack();
|
|
49
68
|
this.maxAgeMs = (limits.maxAgeDays ?? DEFAULT_MAX_AGE_DAYS) * 24 * 60 * 60 * 1000;
|
|
50
69
|
}
|
|
51
70
|
/** Whether the history file exists yet (diagnostics and tests). */
|
|
@@ -66,6 +85,7 @@ export class HistoryStore {
|
|
|
66
85
|
catch {
|
|
67
86
|
// Best-effort: history is not worth failing a navigation over.
|
|
68
87
|
}
|
|
88
|
+
this.appendedSincePrune++;
|
|
69
89
|
}
|
|
70
90
|
/**
|
|
71
91
|
* Read visits, newest first. Unparseable lines are skipped; entries older
|
|
@@ -108,7 +128,22 @@ export class HistoryStore {
|
|
|
108
128
|
* the count cap. Cheap no-op until the file grows past the cap, so callers can
|
|
109
129
|
* run it after every append.
|
|
110
130
|
*/
|
|
131
|
+
/**
|
|
132
|
+
* How far past the cap the file may grow before it is rewritten.
|
|
133
|
+
*
|
|
134
|
+
* Scaled to the cap rather than fixed: a fixed 500 was meaningless when the cap is
|
|
135
|
+
* small (a caller asking for three entries would never reach 503), and the slack is
|
|
136
|
+
* there to batch rewrites, not to overrule the caller's bound.
|
|
137
|
+
* @returns the number of extra entries tolerated before a rewrite.
|
|
138
|
+
*/
|
|
139
|
+
slack() {
|
|
140
|
+
return Math.min(PRUNE_SLACK, Math.max(1, Math.floor(this.maxEntries / 2)));
|
|
141
|
+
}
|
|
111
142
|
prune() {
|
|
143
|
+
// Cheap first: past the cap but without enough new entries to justify a rewrite.
|
|
144
|
+
// Reading the file to reach this conclusion was the remaining per-navigation cost.
|
|
145
|
+
if (this.appendedSincePrune === 0)
|
|
146
|
+
return;
|
|
112
147
|
const all = this.readAll();
|
|
113
148
|
const cutoff = Date.now() - this.maxAgeMs;
|
|
114
149
|
const fresh = all.filter(page => page.at >= cutoff);
|
|
@@ -117,14 +152,26 @@ export class HistoryStore {
|
|
|
117
152
|
// is worth a full rewrite.
|
|
118
153
|
if (kept.length === all.length)
|
|
119
154
|
return;
|
|
120
|
-
|
|
155
|
+
// Only rewrite once the file has grown PRUNE_SLACK past what we last wrote. Asking
|
|
156
|
+
// "is the file over the cap?" instead was true from the first over-cap append
|
|
157
|
+
// onwards, so every single navigation after that rewrote the whole file.
|
|
158
|
+
if (all.length < this.pruneThreshold)
|
|
121
159
|
return;
|
|
122
160
|
try {
|
|
123
161
|
const body = kept.map(page => `${JSON.stringify(page)}\n`).join('');
|
|
124
|
-
writeFileSync
|
|
162
|
+
// Atomic, like the settings document. A plain writeFileSync truncates first, and this is
|
|
163
|
+
// the whole history in one call: a TerminateProcess mid-write — the browser host is
|
|
164
|
+
// killed exactly that way on Windows — left a partial file, and the reader drops a
|
|
165
|
+
// half-written TAIL rather than the middle, so everything after the tear was lost.
|
|
166
|
+
writeFileAtomic(this.file, body);
|
|
167
|
+
// The file now holds only what was kept, so the next rewrite is due once
|
|
168
|
+
// PRUNE_SLACK more entries have been appended.
|
|
169
|
+
this.pruneThreshold = kept.length + this.slack();
|
|
170
|
+
this.appendedSincePrune = 0;
|
|
125
171
|
}
|
|
126
172
|
catch {
|
|
127
|
-
// Best-effort: an unpruned file still reads correctly
|
|
173
|
+
// Best-effort: an unpruned file still reads correctly, and the atomic write leaves the
|
|
174
|
+
// previous one in place when it fails.
|
|
128
175
|
}
|
|
129
176
|
}
|
|
130
177
|
/** Parse the whole file, skipping blank and damaged lines. */
|
|
@@ -47,26 +47,42 @@ catch (error) {
|
|
|
47
47
|
process.stderr.write(`[dsh-browser host] userData setup failed: ${String(error)}\n`);
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Read the spawn token. The parent sends it two ways
|
|
51
|
-
* the first that yields a value:
|
|
50
|
+
* Read the spawn token. The parent sends it two ways:
|
|
52
51
|
* 1. stdin (first line) — the secure channel on Unix/macOS;
|
|
53
52
|
* 2. DSH_BROWSER_RPC_TOKEN env — the fallback on Windows, where Electron is
|
|
54
53
|
* a GUI-subsystem process and never receives piped stdin (the parent
|
|
55
54
|
* sets it in the spawn env for exactly this reason).
|
|
56
|
-
*
|
|
55
|
+
*
|
|
56
|
+
* stdin WINS over env, which is what the ordering here has to express. It used to read env
|
|
57
|
+
* first and only consult stdin when that came up empty, so the stdin branch never ran while
|
|
58
|
+
* env was set — every spawn was told both ways, so the documented preference was silently
|
|
59
|
+
* the other way round. The env value is kept as the default for the common case where stdin
|
|
60
|
+
* delivers nothing; the interface below overwrites it when a line arrives.
|
|
57
61
|
*/
|
|
58
62
|
let RPC_TOKEN = process.env.DSH_BROWSER_RPC_TOKEN ?? '';
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
/** True once a line actually arrived on stdin (as opposed to env being present). */
|
|
64
|
+
let tokenFromStdin = false;
|
|
65
|
+
/**
|
|
66
|
+
* The one and only stdin reader.
|
|
67
|
+
*
|
|
68
|
+
* It is created here and reused by the hello path inside `whenReady` below: two
|
|
69
|
+
* `createInterface` calls on the same stream both consume its lines, so the second one saw
|
|
70
|
+
* nothing at all. A single reader means whoever needs the token from stdin waits on it.
|
|
71
|
+
*/
|
|
72
|
+
const stdinLines = process.stdin !== null && process.stdin.readable
|
|
73
|
+
? createInterface({ input: process.stdin, terminal: false })
|
|
74
|
+
: undefined;
|
|
75
|
+
if (stdinLines !== undefined) {
|
|
62
76
|
stdinLines.on('line', (line) => {
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
if (tokenFromStdin)
|
|
78
|
+
return;
|
|
79
|
+
tokenFromStdin = true;
|
|
80
|
+
const token = line.trim();
|
|
81
|
+
if (token !== '')
|
|
82
|
+
RPC_TOKEN = token;
|
|
67
83
|
});
|
|
68
84
|
stdinLines.on('close', () => {
|
|
69
|
-
if (RPC_TOKEN === ''
|
|
85
|
+
if (RPC_TOKEN === '') {
|
|
70
86
|
process.stderr.write('[dsh-browser host] warning: no token received on stdin or env\n');
|
|
71
87
|
}
|
|
72
88
|
});
|
|
@@ -819,12 +835,12 @@ async function handle(op, msg) {
|
|
|
819
835
|
try {
|
|
820
836
|
let image;
|
|
821
837
|
try {
|
|
822
|
-
image = await entry.webContentsView.webContents.capturePage();
|
|
838
|
+
image = await withTimeout(entry.webContentsView.webContents.capturePage(), COMMAND_TIMEOUT_MS, 'capturePage');
|
|
823
839
|
}
|
|
824
840
|
catch (error) {
|
|
825
841
|
process.stderr.write(`[dsh-browser host] capturePage failed: ${String(error)}\n`);
|
|
826
842
|
await new Promise(resolve => setTimeout(resolve, 400));
|
|
827
|
-
image = await entry.webContentsView.webContents.capturePage();
|
|
843
|
+
image = await withTimeout(entry.webContentsView.webContents.capturePage(), COMMAND_TIMEOUT_MS, 'capturePage');
|
|
828
844
|
}
|
|
829
845
|
// Downscale to fit the requested box, preserving aspect ratio.
|
|
830
846
|
const size = image.getSize();
|
|
@@ -870,7 +886,7 @@ async function handle(op, msg) {
|
|
|
870
886
|
catch { /* already gone */ }
|
|
871
887
|
}
|
|
872
888
|
try {
|
|
873
|
-
const shot = await entry.webContentsView.webContents.debugger.sendCommand('Page.captureScreenshot', {});
|
|
889
|
+
const shot = await withTimeout(entry.webContentsView.webContents.debugger.sendCommand('Page.captureScreenshot', {}), COMMAND_TIMEOUT_MS, 'Page.captureScreenshot');
|
|
874
890
|
const data = shot.data;
|
|
875
891
|
if (typeof data === 'string' && data.length > 0)
|
|
876
892
|
base64 = data;
|
|
@@ -918,7 +934,11 @@ async function handle(op, msg) {
|
|
|
918
934
|
// the parent's memory or hit the single-line cap. This also avoids
|
|
919
935
|
// Electron's download pipeline entirely (CDP debugger attach can
|
|
920
936
|
// interfere with will-download).
|
|
921
|
-
|
|
937
|
+
// Bounded like every other op. The evaluate awaits a page-side fetch, so a slow or
|
|
938
|
+
// unresponsive server held the serial queue open forever: handle() never settled,
|
|
939
|
+
// every later op queued behind it, and the parent's plain timeout does not restart
|
|
940
|
+
// the host.
|
|
941
|
+
const result = await withTimeout(entry.webContentsView.webContents.debugger.sendCommand('Runtime.evaluate', {
|
|
922
942
|
// Stream the body through a reader so the size cap is enforced as
|
|
923
943
|
// bytes arrive — never buffer an unbounded download into memory
|
|
924
944
|
// before checking the limit (a huge URL would otherwise OOM the
|
|
@@ -959,7 +979,7 @@ async function handle(op, msg) {
|
|
|
959
979
|
})()`,
|
|
960
980
|
awaitPromise: true,
|
|
961
981
|
returnByValue: true,
|
|
962
|
-
});
|
|
982
|
+
}), COMMAND_TIMEOUT_MS, 'download evaluate');
|
|
963
983
|
const value = result.result?.value;
|
|
964
984
|
if (typeof value !== 'string') {
|
|
965
985
|
const detail = result.exceptionDetails;
|
|
@@ -994,7 +1014,21 @@ async function handle(op, msg) {
|
|
|
994
1014
|
throw new Error(`flushAuth: unknown view ${viewId}`);
|
|
995
1015
|
// Export the session's cookies so login state can be saved/restored
|
|
996
1016
|
// across browser hosts (or shared with another machine).
|
|
997
|
-
|
|
1017
|
+
//
|
|
1018
|
+
// Scoped to this view's own page. The jar is SHARED on purpose: views here are built
|
|
1019
|
+
// without a partition and every window drives the same browser, so signing in once
|
|
1020
|
+
// works everywhere. What must not be shared is the HANDING OVER — an unfiltered read
|
|
1021
|
+
// would put every domain every task has visited, plus whatever the human is signed
|
|
1022
|
+
// into, in front of one task's caller. Least exposure, not isolation. An unknown page
|
|
1023
|
+
// refuses rather than widening.
|
|
1024
|
+
//
|
|
1025
|
+
// Bounded like every other op: a renderer that stops answering here would hold the
|
|
1026
|
+
// serial queue forever, and the parent's plain timeout does not restart the host.
|
|
1027
|
+
const pageUrl = entry.webContentsView.webContents.getURL();
|
|
1028
|
+
if (pageUrl === '' || pageUrl === 'about:blank') {
|
|
1029
|
+
throw new Error('flushAuth: this view has no page loaded, so the cookie scope is unknown — reading everything would expose other sessions');
|
|
1030
|
+
}
|
|
1031
|
+
const cookies = await withTimeout(entry.webContentsView.webContents.session.cookies.get({ url: pageUrl }), COMMAND_TIMEOUT_MS, 'cookies.get');
|
|
998
1032
|
const exported = cookies.map(c => {
|
|
999
1033
|
// Electron types the cookie domain as optional; a missing domain
|
|
1000
1034
|
// cannot be exported meaningfully, so default to '' (matches the
|
|
@@ -1032,7 +1066,9 @@ async function handle(op, msg) {
|
|
|
1032
1066
|
for (const c of cookies) {
|
|
1033
1067
|
if (typeof c.url !== 'string' || typeof c.name !== 'string' || typeof c.value !== 'string')
|
|
1034
1068
|
continue;
|
|
1035
|
-
|
|
1069
|
+
// Bounded per cookie: this runs once for every cookie, so a single hang held the
|
|
1070
|
+
// serial queue open forever with no path to recovery.
|
|
1071
|
+
await withTimeout(entry.webContentsView.webContents.session.cookies.set({
|
|
1036
1072
|
url: c.url,
|
|
1037
1073
|
name: c.name,
|
|
1038
1074
|
value: c.value,
|
|
@@ -1041,7 +1077,7 @@ async function handle(op, msg) {
|
|
|
1041
1077
|
...typeof c.secure === 'boolean' ? { secure: c.secure } : {},
|
|
1042
1078
|
...typeof c.httpOnly === 'boolean' ? { httpOnly: c.httpOnly } : {},
|
|
1043
1079
|
...typeof c.expirationDate === 'number' ? { expirationDate: c.expirationDate } : {},
|
|
1044
|
-
});
|
|
1080
|
+
}), COMMAND_TIMEOUT_MS, 'cookies.set');
|
|
1045
1081
|
restored++;
|
|
1046
1082
|
}
|
|
1047
1083
|
reply(msg.id, { ok: true, result: { restored } });
|
|
@@ -1110,17 +1146,19 @@ void app.whenReady().then(() => {
|
|
|
1110
1146
|
socket.write(JSON.stringify({ id: 0, op: 'hello', token: RPC_TOKEN }) + '\n');
|
|
1111
1147
|
};
|
|
1112
1148
|
if (RPC_TOKEN !== '') {
|
|
1149
|
+
// Either stdin already delivered a line (it wins) or env supplied the fallback.
|
|
1113
1150
|
writeHello();
|
|
1114
1151
|
}
|
|
1115
1152
|
else {
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
//
|
|
1153
|
+
// Neither yet: wait for stdin's first line with a bounded fallback, then send whatever
|
|
1154
|
+
// we have — an empty hello is refused by the parent, which then fails cleanly. The
|
|
1155
|
+
// reader is the module-level one, NOT a second createInterface on the same stream: two
|
|
1156
|
+
// readers of one stdin both consume its lines, so the second one never saw the token.
|
|
1119
1157
|
const fallback = setTimeout(() => writeHello(), 2_000);
|
|
1120
|
-
if (
|
|
1121
|
-
const
|
|
1122
|
-
stdinLines.once('line',
|
|
1123
|
-
stdinLines.once('close',
|
|
1158
|
+
if (stdinLines !== undefined) {
|
|
1159
|
+
const send = () => { clearTimeout(fallback); writeHello(); };
|
|
1160
|
+
stdinLines.once('line', send);
|
|
1161
|
+
stdinLines.once('close', send);
|
|
1124
1162
|
}
|
|
1125
1163
|
}
|
|
1126
1164
|
const rl = createInterface({ input: socket });
|
|
@@ -1156,7 +1194,17 @@ void app.whenReady().then(() => {
|
|
|
1156
1194
|
// without cleanup, exit so no zombie Electron window is left behind.
|
|
1157
1195
|
socket.on('close', () => {
|
|
1158
1196
|
process.stderr.write('[dsh-browser host] parent connection closed, exiting\n');
|
|
1159
|
-
app.exit(0)
|
|
1197
|
+
// A normal quit, not `app.exit(0)`. exit() skips before-quit/will-quit and does not wait
|
|
1198
|
+
// for Chromium to finish writing the profile — the leveldb files holding cookies and site
|
|
1199
|
+
// data. This path runs on every dispose, and a session that has just logged in is exactly
|
|
1200
|
+
// when the profile holds unwritten state, so a hard exit could leave a half-written leveldb
|
|
1201
|
+
// behind, which is what makes a browser profile fail to open on the next launch.
|
|
1202
|
+
app.quit();
|
|
1203
|
+
// Belt and braces: the parent is already gone, so there is nothing left to serve. If a
|
|
1204
|
+
// listener or a window blocks the quit, forcing the exit after a grace period is still
|
|
1205
|
+
// strictly better than an orphaned Electron window the user has to close by hand.
|
|
1206
|
+
const force = setTimeout(() => app.exit(0), 5000);
|
|
1207
|
+
force.unref();
|
|
1160
1208
|
});
|
|
1161
1209
|
// Keep the process alive until the parent closes the socket or kills us.
|
|
1162
1210
|
});
|
|
@@ -204,7 +204,16 @@ export interface ElectronBrowserProviderConfig {
|
|
|
204
204
|
readonly settings?: () => BrowserSettings;
|
|
205
205
|
/** Maximum snapshot elements before truncation. Default 60. */
|
|
206
206
|
readonly snapshotMaxElements?: number;
|
|
207
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Explicit content cap, overriding the per-format default when set.
|
|
209
|
+
*
|
|
210
|
+
* Honoured again, and it is worth saying why that is a fix rather than a feature: this was
|
|
211
|
+
* `config.contentMaxChars ?? 100_000`, one number for every format, and when the caps became
|
|
212
|
+
* per-format (html and json 50 000, otherwise 20 000) the field was left assigned and never
|
|
213
|
+
* read. A configuration knob that silently does nothing is worse than no knob — it is an
|
|
214
|
+
* operator's decision that quietly went nowhere. Leaving it unset means the per-format
|
|
215
|
+
* default; setting it means the operator meant it.
|
|
216
|
+
*/
|
|
208
217
|
readonly contentMaxChars?: number;
|
|
209
218
|
/**
|
|
210
219
|
* Directory `browser_download` save paths must resolve inside (prevents a
|
|
@@ -390,8 +399,26 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
390
399
|
* Chromium drops `Input.*` events for a view with no display surface, so this
|
|
391
400
|
* runs before click/type/key and fails loudly (BROWSER_VIEW_NOT_PRESENTED)
|
|
392
401
|
* rather than reporting a success the page never saw.
|
|
402
|
+
* @param s - the session whose active view must be presented.
|
|
403
|
+
* @param signal - optional cancellation.
|
|
404
|
+
* @returns the view the session's active tab has AFTER the presentation barrier.
|
|
393
405
|
*/
|
|
394
406
|
private present;
|
|
407
|
+
/**
|
|
408
|
+
* Refuse to send synthesized input to a view that is no longer the one it was located
|
|
409
|
+
* against.
|
|
410
|
+
*
|
|
411
|
+
* `locateHandle` was captured before a page-side locate that can consume its whole
|
|
412
|
+
* 10s budget; the session's active tab is re-read afterwards. If they differ, the human
|
|
413
|
+
* switched tabs mid-flight (the product's whole point is that they can take over), and
|
|
414
|
+
* the coordinates belong to one page while the dispatch would go to another. Comparing
|
|
415
|
+
* the handles turns that into a loud, retryable error instead of input the caller
|
|
416
|
+
* believes landed.
|
|
417
|
+
* @param session - the session id, for the message.
|
|
418
|
+
* @param locateHandle - the view the locate ran in.
|
|
419
|
+
* @param liveHandle - the view that is active now.
|
|
420
|
+
*/
|
|
421
|
+
private assertSameView;
|
|
395
422
|
/**
|
|
396
423
|
* Give the view web focus before synthesizing keyboard input.
|
|
397
424
|
*
|
|
@@ -498,6 +525,22 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
498
525
|
* of text (`a@href` yields an absolute URL).
|
|
499
526
|
*/
|
|
500
527
|
scrape(session: BrowserSessionId, request: BrowserScrapeRequest, signal?: AbortSignal): Promise<BrowserScrapeResult>;
|
|
528
|
+
/**
|
|
529
|
+
* Refuse an action the settings have switched off.
|
|
530
|
+
*
|
|
531
|
+
* These switches belong to the OPERATOR. The settings document is written through the
|
|
532
|
+
* plugin's own panel and no tool can reach it, so — unlike `browser_restrict`, which the
|
|
533
|
+
* model owns and can lift at will — a refusal here stands for as long as the setting does.
|
|
534
|
+
* That is the point of them: a deployment can take page-script execution, downloads, or
|
|
535
|
+
* login-state writes off the table without relying on the model's cooperation.
|
|
536
|
+
*
|
|
537
|
+
* Read through `settingsSource` on every call rather than captured at construction, so
|
|
538
|
+
* flipping a switch applies to the next command instead of the next restart — the same
|
|
539
|
+
* rule the credentials gate follows.
|
|
540
|
+
* @param action - which switch to consult.
|
|
541
|
+
* @throws BrowserError when that switch is off.
|
|
542
|
+
*/
|
|
543
|
+
private assertActionAllowed;
|
|
501
544
|
/**
|
|
502
545
|
* Admit a caller-supplied save path for a file the browser writes to disk.
|
|
503
546
|
* ONE gate for both `browser_download` and `browser_screenshot`: the path
|