local-operator-ui 0.34.31 → 0.34.33
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/out/main/index.js +838 -499
- package/out/preload/index.js +4 -0
- package/out/renderer/assets/{_basePickBy-gsWNnL8j.js → _basePickBy-CNMIid3L.js} +1 -1
- package/out/renderer/assets/{_baseUniq-C5l0J_O6.js → _baseUniq-BesnMR63.js} +1 -1
- package/out/renderer/assets/{agent-details-page-DOUGX96x.js → agent-details-page-B1wPZHYw.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-DABmkR95.js → agent-hub-page-zLDCcJ0-.js} +1 -1
- package/out/renderer/assets/{agents-page-C0hiW8Y-.js → agents-page-8cT_dI_3.js} +3 -3
- package/out/renderer/assets/{arc-BTaq9nTj.js → arc-MuuzF-kz.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-wVAAAp4Q.js → architectureDiagram-IEHRJDOE-Cla6EwFi.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-Bs4G73iB.js → blockDiagram-JOT3LUYC-Bja9j7dE.js} +1 -1
- package/out/renderer/assets/{book-open-D3nH3Oq3.js → book-open-CKkVZcBo.js} +1 -1
- package/out/renderer/assets/browser-page-BYjoph3Z.js +1 -0
- package/out/renderer/assets/{browser-webauthn-prompt-B9kmFz6z.js → browser-webauthn-prompt-B3uYCxFo.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-D0q1Qpyu.js → c4Diagram-VJAJSXHY-DVoi6Fpy.js} +1 -1
- package/out/renderer/assets/channel-BQiffY-M.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-CvHQheuo.js → chunk-4BMEZGHF-Cih6BNjs.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-BjUHeria.js → chunk-A2AXSNBT-DJo0efSa.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-CgyS77iO.js → chunk-AEK57VVT-DvwXfXNo.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-C7o-nYwn.js → chunk-D6G4REZN-HuVYXeWr.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-BusO_5Tl.js → chunk-RZ5BOZE2-BPLh7jSP.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-CJWkR_g_.js → chunk-XZIHB7SX-Di-TZ5J_.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-DDObgTCR.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-DDObgTCR.js +1 -0
- package/out/renderer/assets/clone-C0j0lwrL.js +1 -0
- package/out/renderer/assets/{console-mirror-Bzwctj4T.js → console-mirror-BAq7R11H.js} +1 -1
- package/out/renderer/assets/{consoleCapture-D4doKdYL.js → consoleCapture-CWJ7Wd4s.js} +1 -1
- package/out/renderer/assets/{conversation-input-sync-CyV2Rt9s.js → conversation-input-sync-Dvqtl3Fr.js} +248 -244
- package/out/renderer/assets/{dagre-OKDRZEBW-Bbq7XOEU.js → dagre-OKDRZEBW-BRG1M9oi.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-CmaRShOS.js → diagram-SSKATNLV-DsHLzFSC.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-vChIl-Zs.js → diagram-VNBRO52H-DuDF21bn.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-DXqNkkeq.js → erDiagram-Q7BY3M3F-BS7Xa-1R.js} +1 -1
- package/out/renderer/assets/{error-boundary-zZlBqg1b.js → error-boundary-CQILQtMV.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-DsGksM3Z.js → flowDiagram-4HSFHLVR-A_BvmOjq.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-C9ODwIj2.js → ganttDiagram-APWFNJXF-BQX036BE.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-s2DTzfkq.js → gitGraphDiagram-7IBYFJ6S-YpfcZ3NA.js} +1 -1
- package/out/renderer/assets/{graph-Cj8Vtlbg.js → graph-lKMrX7qR.js} +1 -1
- package/out/renderer/assets/{index-IINfVLtx.js → index-BEYFd0J9.js} +189 -189
- package/out/renderer/assets/{index-Caxk5tgb.js → index-CYvymE2l.js} +1 -1
- package/out/renderer/assets/index-DBAaqMbw.css +1 -0
- package/out/renderer/assets/{index-e7_5cYB9.js → index-DGSndO-f.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-C0dwbbo2.js → infoDiagram-PH2N3AL5-Dg-ZnTmu.js} +1 -1
- package/out/renderer/assets/{installer-BqHXNN_Q.js → installer-Cgr6muou.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-Ce_-6oPh.js → journeyDiagram-U35MCT3I-DRzUPcQU.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-BfC2rELG.js → kanban-definition-NDS4AKOZ-Q474R71W.js} +1 -1
- package/out/renderer/assets/{layout-C8qa96WZ.js → layout-DTC_hgF6.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-oxgL0K8w.js → legacy-agents-page-k28FA6cr.js} +2 -2
- package/out/renderer/assets/{mermaid.core-XU5D2QM-.js → mermaid.core-C9rYU-0l.js} +5 -5
- package/out/renderer/assets/{mesh-page-CKWQJjR9.js → mesh-page-DfZZjDNB.js} +1 -1
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-BljWsgbL.js → mindmap-definition-ALO5MXBD-Bib16-Jn.js} +1 -1
- package/out/renderer/assets/{mini-Dx_pCNq_.js → mini-BJOZ3ClI.js} +1 -1
- package/out/renderer/assets/notebook-pen-DU0uOGQD.js +6 -0
- package/out/renderer/assets/{org-surface-gate-C_mfNSkA.js → org-surface-gate-C3vaLLOm.js} +1 -1
- package/out/renderer/assets/{page-header-Du_WrlsN.js → page-header-B0mwcG_m.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-0DtrHJC4.js → pieDiagram-IB7DONF6-CFKHjx11.js} +1 -1
- package/out/renderer/assets/{projects-page-BtO7qveV.js → projects-page-BCQMniR-.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-LH8QQXdE.js → quadrantDiagram-7GDLP6J5-B-9tF5TA.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-BXPsHVb3.js → radar-MK3ICKWK-hPJOSxyO.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-BDIMPwPh.js → radient-auth-buttons-CGDfKjvU.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-NUiela-Z.js → requirementDiagram-KVF5MWMF-DapE7E6j.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-Bv32xiGK.js → sankeyDiagram-QLVOVGJD-dQnFf6xh.js} +1 -1
- package/out/renderer/assets/{schedules-page-ZOQ5Aa7i.js → schedules-page-CTF8k7kQ.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-DhKfMbpL.js → sequenceDiagram-X6HHIX6F-DpQcD4s7.js} +1 -1
- package/out/renderer/assets/{settings-page-J0ziKUQu.js → settings-page-Cjj6lVqM.js} +60 -60
- package/out/renderer/assets/{stateDiagram-DGXRK772-DtuHtZqu.js → stateDiagram-DGXRK772-BH86RKPp.js} +1 -1
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CPbeGHOQ.js +1 -0
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-DaghAT0K.js → timeline-definition-BDJGKUSR-I9WIfb3F.js} +1 -1
- package/out/renderer/assets/{ui-preferences-store-C0rHfuT8.js → ui-preferences-store-BiKpLkJR.js} +1 -1
- package/out/renderer/assets/{use-download-agent-mutation-Uqc8JSLO.js → use-download-agent-mutation-DZKciluh.js} +1 -1
- package/out/renderer/assets/{use-memberships-query-Bf_-KANH.js → use-memberships-query-CouwB8Jj.js} +1 -1
- package/out/renderer/assets/{use-session-snapshot-L5kUKUQN.js → use-session-snapshot-BwZz7qKM.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-Q-Qk3b4A.js → xychartDiagram-VJFVF3MP-RQXd6xeu.js} +1 -1
- package/out/renderer/console-capture.html +5 -5
- package/out/renderer/index.html +7 -7
- package/out/renderer/installer.html +4 -4
- package/out/renderer/mini.html +7 -7
- package/package.json +2 -2
- package/out/renderer/assets/browser-page-PLs7fq5C.js +0 -1
- package/out/renderer/assets/channel-DsXie7lp.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-CcOoxcHZ.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-CcOoxcHZ.js +0 -1
- package/out/renderer/assets/clone-r9efBuQF.js +0 -1
- package/out/renderer/assets/index-B9VTDCIg.css +0 -1
- package/out/renderer/assets/settings-section-CvCfGlJR.js +0 -6
- package/out/renderer/assets/stateDiagram-v2-YXO3MK2T-CQ7XIglm.js +0 -1
- /package/out/renderer/assets/{index-FkCpfjDn.js → index-CtslrM_p.js} +0 -0
package/out/main/index.js
CHANGED
|
@@ -4618,6 +4618,55 @@ const serverChannelVerdict = ({
|
|
|
4618
4618
|
return { status: "restart-required", state: "restart-required" };
|
|
4619
4619
|
return { status: "current", state: "current" };
|
|
4620
4620
|
};
|
|
4621
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
4622
|
+
const isInstallerWheel = (entry, version, platform) => {
|
|
4623
|
+
if (!entry || typeof entry !== "object") return false;
|
|
4624
|
+
const file = entry;
|
|
4625
|
+
if (file.packagetype !== "bdist_wheel" || file.yanked) return false;
|
|
4626
|
+
if (typeof file.url !== "string") return false;
|
|
4627
|
+
let url;
|
|
4628
|
+
try {
|
|
4629
|
+
url = new URL(file.url);
|
|
4630
|
+
} catch {
|
|
4631
|
+
return false;
|
|
4632
|
+
}
|
|
4633
|
+
if (url.protocol !== "https:" || url.hostname !== "files.pythonhosted.org")
|
|
4634
|
+
return false;
|
|
4635
|
+
const digests = file.digests;
|
|
4636
|
+
const sha256 = digests && typeof digests === "object" ? digests.sha256 : null;
|
|
4637
|
+
if (typeof sha256 !== "string" || !SHA256_HEX.test(sha256)) return false;
|
|
4638
|
+
if (typeof file.filename !== "string") return false;
|
|
4639
|
+
if (!file.filename.startsWith(`local_operator-${version}-`)) return false;
|
|
4640
|
+
return platform === "darwin" ? file.filename.endsWith("-py3-none-macosx_11_0_universal2.whl") : file.filename.endsWith("-py3-none-any.whl");
|
|
4641
|
+
};
|
|
4642
|
+
const pypiReleaseReading = (payload, platform) => {
|
|
4643
|
+
const unreadable = {
|
|
4644
|
+
namedVersion: null,
|
|
4645
|
+
readyVersion: null
|
|
4646
|
+
};
|
|
4647
|
+
if (!payload || typeof payload !== "object") return unreadable;
|
|
4648
|
+
const info = payload.info;
|
|
4649
|
+
if (!info || typeof info !== "object") return unreadable;
|
|
4650
|
+
const version = info.version;
|
|
4651
|
+
if (typeof version !== "string" || !version.trim()) return unreadable;
|
|
4652
|
+
const trimmed = version.trim();
|
|
4653
|
+
const downloadable = (files) => Array.isArray(files) && files.some((entry) => isInstallerWheel(entry, trimmed, platform));
|
|
4654
|
+
let ready = downloadable(payload.urls);
|
|
4655
|
+
if (!ready) {
|
|
4656
|
+
const releases = payload.releases;
|
|
4657
|
+
if (releases && typeof releases === "object") {
|
|
4658
|
+
ready = downloadable(releases[trimmed]);
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
return { namedVersion: trimmed, readyVersion: ready ? trimmed : null };
|
|
4662
|
+
};
|
|
4663
|
+
const readPypiReleaseBody = (body, platform) => {
|
|
4664
|
+
try {
|
|
4665
|
+
return pypiReleaseReading(JSON.parse(body), platform);
|
|
4666
|
+
} catch {
|
|
4667
|
+
return { namedVersion: null, readyVersion: null };
|
|
4668
|
+
}
|
|
4669
|
+
};
|
|
4621
4670
|
const updateCheckVerdict = (channels) => ({
|
|
4622
4671
|
app: channels.app,
|
|
4623
4672
|
server: channels.server,
|
|
@@ -14800,12 +14849,14 @@ class BrowserStateWriter {
|
|
|
14800
14849
|
this.onError = options.onError ?? (() => {
|
|
14801
14850
|
});
|
|
14802
14851
|
this.appVersion = options.appVersion;
|
|
14852
|
+
this.instanceId = options.instanceId;
|
|
14803
14853
|
}
|
|
14804
14854
|
now;
|
|
14805
14855
|
onError;
|
|
14806
14856
|
timer = null;
|
|
14807
14857
|
written = false;
|
|
14808
14858
|
appVersion;
|
|
14859
|
+
instanceId;
|
|
14809
14860
|
/** Publish the record and start the heartbeat. The heartbeat is the
|
|
14810
14861
|
* difference between "the app is running and can open a tab on demand" and a
|
|
14811
14862
|
* record that names a process that died without cleaning up. */
|
|
@@ -14837,6 +14888,7 @@ class BrowserStateWriter {
|
|
|
14837
14888
|
proto: PROTO_VERSION,
|
|
14838
14889
|
host: "ui",
|
|
14839
14890
|
app_version: this.appVersion,
|
|
14891
|
+
...this.instanceId && { instance_id: this.instanceId },
|
|
14840
14892
|
profile_dir: facts.profileDir,
|
|
14841
14893
|
// Advertised from the ONE list the `/health` route also reads (§6.3), so the
|
|
14842
14894
|
// record a session reads and the probe Python acquits a stale record with
|
|
@@ -24117,6 +24169,57 @@ function restrictMode(path2, log2) {
|
|
|
24117
24169
|
);
|
|
24118
24170
|
}
|
|
24119
24171
|
}
|
|
24172
|
+
class StartupRollback {
|
|
24173
|
+
owners = [];
|
|
24174
|
+
add(cleanup) {
|
|
24175
|
+
this.owners.push(cleanup);
|
|
24176
|
+
}
|
|
24177
|
+
commit() {
|
|
24178
|
+
this.owners = [];
|
|
24179
|
+
}
|
|
24180
|
+
async run(log2) {
|
|
24181
|
+
for (const cleanup of this.owners.splice(0).reverse()) {
|
|
24182
|
+
try {
|
|
24183
|
+
await cleanup();
|
|
24184
|
+
} catch (error) {
|
|
24185
|
+
log2(`[browser] startup cleanup failed: ${String(error)}`);
|
|
24186
|
+
}
|
|
24187
|
+
}
|
|
24188
|
+
}
|
|
24189
|
+
}
|
|
24190
|
+
const MAX_START_ATTEMPTS = 3;
|
|
24191
|
+
async function startWithRetries(options) {
|
|
24192
|
+
let failure = new Error("The browser window closed before startup.");
|
|
24193
|
+
for (let attempt = 1; attempt <= MAX_START_ATTEMPTS; attempt += 1) {
|
|
24194
|
+
if (options.cancelled()) break;
|
|
24195
|
+
options.publish({
|
|
24196
|
+
status: attempt === 1 ? "starting" : "retrying",
|
|
24197
|
+
attempt,
|
|
24198
|
+
maxAttempts: MAX_START_ATTEMPTS,
|
|
24199
|
+
message: null,
|
|
24200
|
+
canRetry: false
|
|
24201
|
+
});
|
|
24202
|
+
try {
|
|
24203
|
+
return await options.start();
|
|
24204
|
+
} catch (error) {
|
|
24205
|
+
failure = error;
|
|
24206
|
+
options.log(
|
|
24207
|
+
`[browser] startup attempt ${attempt}/${MAX_START_ATTEMPTS} failed: ${String(error)}`
|
|
24208
|
+
);
|
|
24209
|
+
}
|
|
24210
|
+
}
|
|
24211
|
+
options.publish({
|
|
24212
|
+
status: "unavailable",
|
|
24213
|
+
attempt: MAX_START_ATTEMPTS,
|
|
24214
|
+
maxAttempts: MAX_START_ATTEMPTS,
|
|
24215
|
+
message: "The browser could not start. Your saved tabs and site approvals are kept; sessions you were signed into may need signing in again.",
|
|
24216
|
+
// The window is still open and no host is running once the sequence
|
|
24217
|
+
// exhausts, so the renderer may offer a retry. `startBrowserHost` is
|
|
24218
|
+
// idempotent on re-registration, which is what makes the control safe.
|
|
24219
|
+
canRetry: true
|
|
24220
|
+
});
|
|
24221
|
+
throw failure;
|
|
24222
|
+
}
|
|
24120
24223
|
const BROWSER_IPC_CHANNELS = [
|
|
24121
24224
|
"browser-state",
|
|
24122
24225
|
"browser-new-tab",
|
|
@@ -24138,19 +24241,53 @@ const BROWSER_IPC_CHANNELS = [
|
|
|
24138
24241
|
"browser-clear-data",
|
|
24139
24242
|
"browser-webauthn-respond",
|
|
24140
24243
|
"browser-webauthn-pending",
|
|
24141
|
-
"browser-reveal-downloads"
|
|
24244
|
+
"browser-reveal-downloads",
|
|
24245
|
+
/** The user's own recovery click on the `unavailable` state (round 1, D1a).
|
|
24246
|
+
* Named here so the namespace stays the one place that lists its channels. */
|
|
24247
|
+
"browser-retry"
|
|
24142
24248
|
];
|
|
24143
24249
|
function registerBrowserIpc(options) {
|
|
24144
|
-
function
|
|
24250
|
+
function authorizeSender(event) {
|
|
24145
24251
|
const owner = options.window();
|
|
24146
24252
|
if (!owner || owner.isDestroyed() || event.sender !== owner.webContents || event.senderFrame !== owner.webContents.mainFrame || !trustedDesktopFrame(event.senderFrame.url, options.expectedUrl)) {
|
|
24147
24253
|
throw new Error("This window cannot use the browser controls.");
|
|
24148
24254
|
}
|
|
24255
|
+
}
|
|
24256
|
+
function authorize(event) {
|
|
24257
|
+
authorizeSender(event);
|
|
24149
24258
|
const host = options.host();
|
|
24150
|
-
if (!host)
|
|
24259
|
+
if (!host)
|
|
24260
|
+
throw new Error("browser_unavailable: The browser is not ready.");
|
|
24151
24261
|
return host;
|
|
24152
24262
|
}
|
|
24153
|
-
electron.ipcMain.handle("browser-state", (event) =>
|
|
24263
|
+
electron.ipcMain.handle("browser-state", (event) => {
|
|
24264
|
+
authorizeSender(event);
|
|
24265
|
+
const host = options.host();
|
|
24266
|
+
const availability = options.availability?.();
|
|
24267
|
+
if (host)
|
|
24268
|
+
return { ...host.chromeState(), ...availability && { availability } };
|
|
24269
|
+
return {
|
|
24270
|
+
tabs: [],
|
|
24271
|
+
activeTabId: null,
|
|
24272
|
+
url: "",
|
|
24273
|
+
title: "",
|
|
24274
|
+
loading: false,
|
|
24275
|
+
canGoBack: false,
|
|
24276
|
+
canGoForward: false,
|
|
24277
|
+
pendingConsent: [],
|
|
24278
|
+
approvals: [],
|
|
24279
|
+
navFailure: null,
|
|
24280
|
+
availability: availability ?? {
|
|
24281
|
+
status: "unavailable",
|
|
24282
|
+
attempt: 0,
|
|
24283
|
+
maxAttempts: MAX_START_ATTEMPTS,
|
|
24284
|
+
message: "The browser is not running.",
|
|
24285
|
+
// No host invokes this fallback (it is the pre-start projection), and
|
|
24286
|
+
// there is nothing to retry before a start was asked for.
|
|
24287
|
+
canRetry: false
|
|
24288
|
+
}
|
|
24289
|
+
};
|
|
24290
|
+
});
|
|
24154
24291
|
electron.ipcMain.handle("browser-new-tab", async (event, sessionId2) => {
|
|
24155
24292
|
const host = authorize(event);
|
|
24156
24293
|
return host.newTab(optionalConversationOrThrow(sessionId2));
|
|
@@ -24276,6 +24413,13 @@ function registerBrowserIpc(options) {
|
|
|
24276
24413
|
);
|
|
24277
24414
|
return { opened: message === "", message };
|
|
24278
24415
|
});
|
|
24416
|
+
electron.ipcMain.handle("browser-retry", async (event) => {
|
|
24417
|
+
authorizeSender(event);
|
|
24418
|
+
if (!options.retry)
|
|
24419
|
+
throw new Error("browser_unavailable: Retry is not available.");
|
|
24420
|
+
await options.retry();
|
|
24421
|
+
return { retried: true };
|
|
24422
|
+
});
|
|
24279
24423
|
}
|
|
24280
24424
|
function unregisterBrowserIpc() {
|
|
24281
24425
|
for (const channel of BROWSER_IPC_CHANNELS) electron.ipcMain.removeHandler(channel);
|
|
@@ -24698,7 +24842,7 @@ function installBrowserSessionHandlers(browserSession, hooks = {}) {
|
|
|
24698
24842
|
return false;
|
|
24699
24843
|
}
|
|
24700
24844
|
);
|
|
24701
|
-
|
|
24845
|
+
const onDownload = (event, item, webContents) => {
|
|
24702
24846
|
const webContentsId = webContents?.id ?? -1;
|
|
24703
24847
|
const decision = hooks.onDownload?.(item, webContentsId) ?? {
|
|
24704
24848
|
cancel: true,
|
|
@@ -24718,7 +24862,11 @@ function installBrowserSessionHandlers(browserSession, hooks = {}) {
|
|
|
24718
24862
|
);
|
|
24719
24863
|
}
|
|
24720
24864
|
hooks.onDownloadDecided?.(outcome);
|
|
24721
|
-
}
|
|
24865
|
+
};
|
|
24866
|
+
browserSession.on("will-download", onDownload);
|
|
24867
|
+
return () => {
|
|
24868
|
+
browserSession.removeListener("will-download", onDownload);
|
|
24869
|
+
};
|
|
24722
24870
|
}
|
|
24723
24871
|
async function clearBrowsingData(browserSession, what) {
|
|
24724
24872
|
browserSession.flushStorageData();
|
|
@@ -24769,6 +24917,7 @@ function flushBrowserStorage(browserSession) {
|
|
|
24769
24917
|
const RPC_PATH = "/rpc";
|
|
24770
24918
|
const HEALTH_PATH = "/health";
|
|
24771
24919
|
const KEY_HEADER = "x-bridge-key";
|
|
24920
|
+
const INSTANCE_HEADER = "x-ui-browser-instance";
|
|
24772
24921
|
const MAX_BODY_BYTES = 1 << 20;
|
|
24773
24922
|
const LOOPBACK_HOST = "127.0.0.1";
|
|
24774
24923
|
function send(res, status2, body, contentType = "application/json") {
|
|
@@ -24895,6 +25044,7 @@ async function handle(req, res, options) {
|
|
|
24895
25044
|
host: "ui",
|
|
24896
25045
|
proto: PROTO_VERSION,
|
|
24897
25046
|
pid: process.pid,
|
|
25047
|
+
...options.instanceId && { instance_id: options.instanceId },
|
|
24898
25048
|
// Additive: an older reader requires `host` and `pid` and ignores the
|
|
24899
25049
|
// rest, and a newer one learns whether the console is up without a
|
|
24900
25050
|
// second probe (design 10.1). `false` when nobody reported a
|
|
@@ -24921,6 +25071,11 @@ async function handle(req, res, options) {
|
|
|
24921
25071
|
send(res, 401, { detail: "unauthorized" });
|
|
24922
25072
|
return;
|
|
24923
25073
|
}
|
|
25074
|
+
const expectedInstance = req.headers[INSTANCE_HEADER];
|
|
25075
|
+
if (expectedInstance !== void 0 && expectedInstance !== options.instanceId) {
|
|
25076
|
+
send(res, 409, { detail: "host instance changed" });
|
|
25077
|
+
return;
|
|
25078
|
+
}
|
|
24924
25079
|
let raw;
|
|
24925
25080
|
try {
|
|
24926
25081
|
raw = JSON.parse(await readBody(req));
|
|
@@ -26040,13 +26195,28 @@ function refusesNavigation(rawUrl) {
|
|
|
26040
26195
|
let activeHost = null;
|
|
26041
26196
|
let stopping = null;
|
|
26042
26197
|
function browserHostStopPending() {
|
|
26043
|
-
return activeHost !== null || stopping !== null;
|
|
26198
|
+
return activeHost !== null || stopping !== null || starting !== null;
|
|
26044
26199
|
}
|
|
26045
26200
|
async function stopBrowserHost() {
|
|
26046
26201
|
if (stopping) return stopping;
|
|
26202
|
+
if (starting) {
|
|
26203
|
+
cancelStartup = true;
|
|
26204
|
+
stopping = starting.catch(() => null).then(async () => {
|
|
26205
|
+
const current = activeHost;
|
|
26206
|
+
activeHost = null;
|
|
26207
|
+
if (current) await current.stop();
|
|
26208
|
+
else unregisterBrowserIpc();
|
|
26209
|
+
}).finally(() => {
|
|
26210
|
+
stopping = null;
|
|
26211
|
+
});
|
|
26212
|
+
return stopping;
|
|
26213
|
+
}
|
|
26047
26214
|
const handle2 = activeHost;
|
|
26048
26215
|
activeHost = null;
|
|
26049
|
-
if (!handle2)
|
|
26216
|
+
if (!handle2) {
|
|
26217
|
+
unregisterBrowserIpc();
|
|
26218
|
+
return;
|
|
26219
|
+
}
|
|
26050
26220
|
stopping = handle2.stop().finally(() => {
|
|
26051
26221
|
stopping = null;
|
|
26052
26222
|
});
|
|
@@ -26056,524 +26226,650 @@ function browserHostEnabled(env = process.env) {
|
|
|
26056
26226
|
const flag = env.LOCAL_OPERATOR_UI_BROWSER_HOST?.trim().toLowerCase();
|
|
26057
26227
|
return !(flag === "0" || flag === "false" || flag === "off");
|
|
26058
26228
|
}
|
|
26059
|
-
|
|
26060
|
-
|
|
26061
|
-
|
|
26062
|
-
|
|
26063
|
-
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
|
|
26067
|
-
|
|
26068
|
-
|
|
26069
|
-
// (a tab already gone), which the row renders the old way rather than guessing.
|
|
26070
|
-
ownerKindFor: (tabId) => registryForDownloads?.get(tabId)?.owner ?? null,
|
|
26071
|
-
log: log2,
|
|
26072
|
-
onActivity: () => {
|
|
26073
|
-
if (options.window.isDestroyed()) return;
|
|
26074
|
-
options.window.webContents.send("browser-state-changed");
|
|
26075
|
-
}
|
|
26076
|
-
});
|
|
26077
|
-
installBrowserSessionHandlers(browserSession, {
|
|
26078
|
-
onPermissionRequested: (details) => {
|
|
26079
|
-
log2(
|
|
26080
|
-
`[browser] denied a ${details.permission} permission request from ${details.origin || "(unknown origin)"}`
|
|
26081
|
-
);
|
|
26082
|
-
},
|
|
26083
|
-
onDownload: (item, webContentsId) => downloads.decide(item, webContentsId),
|
|
26084
|
-
onDownloadDecided: (outcome) => {
|
|
26085
|
-
if (outcome.savePath) {
|
|
26086
|
-
log2(
|
|
26087
|
-
`[browser] saved a download from ${outcome.url} to ${outcome.savePath}`
|
|
26088
|
-
);
|
|
26089
|
-
}
|
|
26090
|
-
},
|
|
26091
|
-
log: log2
|
|
26092
|
-
});
|
|
26093
|
-
configurePslRules(PSL_RULES);
|
|
26094
|
-
await installWebauthn({ log: log2 });
|
|
26095
|
-
const describeSource = (frame) => {
|
|
26096
|
-
if (!frame || typeof frame !== "object") {
|
|
26097
|
-
return { tabId: null, pageTitle: null };
|
|
26098
|
-
}
|
|
26099
|
-
const contents = electron.webContents.fromFrame(frame);
|
|
26100
|
-
const record = contents === void 0 ? void 0 : registry.byWebContentsId(contents.id);
|
|
26101
|
-
if (!record) return { tabId: null, pageTitle: null };
|
|
26102
|
-
const title = record.view.webContents.isDestroyed() ? "" : record.view.webContents.getTitle();
|
|
26103
|
-
return { tabId: record.tabId, pageTitle: title || null };
|
|
26104
|
-
};
|
|
26105
|
-
const webauthn = new WebauthnChooser({
|
|
26106
|
-
notify: (request) => {
|
|
26107
|
-
if (options.window.isDestroyed()) return;
|
|
26108
|
-
options.window.webContents.send("browser-webauthn-request", request);
|
|
26109
|
-
},
|
|
26110
|
-
/*
|
|
26111
|
-
* AND THE OTHER DIRECTION: every settle is pushed, not only every arrival.
|
|
26112
|
-
*
|
|
26113
|
-
* Without this the renderer cannot tell a live request from one main has
|
|
26114
|
-
* already cancelled, so a chooser whose request expired while the user was
|
|
26115
|
-
* away stays on screen offering rows that can no longer do anything (design
|
|
26116
|
-
* round 1, D2) — and the click is then discarded silently. The outcome travels
|
|
26117
|
-
* with the push because the two cases differ for the user: their own answer is
|
|
26118
|
-
* already off the screen, an expiry has to be explained.
|
|
26119
|
-
*/
|
|
26120
|
-
onSettled: (requestId2, outcome) => {
|
|
26121
|
-
if (options.window.isDestroyed()) return;
|
|
26122
|
-
options.window.webContents.send("browser-webauthn-settled", {
|
|
26123
|
-
requestId: requestId2,
|
|
26124
|
-
outcome
|
|
26125
|
-
});
|
|
26126
|
-
},
|
|
26127
|
-
describeSource,
|
|
26128
|
-
log: log2
|
|
26129
|
-
});
|
|
26130
|
-
const detachWebauthn = attachWebauthnChooser(browserSession, webauthn, log2);
|
|
26131
|
-
const cookieJar = createHiddenCookieJarTarget(
|
|
26132
|
-
new electron.WebContentsView({
|
|
26133
|
-
webPreferences: { ...HIDDEN_COOKIE_JAR_WEB_PREFERENCES }
|
|
26134
|
-
})
|
|
26135
|
-
);
|
|
26136
|
-
const sessionCookies = new SessionCookieVault({
|
|
26137
|
-
jar: cookieJar.jar,
|
|
26138
|
-
cipher: browserProfileCipher(),
|
|
26139
|
-
...sessionCookiePaths(options.userDataDir),
|
|
26140
|
-
flushStore: () => browserSession.cookies.flushStore(),
|
|
26141
|
-
clearSessionData: (what) => clearBrowsingData(browserSession, what),
|
|
26142
|
-
log: log2
|
|
26143
|
-
});
|
|
26144
|
-
let restoreReport = null;
|
|
26145
|
-
try {
|
|
26146
|
-
restoreReport = await sessionCookies.restore();
|
|
26147
|
-
} catch (error) {
|
|
26148
|
-
log2(`[browser] session cookies: the restore failed (${String(error)})`);
|
|
26149
|
-
}
|
|
26150
|
-
const cdp = new CdpPool({ log: log2 });
|
|
26151
|
-
const views = /* @__PURE__ */ new Map();
|
|
26152
|
-
let stateWriter = null;
|
|
26153
|
-
const sessionStore = new BrowserSessionStore({
|
|
26154
|
-
dir: path$1.join(options.userDataDir, "browser"),
|
|
26155
|
-
log: log2
|
|
26156
|
-
});
|
|
26157
|
-
const consentNotifier = new ConsentNotifier({
|
|
26158
|
-
show: options.windowShow,
|
|
26159
|
-
/*
|
|
26160
|
-
* The click's own rule lives in `consentClickHandler` (see it: the delivered
|
|
26161
|
-
* payload, the order, and the raise are all asserted there), and this is the
|
|
26162
|
-
* wiring that gives it the real window and the real plan. `windowShow` is the
|
|
26163
|
-
* plan the click comes forward under — this module still never decides whether a
|
|
26164
|
-
* window comes forward (design 11.4), it hands the plan to `window-raise.ts`.
|
|
26165
|
-
*/
|
|
26166
|
-
onAttention: consentClickHandler({
|
|
26167
|
-
/*
|
|
26168
|
-
* THE WINDOW IS ASKED FOR, NOT CAPTURED, and the difference is the U2 fix:
|
|
26169
|
-
* this host is stopped the moment its window closes (`src/main/index.ts`'s
|
|
26170
|
-
* `closed` hook), but a banner already raised is still in Notification
|
|
26171
|
-
* Center and its click still lands here. A captured window would be a
|
|
26172
|
-
* destroyed one by then, and reading it throws.
|
|
26173
|
-
*/
|
|
26174
|
-
window: () => options.window,
|
|
26175
|
-
show: options.windowShow,
|
|
26176
|
-
report: options.reportRaise,
|
|
26177
|
-
reopen: options.reopenConsent
|
|
26178
|
-
}),
|
|
26179
|
-
log: log2
|
|
26180
|
-
});
|
|
26181
|
-
const captureSession = () => {
|
|
26182
|
-
sessionStore.record(
|
|
26183
|
-
captureTabs(
|
|
26184
|
-
registry.list(),
|
|
26185
|
-
registry.activeTab?.tabId ?? null,
|
|
26186
|
-
host.failedTabIds()
|
|
26187
|
-
)
|
|
26188
|
-
);
|
|
26189
|
-
};
|
|
26190
|
-
const notifyChanged = () => {
|
|
26191
|
-
stateWriter?.publishNow();
|
|
26192
|
-
options.window.webContents.send("browser-state-changed");
|
|
26193
|
-
captureSession();
|
|
26194
|
-
};
|
|
26195
|
-
const registry = new TabRegistry(
|
|
26196
|
-
(_createOptions, tabId) => {
|
|
26197
|
-
const view = buildView(options.window);
|
|
26198
|
-
views.set(tabId, view);
|
|
26199
|
-
wireView(view, tabId);
|
|
26200
|
-
return view;
|
|
26201
|
-
},
|
|
26202
|
-
releaseTab,
|
|
26203
|
-
notifyChanged
|
|
26204
|
-
);
|
|
26205
|
-
registryForDownloads = registry;
|
|
26206
|
-
function releaseTab(tabId, webContentsId) {
|
|
26207
|
-
const view = views.get(tabId);
|
|
26208
|
-
views.delete(tabId);
|
|
26209
|
-
downloads.forget(tabId);
|
|
26210
|
-
void cdp.detach(webContentsId).finally(() => {
|
|
26211
|
-
releaseView(options.window, view);
|
|
26229
|
+
const hostInstanceId = node_crypto.randomUUID();
|
|
26230
|
+
let starting = null;
|
|
26231
|
+
let startingWindow = null;
|
|
26232
|
+
let cancelStartup = false;
|
|
26233
|
+
function startBrowserHost(options) {
|
|
26234
|
+
if (starting) {
|
|
26235
|
+
if (startingWindow === options.window) return starting;
|
|
26236
|
+
return starting.catch(() => null).then(async () => {
|
|
26237
|
+
await stopBrowserHost();
|
|
26238
|
+
return startBrowserHost(options);
|
|
26212
26239
|
});
|
|
26213
26240
|
}
|
|
26214
|
-
|
|
26215
|
-
|
|
26216
|
-
|
|
26217
|
-
|
|
26218
|
-
|
|
26219
|
-
|
|
26220
|
-
|
|
26221
|
-
|
|
26222
|
-
|
|
26223
|
-
|
|
26224
|
-
} catch {
|
|
26225
|
-
}
|
|
26226
|
-
return true;
|
|
26227
|
-
},
|
|
26228
|
-
isLive: (token) => {
|
|
26229
|
-
try {
|
|
26230
|
-
registry.requireSurface(token);
|
|
26231
|
-
return true;
|
|
26232
|
-
} catch {
|
|
26233
|
-
return false;
|
|
26234
|
-
}
|
|
26235
|
-
}
|
|
26236
|
-
});
|
|
26237
|
-
const approvals = new ApprovalStore({
|
|
26238
|
-
dir: path$1.join(options.userDataDir, "browser"),
|
|
26239
|
-
onChanged: () => {
|
|
26240
|
-
options.window.webContents.send("browser-consent-changed");
|
|
26241
|
-
consentNotifier.announce(approvals.pendingEntries());
|
|
26242
|
-
},
|
|
26243
|
-
log: log2
|
|
26244
|
-
});
|
|
26245
|
-
const profileDir = browserStoragePath(browserSession) ?? "";
|
|
26246
|
-
const facts = {
|
|
26247
|
-
proto: 1,
|
|
26248
|
-
appVersion: options.appVersion,
|
|
26249
|
-
profileDir,
|
|
26250
|
-
profilePersistent: browserSession.isPersistent(),
|
|
26251
|
-
userAgent: browserUserAgent(),
|
|
26252
|
-
domainScope: domainScopeAvailable()
|
|
26241
|
+
if (stopping) return stopping.then(() => startBrowserHost(options));
|
|
26242
|
+
if (activeHost) return Promise.resolve(activeHost);
|
|
26243
|
+
cancelStartup = false;
|
|
26244
|
+
startingWindow = options.window;
|
|
26245
|
+
let availability = {
|
|
26246
|
+
status: "starting",
|
|
26247
|
+
attempt: 1,
|
|
26248
|
+
maxAttempts: MAX_START_ATTEMPTS,
|
|
26249
|
+
message: null,
|
|
26250
|
+
canRetry: false
|
|
26253
26251
|
};
|
|
26254
|
-
const
|
|
26255
|
-
|
|
26256
|
-
|
|
26257
|
-
|
|
26258
|
-
|
|
26259
|
-
|
|
26260
|
-
log: log2,
|
|
26261
|
-
onChanged: notifyChanged,
|
|
26262
|
-
facts: () => facts,
|
|
26263
|
-
forgetSiteData: (origin) => clearOriginData(browserSession, origin)
|
|
26264
|
-
});
|
|
26265
|
-
const recorded = readSession(sessionStore.filePath, log2);
|
|
26266
|
-
host.restoreTabs(recorded);
|
|
26267
|
-
if (recorded.length) {
|
|
26268
|
-
log2(
|
|
26269
|
-
`[browser] restored ${recorded.length} tab(s) from ${SESSION_FILENAME}; every restored tab is user-owned and holds no handle (design 7.3)`
|
|
26270
|
-
);
|
|
26271
|
-
}
|
|
26272
|
-
const consoleStartup = await startConsoleHost({
|
|
26273
|
-
window: options.window,
|
|
26274
|
-
expectedUrl: options.expectedUrl,
|
|
26275
|
-
appVersion: options.appVersion,
|
|
26276
|
-
notifier: options.notifier,
|
|
26277
|
-
/*
|
|
26278
|
-
* The capture view's document and preload, forwarded rather than re-derived:
|
|
26279
|
-
* only the app knows where its own bundle lives, and a console host that
|
|
26280
|
-
* guessed would answer a screenshot with a blank frame rather than with its
|
|
26281
|
-
* own typed refusal. WITHOUT THESE TWO the host builds no capture view at all,
|
|
26282
|
-
* which the live proof rig caught as `capture_unavailable` on a surface with
|
|
26283
|
-
* no displayed pane — the exact shape of a seam that is wired in tests and
|
|
26284
|
-
* forgotten in the one call site that matters.
|
|
26285
|
-
*/
|
|
26286
|
-
consoleCaptureUrl: options.consoleCaptureUrl,
|
|
26287
|
-
preloadPath: options.preloadPath,
|
|
26288
|
-
userShellPath: options.userShellPath,
|
|
26289
|
-
log: log2
|
|
26290
|
-
});
|
|
26291
|
-
const sessionKey = mintSessionKey();
|
|
26292
|
-
const server = await startRpcServer({
|
|
26293
|
-
key: sessionKey,
|
|
26294
|
-
dispatch: async (method, params, requestId2) => {
|
|
26295
|
-
if (!isConsoleDispatchMethod(method)) {
|
|
26296
|
-
return host.dispatch(method, params, requestId2);
|
|
26297
|
-
}
|
|
26298
|
-
if (!consoleStartup.ok) {
|
|
26299
|
-
throw new ConsoleError(
|
|
26300
|
-
"console_unavailable",
|
|
26301
|
-
`this app's console is not available (${consoleStartup.reason}): ${consoleStartup.detail}`,
|
|
26302
|
-
{ reason: consoleStartup.reason }
|
|
26303
|
-
);
|
|
26304
|
-
}
|
|
26305
|
-
return consoleStartup.dispatch(method, params);
|
|
26306
|
-
},
|
|
26307
|
-
log: log2,
|
|
26308
|
-
capabilities: () => ({ console: consoleStartup.ok })
|
|
26309
|
-
});
|
|
26310
|
-
stateWriter = new BrowserStateWriter(
|
|
26311
|
-
stateFilePath(),
|
|
26312
|
-
() => {
|
|
26313
|
-
const consoleCounts = consoleStartup.ok ? consoleStartup.counts() : { total: 0, agent: 0 };
|
|
26314
|
-
return {
|
|
26315
|
-
tabs: registry.count(),
|
|
26316
|
-
agentTabs: registry.agentTabCount(),
|
|
26317
|
-
profileDir,
|
|
26318
|
-
console: consoleStartup.ok,
|
|
26319
|
-
consoleSurfaces: consoleCounts.total,
|
|
26320
|
-
consoleAgentSurfaces: consoleCounts.agent
|
|
26321
|
-
};
|
|
26322
|
-
},
|
|
26323
|
-
{
|
|
26324
|
-
appVersion: options.appVersion,
|
|
26325
|
-
onError: (error) => log2(`[browser] state file: ${String(error)}`)
|
|
26326
|
-
}
|
|
26327
|
-
);
|
|
26328
|
-
stateWriter.start(server.port, sessionKey);
|
|
26252
|
+
const bindings = {};
|
|
26253
|
+
const notify = () => {
|
|
26254
|
+
if (!options.window.isDestroyed())
|
|
26255
|
+
options.window.webContents.send("browser-state-changed");
|
|
26256
|
+
};
|
|
26257
|
+
unregisterBrowserIpc();
|
|
26329
26258
|
registerBrowserIpc({
|
|
26330
26259
|
window: () => options.window,
|
|
26331
26260
|
expectedUrl: options.expectedUrl,
|
|
26332
|
-
host: () => host,
|
|
26333
|
-
webauthn: () => webauthn,
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
// download.
|
|
26261
|
+
host: () => bindings.host?.() ?? null,
|
|
26262
|
+
webauthn: () => bindings.webauthn?.() ?? null,
|
|
26263
|
+
availability: () => availability,
|
|
26264
|
+
clearData: async (what) => {
|
|
26265
|
+
if (!bindings.clearData)
|
|
26266
|
+
throw new Error("browser_unavailable: The browser is not ready.");
|
|
26267
|
+
return bindings.clearData(what);
|
|
26268
|
+
},
|
|
26341
26269
|
revealDownloads: async () => {
|
|
26342
|
-
|
|
26343
|
-
|
|
26344
|
-
return
|
|
26270
|
+
if (!bindings.revealDownloads)
|
|
26271
|
+
throw new Error("browser_unavailable: The browser is not ready.");
|
|
26272
|
+
return bindings.revealDownloads();
|
|
26273
|
+
},
|
|
26274
|
+
// The recovery click on the `unavailable` state (round 1, D1a). It restarts
|
|
26275
|
+
// THIS module's host for the same window — the same call `createWindow`
|
|
26276
|
+
// makes — so the retry travels the one lifecycle path rather than a second,
|
|
26277
|
+
// parallel start. A doubled click needs no guard of its own:
|
|
26278
|
+
// `startBrowserHost` is already single-flight (a second call for the same
|
|
26279
|
+
// window returns the in-flight `starting` promise), which QA's concurrent
|
|
26280
|
+
// probe confirmed — three simultaneous retries produced ONE startup
|
|
26281
|
+
// sequence. Adding a local guard beside that would be a second mechanism
|
|
26282
|
+
// for a rule the module already owns (round 2, Q-R2-2).
|
|
26283
|
+
retry: async () => {
|
|
26284
|
+
await startBrowserHost(options).catch(() => {
|
|
26285
|
+
});
|
|
26345
26286
|
},
|
|
26346
|
-
log:
|
|
26287
|
+
log: options.log
|
|
26347
26288
|
});
|
|
26348
|
-
|
|
26349
|
-
|
|
26350
|
-
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26357
|
-
|
|
26358
|
-
|
|
26359
|
-
|
|
26360
|
-
|
|
26361
|
-
|
|
26362
|
-
|
|
26363
|
-
|
|
26364
|
-
|
|
26365
|
-
log2(
|
|
26366
|
-
`[browser] not overwriting ${SESSION_FILENAME} at stop: ${decision.reason}`
|
|
26367
|
-
);
|
|
26368
|
-
}
|
|
26369
|
-
sessionStore.flush();
|
|
26370
|
-
registry.destroyAll();
|
|
26371
|
-
await cdp.close();
|
|
26372
|
-
await server.close();
|
|
26373
|
-
await consoleStartup.stop();
|
|
26374
|
-
unregisterBrowserIpc();
|
|
26375
|
-
detachWebauthn();
|
|
26376
|
-
webauthn.dispose();
|
|
26377
|
-
stateWriter?.clear();
|
|
26378
|
-
approvals.resetPending();
|
|
26379
|
-
ownership.clear();
|
|
26380
|
-
flushBrowserStorage(browserSession);
|
|
26381
|
-
try {
|
|
26382
|
-
await sessionCookies.snapshot();
|
|
26383
|
-
} catch (error) {
|
|
26384
|
-
log2(
|
|
26385
|
-
`[browser] session cookies: the snapshot failed (${String(error)})`
|
|
26386
|
-
);
|
|
26387
|
-
}
|
|
26388
|
-
cookieJar.dispose();
|
|
26389
|
-
log2("[browser] host stopped");
|
|
26289
|
+
starting = startWithRetries({
|
|
26290
|
+
cancelled: () => cancelStartup || options.window.isDestroyed(),
|
|
26291
|
+
publish: (next) => {
|
|
26292
|
+
availability = next;
|
|
26293
|
+
notify();
|
|
26294
|
+
},
|
|
26295
|
+
log: options.log,
|
|
26296
|
+
start: async () => {
|
|
26297
|
+
const handle2 = await initializeBrowserHost(options, bindings);
|
|
26298
|
+
availability = {
|
|
26299
|
+
...availability,
|
|
26300
|
+
status: "ready",
|
|
26301
|
+
message: null,
|
|
26302
|
+
canRetry: false
|
|
26303
|
+
};
|
|
26304
|
+
notify();
|
|
26305
|
+
return handle2;
|
|
26390
26306
|
}
|
|
26307
|
+
}).finally(() => {
|
|
26308
|
+
starting = null;
|
|
26309
|
+
startingWindow = null;
|
|
26310
|
+
});
|
|
26311
|
+
return starting;
|
|
26312
|
+
}
|
|
26313
|
+
async function initializeBrowserHost(options, bindings) {
|
|
26314
|
+
const { log: log2 } = options;
|
|
26315
|
+
const rollback = new StartupRollback();
|
|
26316
|
+
let published = false;
|
|
26317
|
+
const checkpoint = () => {
|
|
26318
|
+
if (cancelStartup || options.window.isDestroyed())
|
|
26319
|
+
throw new Error("The browser window closed during startup.");
|
|
26391
26320
|
};
|
|
26392
|
-
|
|
26393
|
-
|
|
26394
|
-
|
|
26395
|
-
|
|
26396
|
-
|
|
26397
|
-
|
|
26398
|
-
|
|
26399
|
-
|
|
26400
|
-
|
|
26401
|
-
|
|
26402
|
-
|
|
26403
|
-
|
|
26404
|
-
|
|
26405
|
-
|
|
26406
|
-
|
|
26407
|
-
|
|
26408
|
-
|
|
26409
|
-
|
|
26410
|
-
|
|
26411
|
-
|
|
26412
|
-
|
|
26413
|
-
|
|
26414
|
-
|
|
26415
|
-
|
|
26416
|
-
|
|
26417
|
-
|
|
26418
|
-
|
|
26419
|
-
|
|
26321
|
+
try {
|
|
26322
|
+
let releaseTab = function(tabId, webContentsId) {
|
|
26323
|
+
const view = views.get(tabId);
|
|
26324
|
+
views.delete(tabId);
|
|
26325
|
+
downloads.forget(tabId);
|
|
26326
|
+
const release = cdp.detach(webContentsId).finally(() => releaseView(options.window, view));
|
|
26327
|
+
releases.add(release);
|
|
26328
|
+
void release.catch(() => {
|
|
26329
|
+
}).finally(() => releases.delete(release));
|
|
26330
|
+
}, buildView = function(window) {
|
|
26331
|
+
const view = new electron.WebContentsView({
|
|
26332
|
+
webPreferences: {
|
|
26333
|
+
partition: BROWSER_PARTITION,
|
|
26334
|
+
contextIsolation: true,
|
|
26335
|
+
nodeIntegration: false,
|
|
26336
|
+
webSecurity: true,
|
|
26337
|
+
allowRunningInsecureContent: false,
|
|
26338
|
+
sandbox: true
|
|
26339
|
+
// No `preload`: a driven page has no bridge to anything.
|
|
26340
|
+
}
|
|
26341
|
+
});
|
|
26342
|
+
window.contentView.addChildView(view);
|
|
26343
|
+
view.setBounds(BACKGROUND_VIEWPORT);
|
|
26344
|
+
view.setVisible(false);
|
|
26345
|
+
return view;
|
|
26346
|
+
}, wireView = function(view, tabId) {
|
|
26347
|
+
const contents = view.webContents;
|
|
26348
|
+
const popupChildren = /* @__PURE__ */ new Set();
|
|
26349
|
+
contents.setWindowOpenHandler((details) => {
|
|
26350
|
+
const decision = decidePopup(
|
|
26351
|
+
{ url: details.url, disposition: details.disposition },
|
|
26352
|
+
{ mode: options.windowShow, live: popupChildren.size }
|
|
26353
|
+
);
|
|
26354
|
+
if (!decision.allow) {
|
|
26355
|
+
log2(
|
|
26356
|
+
`[browser] refused a popup from a driven page: ${details.url} (${decision.reason})`
|
|
26357
|
+
);
|
|
26358
|
+
return { action: "deny" };
|
|
26359
|
+
}
|
|
26420
26360
|
log2(
|
|
26421
|
-
`[browser]
|
|
26361
|
+
`[browser] tab ${tabId} opened a popup: ${details.url} (disposition=${details.disposition}, presentation=${decision.presentation})`
|
|
26422
26362
|
);
|
|
26423
|
-
return {
|
|
26424
|
-
|
|
26425
|
-
|
|
26426
|
-
|
|
26427
|
-
|
|
26428
|
-
|
|
26429
|
-
|
|
26430
|
-
|
|
26431
|
-
|
|
26432
|
-
|
|
26433
|
-
|
|
26434
|
-
|
|
26435
|
-
|
|
26436
|
-
|
|
26437
|
-
|
|
26438
|
-
|
|
26439
|
-
|
|
26440
|
-
|
|
26441
|
-
|
|
26442
|
-
|
|
26443
|
-
|
|
26444
|
-
|
|
26445
|
-
|
|
26446
|
-
|
|
26447
|
-
|
|
26448
|
-
|
|
26449
|
-
|
|
26450
|
-
|
|
26451
|
-
|
|
26452
|
-
|
|
26453
|
-
|
|
26454
|
-
|
|
26455
|
-
|
|
26456
|
-
|
|
26457
|
-
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26464
|
-
// Not security-inherited either: a hidden popup must render like a
|
|
26465
|
-
// shown one so a capture of it is evidence (design 2.5).
|
|
26466
|
-
backgroundThrottling: options.backgroundThrottling
|
|
26467
|
-
// No `preload` — a popup has no bridge to anything, like the view.
|
|
26363
|
+
return {
|
|
26364
|
+
action: "allow",
|
|
26365
|
+
overrideBrowserWindowOptions: {
|
|
26366
|
+
width: 640,
|
|
26367
|
+
height: 720,
|
|
26368
|
+
minWidth: 480,
|
|
26369
|
+
minHeight: 560,
|
|
26370
|
+
center: true,
|
|
26371
|
+
frame: true,
|
|
26372
|
+
autoHideMenuBar: true,
|
|
26373
|
+
// Nothing shows itself: presentation happens only through
|
|
26374
|
+
// `presentPopupWindow` (window-raise.ts), under the launch plan's own
|
|
26375
|
+
// gate. Under `never` the window exists hidden and drivable over CDP.
|
|
26376
|
+
show: false,
|
|
26377
|
+
// Defence in depth beside the plan gate: a window that will never be
|
|
26378
|
+
// shown must also not be focusable — the same pairing `window-mode.ts`
|
|
26379
|
+
// applies to the main window in `headless`.
|
|
26380
|
+
focusable: decision.presentation !== "never",
|
|
26381
|
+
backgroundColor: "#FFFFFF",
|
|
26382
|
+
webPreferences: {
|
|
26383
|
+
/*
|
|
26384
|
+
* EXPLICIT, and load-bearing (design 2.3): `partition` is NOT among the
|
|
26385
|
+
* security-related prefs Electron merges from the parent, and a popup
|
|
26386
|
+
* created in a different session is not RELATED to its opener —
|
|
26387
|
+
* Chromium severs `window.opener`, which is the entire contract a
|
|
26388
|
+
* popup-based sign-in needs. The `about:blank` case lands in this same
|
|
26389
|
+
* session by prefs-copy instead of by this line.
|
|
26390
|
+
*/
|
|
26391
|
+
partition: BROWSER_PARTITION,
|
|
26392
|
+
sandbox: true,
|
|
26393
|
+
contextIsolation: true,
|
|
26394
|
+
nodeIntegration: false,
|
|
26395
|
+
webSecurity: true,
|
|
26396
|
+
allowRunningInsecureContent: false,
|
|
26397
|
+
spellcheck: false,
|
|
26398
|
+
navigateOnDragDrop: false,
|
|
26399
|
+
// Not security-inherited either: a hidden popup must render like a
|
|
26400
|
+
// shown one so a capture of it is evidence (design 2.5).
|
|
26401
|
+
backgroundThrottling: options.backgroundThrottling
|
|
26402
|
+
// No `preload` — a popup has no bridge to anything, like the view.
|
|
26403
|
+
}
|
|
26468
26404
|
}
|
|
26405
|
+
};
|
|
26406
|
+
});
|
|
26407
|
+
contents.on("did-create-window", (child, details) => {
|
|
26408
|
+
wirePopup(child, details, popupChildren);
|
|
26409
|
+
});
|
|
26410
|
+
const onWillNavigate = (event, rawUrl) => {
|
|
26411
|
+
if (refusesNavigation(rawUrl)) {
|
|
26412
|
+
event.preventDefault();
|
|
26413
|
+
log2(`[browser] refused a navigation to ${rawUrl}`);
|
|
26469
26414
|
}
|
|
26470
26415
|
};
|
|
26471
|
-
|
|
26472
|
-
|
|
26473
|
-
|
|
26474
|
-
|
|
26475
|
-
|
|
26476
|
-
|
|
26416
|
+
contents.on("will-navigate", onWillNavigate);
|
|
26417
|
+
contents.on("will-redirect", onWillNavigate);
|
|
26418
|
+
contents.on("before-input-event", (event, input) => {
|
|
26419
|
+
const direction = paneNavigationDirection(input);
|
|
26420
|
+
if (!direction) return;
|
|
26421
|
+
event.preventDefault();
|
|
26422
|
+
host.historyActive(direction);
|
|
26423
|
+
});
|
|
26424
|
+
const notifyChrome = () => {
|
|
26425
|
+
options.window.webContents.send("browser-state-changed");
|
|
26426
|
+
};
|
|
26427
|
+
contents.on("did-start-loading", () => {
|
|
26428
|
+
host.clearLoadFailure(tabId);
|
|
26429
|
+
notifyChrome();
|
|
26430
|
+
});
|
|
26431
|
+
contents.on("did-stop-loading", notifyChrome);
|
|
26432
|
+
contents.on("page-title-updated", notifyChrome);
|
|
26433
|
+
contents.on(
|
|
26434
|
+
"did-fail-load",
|
|
26435
|
+
(_event, code, description, url, isMainFrame) => {
|
|
26436
|
+
if (!isMainFrame) return;
|
|
26437
|
+
if (isReportableLoadFailure(code)) {
|
|
26438
|
+
host.recordLoadFailure(tabId, {
|
|
26439
|
+
code,
|
|
26440
|
+
description,
|
|
26441
|
+
url
|
|
26442
|
+
});
|
|
26443
|
+
}
|
|
26444
|
+
notifyChrome();
|
|
26445
|
+
}
|
|
26446
|
+
);
|
|
26447
|
+
contents.on("will-attach-webview", (event) => {
|
|
26477
26448
|
event.preventDefault();
|
|
26478
|
-
log2(
|
|
26449
|
+
log2("[browser] refused a webview attachment inside a driven page");
|
|
26450
|
+
});
|
|
26451
|
+
contents.on("did-navigate", (_event, url) => {
|
|
26452
|
+
registry.bumpEpoch(tabId);
|
|
26453
|
+
host.clearLoadFailure(tabId);
|
|
26454
|
+
stopLogCapture(contents.id);
|
|
26455
|
+
startLogCapture(contents.id);
|
|
26456
|
+
log2(`[browser] tab ${tabId} navigated to ${url}`);
|
|
26457
|
+
notifyChanged();
|
|
26458
|
+
});
|
|
26459
|
+
contents.on("did-navigate-in-page", (_event, url, isMainFrame) => {
|
|
26460
|
+
if (!isMainFrame) return;
|
|
26461
|
+
registry.bumpEpoch(tabId, false);
|
|
26462
|
+
log2(`[browser] tab ${tabId} navigated in page to ${url}`);
|
|
26463
|
+
notifyChanged();
|
|
26464
|
+
});
|
|
26465
|
+
contents.on("destroyed", () => {
|
|
26466
|
+
const record = registry.get(tabId);
|
|
26467
|
+
if (record) {
|
|
26468
|
+
const token = surfaceToken(record);
|
|
26469
|
+
if (token) approvals.forgetDocument(token);
|
|
26470
|
+
registry.forget(tabId);
|
|
26471
|
+
}
|
|
26472
|
+
host.clearLoadFailure(tabId);
|
|
26473
|
+
releaseTab(tabId, contents.id);
|
|
26474
|
+
});
|
|
26475
|
+
}, wirePopup = function(child, details, popupChildren) {
|
|
26476
|
+
popupChildren.add(child);
|
|
26477
|
+
child.on("closed", () => popupChildren.delete(child));
|
|
26478
|
+
const childContents = child.webContents;
|
|
26479
|
+
childContents.setWindowOpenHandler((childDetails) => {
|
|
26480
|
+
log2(`[browser] refused a popup from a popup: ${childDetails.url}`);
|
|
26481
|
+
return { action: "deny" };
|
|
26482
|
+
});
|
|
26483
|
+
const onWillNavigate = (event, rawUrl) => {
|
|
26484
|
+
if (refusesNavigation(rawUrl)) {
|
|
26485
|
+
event.preventDefault();
|
|
26486
|
+
log2(`[browser] refused a popup navigation to ${rawUrl}`);
|
|
26487
|
+
}
|
|
26488
|
+
};
|
|
26489
|
+
childContents.on("will-navigate", onWillNavigate);
|
|
26490
|
+
childContents.on("will-redirect", onWillNavigate);
|
|
26491
|
+
childContents.on("will-attach-webview", (event) => {
|
|
26492
|
+
event.preventDefault();
|
|
26493
|
+
log2("[browser] refused a webview attachment inside a popup");
|
|
26494
|
+
});
|
|
26495
|
+
childContents.on("before-input-event", (event, input) => {
|
|
26496
|
+
const direction = paneNavigationDirection(input);
|
|
26497
|
+
if (!direction) return;
|
|
26498
|
+
event.preventDefault();
|
|
26499
|
+
const navigation = childContents.navigationHistory;
|
|
26500
|
+
if (direction === "back") navigation.goBack();
|
|
26501
|
+
else navigation.goForward();
|
|
26502
|
+
});
|
|
26503
|
+
if (!options.backgroundThrottling) {
|
|
26504
|
+
childContents.setBackgroundThrottling(false);
|
|
26479
26505
|
}
|
|
26506
|
+
presentPopupWindow(
|
|
26507
|
+
child,
|
|
26508
|
+
effectivePresentation(options.windowShow, details.disposition),
|
|
26509
|
+
{ trigger: "popup-open", report: options.reportRaise }
|
|
26510
|
+
);
|
|
26480
26511
|
};
|
|
26481
|
-
|
|
26482
|
-
|
|
26483
|
-
|
|
26484
|
-
|
|
26485
|
-
|
|
26486
|
-
|
|
26487
|
-
|
|
26512
|
+
const browserSession = resolveBrowserSession();
|
|
26513
|
+
let registryForDownloads = null;
|
|
26514
|
+
const downloads = new DownloadArmer({
|
|
26515
|
+
tabForWebContents: (webContentsId) => registryForDownloads?.byWebContents(webContentsId)?.tabId ?? null,
|
|
26516
|
+
// WHO OWNED THE TAB, recorded on the decision (review round 2, U10). Read from
|
|
26517
|
+
// the registry at the moment the note is written, because the row may render it
|
|
26518
|
+
// after the tab is closed — and "· on another tab" about a tab that no longer
|
|
26519
|
+
// exists is a marker pointing at nothing. `null` when the lookup cannot answer
|
|
26520
|
+
// (a tab already gone), which the row renders the old way rather than guessing.
|
|
26521
|
+
ownerKindFor: (tabId) => registryForDownloads?.get(tabId)?.owner ?? null,
|
|
26522
|
+
log: log2,
|
|
26523
|
+
onActivity: () => {
|
|
26524
|
+
if (options.window.isDestroyed()) return;
|
|
26525
|
+
options.window.webContents.send("browser-state-changed");
|
|
26526
|
+
}
|
|
26527
|
+
});
|
|
26528
|
+
const detachSessionHandlers = installBrowserSessionHandlers(
|
|
26529
|
+
browserSession,
|
|
26530
|
+
{
|
|
26531
|
+
onPermissionRequested: (details) => {
|
|
26532
|
+
log2(
|
|
26533
|
+
`[browser] denied a ${details.permission} permission request from ${details.origin || "(unknown origin)"}`
|
|
26534
|
+
);
|
|
26535
|
+
},
|
|
26536
|
+
onDownload: (item, webContentsId) => downloads.decide(item, webContentsId),
|
|
26537
|
+
onDownloadDecided: (outcome) => {
|
|
26538
|
+
if (outcome.savePath) {
|
|
26539
|
+
log2(
|
|
26540
|
+
`[browser] saved a download from ${outcome.url} to ${outcome.savePath}`
|
|
26541
|
+
);
|
|
26542
|
+
}
|
|
26543
|
+
},
|
|
26544
|
+
log: log2
|
|
26545
|
+
}
|
|
26546
|
+
);
|
|
26547
|
+
rollback.add(detachSessionHandlers);
|
|
26548
|
+
configurePslRules(PSL_RULES);
|
|
26549
|
+
await installWebauthn({ log: log2 });
|
|
26550
|
+
checkpoint();
|
|
26551
|
+
const describeSource = (frame) => {
|
|
26552
|
+
if (!frame || typeof frame !== "object") {
|
|
26553
|
+
return { tabId: null, pageTitle: null };
|
|
26554
|
+
}
|
|
26555
|
+
const contents = electron.webContents.fromFrame(frame);
|
|
26556
|
+
const record = contents === void 0 ? void 0 : registry.byWebContentsId(contents.id);
|
|
26557
|
+
if (!record) return { tabId: null, pageTitle: null };
|
|
26558
|
+
const title = record.view.webContents.isDestroyed() ? "" : record.view.webContents.getTitle();
|
|
26559
|
+
return { tabId: record.tabId, pageTitle: title || null };
|
|
26560
|
+
};
|
|
26561
|
+
const webauthn = new WebauthnChooser({
|
|
26562
|
+
notify: (request) => {
|
|
26563
|
+
if (options.window.isDestroyed()) return;
|
|
26564
|
+
options.window.webContents.send("browser-webauthn-request", request);
|
|
26565
|
+
},
|
|
26566
|
+
/*
|
|
26567
|
+
* AND THE OTHER DIRECTION: every settle is pushed, not only every arrival.
|
|
26568
|
+
*
|
|
26569
|
+
* Without this the renderer cannot tell a live request from one main has
|
|
26570
|
+
* already cancelled, so a chooser whose request expired while the user was
|
|
26571
|
+
* away stays on screen offering rows that can no longer do anything (design
|
|
26572
|
+
* round 1, D2) — and the click is then discarded silently. The outcome travels
|
|
26573
|
+
* with the push because the two cases differ for the user: their own answer is
|
|
26574
|
+
* already off the screen, an expiry has to be explained.
|
|
26575
|
+
*/
|
|
26576
|
+
onSettled: (requestId2, outcome) => {
|
|
26577
|
+
if (options.window.isDestroyed()) return;
|
|
26578
|
+
options.window.webContents.send("browser-webauthn-settled", {
|
|
26579
|
+
requestId: requestId2,
|
|
26580
|
+
outcome
|
|
26581
|
+
});
|
|
26582
|
+
},
|
|
26583
|
+
describeSource,
|
|
26584
|
+
log: log2
|
|
26488
26585
|
});
|
|
26489
|
-
|
|
26586
|
+
rollback.add(() => webauthn.dispose());
|
|
26587
|
+
const detachWebauthn = attachWebauthnChooser(browserSession, webauthn, log2);
|
|
26588
|
+
rollback.add(detachWebauthn);
|
|
26589
|
+
const cookieJar = createHiddenCookieJarTarget(
|
|
26590
|
+
new electron.WebContentsView({
|
|
26591
|
+
webPreferences: { ...HIDDEN_COOKIE_JAR_WEB_PREFERENCES }
|
|
26592
|
+
})
|
|
26593
|
+
);
|
|
26594
|
+
rollback.add(() => cookieJar.dispose());
|
|
26595
|
+
const sessionCookies = new SessionCookieVault({
|
|
26596
|
+
jar: cookieJar.jar,
|
|
26597
|
+
cipher: browserProfileCipher(),
|
|
26598
|
+
...sessionCookiePaths(options.userDataDir),
|
|
26599
|
+
flushStore: () => browserSession.cookies.flushStore(),
|
|
26600
|
+
clearSessionData: (what) => clearBrowsingData(browserSession, what),
|
|
26601
|
+
log: log2
|
|
26602
|
+
});
|
|
26603
|
+
let restoreReport = null;
|
|
26604
|
+
try {
|
|
26605
|
+
restoreReport = await sessionCookies.restore();
|
|
26606
|
+
} catch (error) {
|
|
26607
|
+
log2(`[browser] session cookies: the restore failed (${String(error)})`);
|
|
26608
|
+
}
|
|
26609
|
+
checkpoint();
|
|
26610
|
+
const cdp = new CdpPool({ log: log2 });
|
|
26611
|
+
rollback.add(() => cdp.close());
|
|
26612
|
+
const views = /* @__PURE__ */ new Map();
|
|
26613
|
+
const releases = /* @__PURE__ */ new Set();
|
|
26614
|
+
let stateWriter = null;
|
|
26615
|
+
const sessionStore = new BrowserSessionStore({
|
|
26616
|
+
dir: path$1.join(options.userDataDir, "browser"),
|
|
26617
|
+
log: log2
|
|
26618
|
+
});
|
|
26619
|
+
const consentNotifier = new ConsentNotifier({
|
|
26620
|
+
show: options.windowShow,
|
|
26621
|
+
/*
|
|
26622
|
+
* The click's own rule lives in `consentClickHandler` (see it: the delivered
|
|
26623
|
+
* payload, the order, and the raise are all asserted there), and this is the
|
|
26624
|
+
* wiring that gives it the real window and the real plan. `windowShow` is the
|
|
26625
|
+
* plan the click comes forward under — this module still never decides whether a
|
|
26626
|
+
* window comes forward (design 11.4), it hands the plan to `window-raise.ts`.
|
|
26627
|
+
*/
|
|
26628
|
+
onAttention: consentClickHandler({
|
|
26629
|
+
/*
|
|
26630
|
+
* THE WINDOW IS ASKED FOR, NOT CAPTURED, and the difference is the U2 fix:
|
|
26631
|
+
* this host is stopped the moment its window closes (`src/main/index.ts`'s
|
|
26632
|
+
* `closed` hook), but a banner already raised is still in Notification
|
|
26633
|
+
* Center and its click still lands here. A captured window would be a
|
|
26634
|
+
* destroyed one by then, and reading it throws.
|
|
26635
|
+
*/
|
|
26636
|
+
window: () => options.window,
|
|
26637
|
+
show: options.windowShow,
|
|
26638
|
+
report: options.reportRaise,
|
|
26639
|
+
reopen: options.reopenConsent
|
|
26640
|
+
}),
|
|
26641
|
+
log: log2
|
|
26642
|
+
});
|
|
26643
|
+
const captureSession = () => {
|
|
26644
|
+
if (!published) return;
|
|
26645
|
+
sessionStore.record(
|
|
26646
|
+
captureTabs(
|
|
26647
|
+
registry.list(),
|
|
26648
|
+
registry.activeTab?.tabId ?? null,
|
|
26649
|
+
host.failedTabIds()
|
|
26650
|
+
)
|
|
26651
|
+
);
|
|
26652
|
+
};
|
|
26653
|
+
const notifyChanged = () => {
|
|
26654
|
+
stateWriter?.publishNow();
|
|
26490
26655
|
options.window.webContents.send("browser-state-changed");
|
|
26656
|
+
captureSession();
|
|
26491
26657
|
};
|
|
26492
|
-
|
|
26493
|
-
|
|
26494
|
-
|
|
26658
|
+
const registry = new TabRegistry(
|
|
26659
|
+
(_createOptions, tabId) => {
|
|
26660
|
+
const view = buildView(options.window);
|
|
26661
|
+
views.set(tabId, view);
|
|
26662
|
+
wireView(view, tabId);
|
|
26663
|
+
return view;
|
|
26664
|
+
},
|
|
26665
|
+
releaseTab,
|
|
26666
|
+
notifyChanged
|
|
26667
|
+
);
|
|
26668
|
+
registryForDownloads = registry;
|
|
26669
|
+
rollback.add(async () => {
|
|
26670
|
+
published = false;
|
|
26671
|
+
sessionStore.discard();
|
|
26672
|
+
registry.destroyAll();
|
|
26673
|
+
await Promise.allSettled([...releases]);
|
|
26495
26674
|
});
|
|
26496
|
-
|
|
26497
|
-
|
|
26498
|
-
|
|
26499
|
-
|
|
26500
|
-
|
|
26501
|
-
|
|
26502
|
-
|
|
26503
|
-
|
|
26504
|
-
|
|
26505
|
-
|
|
26506
|
-
|
|
26507
|
-
|
|
26675
|
+
const ownership = new OwnershipLedger({
|
|
26676
|
+
mayAdopt: (token, requester) => {
|
|
26677
|
+
const record = registry.requireSurface(token);
|
|
26678
|
+
return record.handedTo === requester.slice("session:".length) && !record.allocationId;
|
|
26679
|
+
},
|
|
26680
|
+
closeTab: async (token) => {
|
|
26681
|
+
try {
|
|
26682
|
+
const record = registry.requireSurface(token);
|
|
26683
|
+
approvals.forgetDocument(token);
|
|
26684
|
+
registry.destroy(record.tabId);
|
|
26685
|
+
} catch {
|
|
26686
|
+
}
|
|
26687
|
+
return true;
|
|
26688
|
+
},
|
|
26689
|
+
isLive: (token) => {
|
|
26690
|
+
try {
|
|
26691
|
+
registry.requireSurface(token);
|
|
26692
|
+
return true;
|
|
26693
|
+
} catch {
|
|
26694
|
+
return false;
|
|
26508
26695
|
}
|
|
26509
|
-
notifyChrome();
|
|
26510
26696
|
}
|
|
26511
|
-
);
|
|
26512
|
-
contents.on("will-attach-webview", (event) => {
|
|
26513
|
-
event.preventDefault();
|
|
26514
|
-
log2("[browser] refused a webview attachment inside a driven page");
|
|
26515
26697
|
});
|
|
26516
|
-
|
|
26517
|
-
|
|
26518
|
-
|
|
26519
|
-
|
|
26520
|
-
|
|
26521
|
-
|
|
26522
|
-
|
|
26698
|
+
const approvals = new ApprovalStore({
|
|
26699
|
+
dir: path$1.join(options.userDataDir, "browser"),
|
|
26700
|
+
onChanged: () => {
|
|
26701
|
+
options.window.webContents.send("browser-consent-changed");
|
|
26702
|
+
consentNotifier.announce(approvals.pendingEntries());
|
|
26703
|
+
},
|
|
26704
|
+
log: log2
|
|
26523
26705
|
});
|
|
26524
|
-
|
|
26525
|
-
|
|
26526
|
-
|
|
26527
|
-
|
|
26528
|
-
|
|
26706
|
+
const profileDir = browserStoragePath(browserSession) ?? "";
|
|
26707
|
+
const facts = {
|
|
26708
|
+
proto: 1,
|
|
26709
|
+
appVersion: options.appVersion,
|
|
26710
|
+
profileDir,
|
|
26711
|
+
profilePersistent: browserSession.isPersistent(),
|
|
26712
|
+
userAgent: browserUserAgent(),
|
|
26713
|
+
domainScope: domainScopeAvailable()
|
|
26714
|
+
};
|
|
26715
|
+
const host = new BrowserHost({
|
|
26716
|
+
registry,
|
|
26717
|
+
cdp,
|
|
26718
|
+
approvals,
|
|
26719
|
+
ownership,
|
|
26720
|
+
downloads,
|
|
26721
|
+
log: log2,
|
|
26722
|
+
onChanged: notifyChanged,
|
|
26723
|
+
facts: () => facts,
|
|
26724
|
+
forgetSiteData: (origin) => clearOriginData(browserSession, origin)
|
|
26529
26725
|
});
|
|
26530
|
-
|
|
26531
|
-
|
|
26532
|
-
|
|
26533
|
-
|
|
26534
|
-
|
|
26535
|
-
|
|
26536
|
-
|
|
26537
|
-
|
|
26538
|
-
|
|
26726
|
+
const recorded = readSession(sessionStore.filePath, log2);
|
|
26727
|
+
host.restoreTabs(recorded);
|
|
26728
|
+
if (recorded.length) {
|
|
26729
|
+
log2(
|
|
26730
|
+
`[browser] restored ${recorded.length} tab(s) from ${SESSION_FILENAME}; every restored tab is user-owned and holds no handle (design 7.3)`
|
|
26731
|
+
);
|
|
26732
|
+
}
|
|
26733
|
+
const consoleStartup = await startConsoleHost({
|
|
26734
|
+
window: options.window,
|
|
26735
|
+
expectedUrl: options.expectedUrl,
|
|
26736
|
+
appVersion: options.appVersion,
|
|
26737
|
+
notifier: options.notifier,
|
|
26738
|
+
/*
|
|
26739
|
+
* The capture view's document and preload, forwarded rather than re-derived:
|
|
26740
|
+
* only the app knows where its own bundle lives, and a console host that
|
|
26741
|
+
* guessed would answer a screenshot with a blank frame rather than with its
|
|
26742
|
+
* own typed refusal. WITHOUT THESE TWO the host builds no capture view at all,
|
|
26743
|
+
* which the live proof rig caught as `capture_unavailable` on a surface with
|
|
26744
|
+
* no displayed pane — the exact shape of a seam that is wired in tests and
|
|
26745
|
+
* forgotten in the one call site that matters.
|
|
26746
|
+
*/
|
|
26747
|
+
consoleCaptureUrl: options.consoleCaptureUrl,
|
|
26748
|
+
preloadPath: options.preloadPath,
|
|
26749
|
+
userShellPath: options.userShellPath,
|
|
26750
|
+
log: log2
|
|
26539
26751
|
});
|
|
26540
|
-
|
|
26541
|
-
|
|
26542
|
-
|
|
26543
|
-
|
|
26544
|
-
|
|
26545
|
-
|
|
26546
|
-
|
|
26547
|
-
|
|
26752
|
+
rollback.add(() => consoleStartup.stop());
|
|
26753
|
+
checkpoint();
|
|
26754
|
+
const sessionKey = mintSessionKey();
|
|
26755
|
+
const server = await startRpcServer({
|
|
26756
|
+
key: sessionKey,
|
|
26757
|
+
instanceId: hostInstanceId,
|
|
26758
|
+
dispatch: async (method, params, requestId2) => {
|
|
26759
|
+
if (!isConsoleDispatchMethod(method)) {
|
|
26760
|
+
return host.dispatch(method, params, requestId2);
|
|
26761
|
+
}
|
|
26762
|
+
if (!consoleStartup.ok) {
|
|
26763
|
+
throw new ConsoleError(
|
|
26764
|
+
"console_unavailable",
|
|
26765
|
+
`this app's console is not available (${consoleStartup.reason}): ${consoleStartup.detail}`,
|
|
26766
|
+
{ reason: consoleStartup.reason }
|
|
26767
|
+
);
|
|
26768
|
+
}
|
|
26769
|
+
return consoleStartup.dispatch(method, params);
|
|
26770
|
+
},
|
|
26771
|
+
log: log2,
|
|
26772
|
+
capabilities: () => ({ console: consoleStartup.ok })
|
|
26548
26773
|
});
|
|
26549
|
-
|
|
26550
|
-
|
|
26551
|
-
|
|
26552
|
-
|
|
26774
|
+
rollback.add(() => server.close());
|
|
26775
|
+
checkpoint();
|
|
26776
|
+
stateWriter = new BrowserStateWriter(
|
|
26777
|
+
stateFilePath(),
|
|
26778
|
+
() => {
|
|
26779
|
+
const consoleCounts = consoleStartup.ok ? consoleStartup.counts() : { total: 0, agent: 0 };
|
|
26780
|
+
return {
|
|
26781
|
+
tabs: registry.count(),
|
|
26782
|
+
agentTabs: registry.agentTabCount(),
|
|
26783
|
+
profileDir,
|
|
26784
|
+
console: consoleStartup.ok,
|
|
26785
|
+
consoleSurfaces: consoleCounts.total,
|
|
26786
|
+
consoleAgentSurfaces: consoleCounts.agent
|
|
26787
|
+
};
|
|
26788
|
+
},
|
|
26789
|
+
{
|
|
26790
|
+
appVersion: options.appVersion,
|
|
26791
|
+
instanceId: hostInstanceId,
|
|
26792
|
+
onError: (error) => log2(`[browser] state file: ${String(error)}`)
|
|
26553
26793
|
}
|
|
26554
|
-
|
|
26555
|
-
|
|
26556
|
-
|
|
26557
|
-
|
|
26558
|
-
|
|
26559
|
-
|
|
26560
|
-
|
|
26561
|
-
|
|
26562
|
-
|
|
26563
|
-
|
|
26564
|
-
|
|
26565
|
-
|
|
26566
|
-
|
|
26567
|
-
|
|
26794
|
+
);
|
|
26795
|
+
rollback.add(() => stateWriter?.clear());
|
|
26796
|
+
stateWriter.start(server.port, sessionKey);
|
|
26797
|
+
Object.assign(bindings, {
|
|
26798
|
+
window: () => options.window,
|
|
26799
|
+
expectedUrl: options.expectedUrl,
|
|
26800
|
+
host: () => host,
|
|
26801
|
+
webauthn: () => webauthn,
|
|
26802
|
+
clearData: (what) => sessionCookies.clearBrowsingData(what),
|
|
26803
|
+
// THE REVEAL TAKES NO PATH FROM THE RENDERER (§16.4): it opens the directory the
|
|
26804
|
+
// host is actually writing into, so the one place a page-derived string could
|
|
26805
|
+
// have become a path stays out of the IPC surface as well. `shell.openPath`
|
|
26806
|
+
// answers "" on success and a message on failure, which is returned rather than
|
|
26807
|
+
// thrown: a reveal that fails is a Finder problem, not a fault in the agent's
|
|
26808
|
+
// download.
|
|
26809
|
+
revealDownloads: async () => {
|
|
26810
|
+
const dir = downloads.downloadDir();
|
|
26811
|
+
if (!dir) return "no download directory yet";
|
|
26812
|
+
return electron.shell.openPath(dir);
|
|
26813
|
+
},
|
|
26814
|
+
log: log2
|
|
26568
26815
|
});
|
|
26569
|
-
|
|
26570
|
-
|
|
26571
|
-
}
|
|
26572
|
-
presentPopupWindow(
|
|
26573
|
-
child,
|
|
26574
|
-
effectivePresentation(options.windowShow, details.disposition),
|
|
26575
|
-
{ trigger: "popup-open", report: options.reportRaise }
|
|
26816
|
+
log2(
|
|
26817
|
+
`[browser] host on ${server.address}:${server.port} (proto ${facts.proto}), profile ${profileDir || "(in-memory)"}, agent tabs ${registry.agentTabCount()}/${8}, console ${consoleStartup.ok ? `on (${consoleStartup.counts().total} surface(s))` : `off (${consoleStartup.reason})`}`
|
|
26576
26818
|
);
|
|
26819
|
+
const handle2 = {
|
|
26820
|
+
host,
|
|
26821
|
+
port: server.port,
|
|
26822
|
+
profileDir,
|
|
26823
|
+
agentTabs: () => registry.agentTabCount(),
|
|
26824
|
+
sessionCookies: () => restoreReport,
|
|
26825
|
+
stop: async () => {
|
|
26826
|
+
const atStop = captureTabs(
|
|
26827
|
+
registry.list(),
|
|
26828
|
+
registry.activeTab?.tabId ?? null,
|
|
26829
|
+
host.failedTabIds()
|
|
26830
|
+
);
|
|
26831
|
+
const decision = sessionStore.commitStopCapture(atStop);
|
|
26832
|
+
if (!decision.write) {
|
|
26833
|
+
log2(
|
|
26834
|
+
`[browser] not overwriting ${SESSION_FILENAME} at stop: ${decision.reason}`
|
|
26835
|
+
);
|
|
26836
|
+
}
|
|
26837
|
+
sessionStore.flush();
|
|
26838
|
+
registry.destroyAll();
|
|
26839
|
+
await Promise.allSettled([...releases]);
|
|
26840
|
+
await cdp.close();
|
|
26841
|
+
await server.close();
|
|
26842
|
+
await consoleStartup.stop();
|
|
26843
|
+
unregisterBrowserIpc();
|
|
26844
|
+
detachSessionHandlers();
|
|
26845
|
+
detachWebauthn();
|
|
26846
|
+
webauthn.dispose();
|
|
26847
|
+
stateWriter?.clear();
|
|
26848
|
+
approvals.resetPending();
|
|
26849
|
+
ownership.clear();
|
|
26850
|
+
flushBrowserStorage(browserSession);
|
|
26851
|
+
try {
|
|
26852
|
+
await sessionCookies.snapshot();
|
|
26853
|
+
} catch (error) {
|
|
26854
|
+
log2(
|
|
26855
|
+
`[browser] session cookies: the snapshot failed (${String(error)})`
|
|
26856
|
+
);
|
|
26857
|
+
}
|
|
26858
|
+
cookieJar.dispose();
|
|
26859
|
+
log2("[browser] host stopped");
|
|
26860
|
+
}
|
|
26861
|
+
};
|
|
26862
|
+
checkpoint();
|
|
26863
|
+
published = true;
|
|
26864
|
+
activeHost = handle2;
|
|
26865
|
+
rollback.commit();
|
|
26866
|
+
return handle2;
|
|
26867
|
+
} catch (error) {
|
|
26868
|
+
published = false;
|
|
26869
|
+
for (const key of Object.keys(bindings))
|
|
26870
|
+
delete bindings[key];
|
|
26871
|
+
await rollback.run(log2);
|
|
26872
|
+
throw error;
|
|
26577
26873
|
}
|
|
26578
26874
|
}
|
|
26579
26875
|
function releaseView(window, view) {
|
|
@@ -37741,6 +38037,15 @@ class UpdateService {
|
|
|
37741
38037
|
* so it is kept rather than fetched again on the click (review U17).
|
|
37742
38038
|
*/
|
|
37743
38039
|
lastPublishedBackendVersion = null;
|
|
38040
|
+
/**
|
|
38041
|
+
* The version PyPI most recently NAMED whose files were not yet
|
|
38042
|
+
* downloadable, or null. Kept beside the published reading because the
|
|
38043
|
+
* manual check's sentence depends on the distinction (UX round 1, U1):
|
|
38044
|
+
* "a release is still publishing" and "the version could not be
|
|
38045
|
+
* determined" are different facts with different next steps, and the
|
|
38046
|
+
* gated read alone collapses them into one null.
|
|
38047
|
+
*/
|
|
38048
|
+
pendingPypiVersion = null;
|
|
37744
38049
|
/** Path of the artifact this run downloaded, when the updater reported one. */
|
|
37745
38050
|
downloadedArtifactPath = null;
|
|
37746
38051
|
/**
|
|
@@ -40550,8 +40855,17 @@ ${heal.removed.join("\n")}`,
|
|
|
40550
40855
|
return (await this.readRunningBackend()).version;
|
|
40551
40856
|
}
|
|
40552
40857
|
/**
|
|
40553
|
-
* Get the latest version from PyPI
|
|
40554
|
-
*
|
|
40858
|
+
* Get the latest READY version from PyPI: the newest release whose
|
|
40859
|
+
* platform wheel an installer can actually download, per
|
|
40860
|
+
* `readPypiReleaseBody`. A version PyPI has named but whose files have
|
|
40861
|
+
* not finished uploading resolves null — the same answer as an
|
|
40862
|
+
* unreadable response, because both mean do not offer yet; the next
|
|
40863
|
+
* scheduled check re-reads. The named-but-not-ready version is kept on
|
|
40864
|
+
* `pendingPypiVersion` so the manual check can say which state it is
|
|
40865
|
+
* in. Reading only `info.version` here was the 2026-10-12 race: the
|
|
40866
|
+
* offer appeared minutes after publication and `lop update` exited 1
|
|
40867
|
+
* against artifacts that were not on PyPI yet.
|
|
40868
|
+
* @returns Promise resolving to the latest ready version string or null if unable to fetch
|
|
40555
40869
|
*/
|
|
40556
40870
|
getLatestPypiVersion() {
|
|
40557
40871
|
return new Promise((resolve) => {
|
|
@@ -40567,14 +40881,26 @@ ${heal.removed.join("\n")}`,
|
|
|
40567
40881
|
{ timeout: PYPI_VERSION_READ_TIMEOUT_MS },
|
|
40568
40882
|
(res) => {
|
|
40569
40883
|
let data = "";
|
|
40884
|
+
const settleAborted = () => {
|
|
40885
|
+
this.pendingPypiVersion = null;
|
|
40886
|
+
resolve(null);
|
|
40887
|
+
};
|
|
40888
|
+
res.on("error", settleAborted);
|
|
40889
|
+
res.on("aborted", settleAborted);
|
|
40570
40890
|
res.on("data", (chunk) => {
|
|
40571
40891
|
data += chunk;
|
|
40572
40892
|
});
|
|
40573
40893
|
res.on("end", () => {
|
|
40574
40894
|
try {
|
|
40575
|
-
const
|
|
40576
|
-
|
|
40577
|
-
|
|
40895
|
+
const reading = readPypiReleaseBody(data, process.platform);
|
|
40896
|
+
this.pendingPypiVersion = reading.readyVersion === null ? reading.namedVersion : null;
|
|
40897
|
+
if (reading.readyVersion === null && reading.namedVersion) {
|
|
40898
|
+
logger.info(
|
|
40899
|
+
`PyPI names version ${reading.namedVersion} but its release has no downloadable wheel for this platform yet; not offering it. The next check will re-read.`,
|
|
40900
|
+
LogFileType.UPDATE_SERVICE
|
|
40901
|
+
);
|
|
40902
|
+
}
|
|
40903
|
+
resolve(reading.readyVersion);
|
|
40578
40904
|
} catch (error) {
|
|
40579
40905
|
logger.error(
|
|
40580
40906
|
"Error parsing PyPI response:",
|
|
@@ -40594,6 +40920,7 @@ ${heal.removed.join("\n")}`,
|
|
|
40594
40920
|
);
|
|
40595
40921
|
});
|
|
40596
40922
|
request.on("error", (error) => {
|
|
40923
|
+
this.pendingPypiVersion = null;
|
|
40597
40924
|
logger.error(
|
|
40598
40925
|
"Error fetching from PyPI:",
|
|
40599
40926
|
LogFileType.UPDATE_SERVICE,
|
|
@@ -41028,7 +41355,19 @@ ${heal.removed.join("\n")}`,
|
|
|
41028
41355
|
);
|
|
41029
41356
|
if (reportFailure) {
|
|
41030
41357
|
this.sendToRenderer("backend-update-error", {
|
|
41031
|
-
|
|
41358
|
+
/*
|
|
41359
|
+
* The not-ready state earns its own sentence (UX round 1, U1):
|
|
41360
|
+
* PyPI answered and named a release whose files are still
|
|
41361
|
+
* publishing, which is a wait, not a fault — the generic
|
|
41362
|
+
* sentence below reads as an installation problem and sends
|
|
41363
|
+
* the reader to debug one. Same producer-sentence shape as
|
|
41364
|
+
* the unreadable-install branch below. No retry action is
|
|
41365
|
+
* attached: the shared classifier gives authored sentences
|
|
41366
|
+
* no control (it names its own next step), and this sentence
|
|
41367
|
+
* does — check again in a few minutes, from the same control
|
|
41368
|
+
* the reader just used.
|
|
41369
|
+
*/
|
|
41370
|
+
message: installedVersion && this.pendingPypiVersion ? `A new server release (${this.pendingPypiVersion}) is still publishing, so no update was offered yet. Check again in a few minutes.` : "Unable to determine backend version.",
|
|
41032
41371
|
phase: "check",
|
|
41033
41372
|
logPath: serverUpdateLogPath()
|
|
41034
41373
|
});
|