@yeaft/webchat-agent 1.0.415 → 1.0.417

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.
@@ -0,0 +1,192 @@
1
+ import { mkdtemp, mkdir, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import {
4
+ BROWSER_RUNTIME_CHROME_BUILD,
5
+ readBrowserExecutableVersion,
6
+ resolveBrowserExecutable,
7
+ } from './browser-install.js';
8
+ import { BROWSER_EXTENSION_DIR } from './extension.js';
9
+ import { BrowserRuntimeError } from './errors.js';
10
+
11
+ const BROWSER_CLOSE_TIMEOUT_MS = 5_000;
12
+ const BROWSER_FORCE_EXIT_TIMEOUT_MS = 1_000;
13
+
14
+ function browserProcess(browser) {
15
+ try { return browser?.process?.() || null; } catch { return null; }
16
+ }
17
+
18
+ function forceKillBrowser(browser) {
19
+ const child = browserProcess(browser);
20
+ if (!child || child.exitCode !== null) return child;
21
+ try {
22
+ if (process.platform !== 'win32' && Number.isInteger(child.pid)) process.kill(-child.pid, 'SIGKILL');
23
+ else child.kill('SIGKILL');
24
+ } catch {
25
+ try { child.kill('SIGKILL'); } catch {}
26
+ }
27
+ return child;
28
+ }
29
+
30
+ async function settleWithin(promise, timeoutMs) {
31
+ let timer;
32
+ try {
33
+ return await Promise.race([
34
+ Promise.resolve(promise).then(() => true, () => false),
35
+ new Promise(resolve => { timer = setTimeout(() => resolve(false), timeoutMs); }),
36
+ ]);
37
+ } finally {
38
+ clearTimeout(timer);
39
+ }
40
+ }
41
+
42
+ async function waitForExit(child, timeoutMs) {
43
+ if (!child || child.exitCode !== null || typeof child.once !== 'function') return;
44
+ let timer;
45
+ await Promise.race([
46
+ new Promise(resolve => child.once('exit', resolve)),
47
+ new Promise(resolve => { timer = setTimeout(resolve, timeoutMs); }),
48
+ ]);
49
+ clearTimeout(timer);
50
+ }
51
+
52
+ async function cleanupBrowser(browser, profileDir) {
53
+ if (browser) {
54
+ let closePromise;
55
+ try { closePromise = browser.close(); } catch { closePromise = Promise.reject(new Error('Browser close failed')); }
56
+ if (!await settleWithin(closePromise, BROWSER_CLOSE_TIMEOUT_MS)) {
57
+ const child = forceKillBrowser(browser);
58
+ await waitForExit(child, BROWSER_FORCE_EXIT_TIMEOUT_MS);
59
+ }
60
+ }
61
+ await rm(profileDir, { recursive: true, force: true }).catch(() => {});
62
+ }
63
+
64
+ function actualChromeBuild(version) {
65
+ return String(version || '').match(/(?:Chrome(?:\s+for\s+Testing)?|Chromium)(?:\/|\s+)(\d+(?:\.\d+){0,3})/i)?.[1] || null;
66
+ }
67
+
68
+ function boundedViewport(value, config) {
69
+ const width = Math.min(config.maxWidth, Math.max(320, Math.floor(Number(value?.width) || 1280)));
70
+ const height = Math.min(config.maxHeight, Math.max(240, Math.floor(Number(value?.height) || 720)));
71
+ const deviceScaleFactor = Math.min(2, Math.max(1, Number(value?.deviceScaleFactor) || 1));
72
+ return Object.freeze({ width, height, deviceScaleFactor });
73
+ }
74
+
75
+ async function prepareExtensionRuntime(browser, installed, page, launchConfig, timeoutMs) {
76
+ // Loading the bundled popup in bootstrap mode starts the MV3 worker without
77
+ // consuming the extension-action gesture required by tabCapture.
78
+ const bootstrap = await browser.newPage();
79
+ try {
80
+ await bootstrap.goto(`chrome-extension://${installed.id}/popup.html?bootstrap=1`, {
81
+ waitUntil: 'domcontentloaded',
82
+ timeout: timeoutMs,
83
+ });
84
+ const target = await browser.waitForTarget(candidate => (
85
+ candidate.type() === 'service_worker'
86
+ && candidate.url().startsWith(`chrome-extension://${installed.id}/`)
87
+ ), { timeout: timeoutMs });
88
+ const worker = await target.worker();
89
+ if (!worker) throw new BrowserRuntimeError('browser_extension_worker_missing');
90
+ await worker.evaluate(async value => {
91
+ await chrome.storage.session.set({ browserRuntimeLaunch: value });
92
+ }, launchConfig);
93
+ } finally {
94
+ await bootstrap.close().catch(() => {});
95
+ }
96
+ await page.bringToFront();
97
+ await page.triggerExtensionAction(installed);
98
+ }
99
+
100
+ /** Launch one isolated Chromium process and page owned by one Browser Session. */
101
+ export async function launchBrowserSession({
102
+ browserSessionId,
103
+ bridgeUrl,
104
+ config,
105
+ initialUrl = 'about:blank',
106
+ viewport,
107
+ locale = 'en-US',
108
+ launch = null,
109
+ signal = null,
110
+ } = {}) {
111
+ if (!browserSessionId || !bridgeUrl) throw new BrowserRuntimeError('browser_session_invalid');
112
+ const cacheDir = config.cacheDir;
113
+ const profilesDir = `${cacheDir}-profiles`;
114
+ await mkdir(profilesDir, { recursive: true, mode: 0o700 });
115
+ const profileDir = await mkdtemp(join(profilesDir, 'session-'));
116
+ let browser = null;
117
+ try {
118
+ signal?.throwIfAborted();
119
+ const executablePath = await resolveBrowserExecutable({
120
+ executablePath: config.executablePath,
121
+ cacheDir,
122
+ });
123
+ if (!executablePath) throw new BrowserRuntimeError('browser_executable_missing');
124
+ signal?.throwIfAborted();
125
+ const version = await readBrowserExecutableVersion(executablePath, { signal });
126
+ if (actualChromeBuild(version) !== BROWSER_RUNTIME_CHROME_BUILD) {
127
+ throw new BrowserRuntimeError('browser_version_mismatch');
128
+ }
129
+ const launchBrowser = launch || (await import('puppeteer-core')).default.launch;
130
+ const resolvedViewport = boundedViewport(viewport, config);
131
+ browser = await launchBrowser({
132
+ executablePath,
133
+ headless: config.headless,
134
+ userDataDir: profileDir,
135
+ acceptInsecureCerts: false,
136
+ timeout: config.startupProbeTimeoutMs,
137
+ protocolTimeout: config.maxActionRuntimeMs,
138
+ defaultViewport: resolvedViewport,
139
+ signal,
140
+ args: [
141
+ `--disable-extensions-except=${BROWSER_EXTENSION_DIR}`,
142
+ `--load-extension=${BROWSER_EXTENSION_DIR}`,
143
+ '--disable-background-networking',
144
+ '--disable-component-update',
145
+ '--disable-default-apps',
146
+ '--disable-features=MediaRouter,Translate',
147
+ '--disable-sync',
148
+ '--no-first-run',
149
+ `--lang=${String(locale || 'en-US').slice(0, 32)}`,
150
+ ],
151
+ });
152
+ signal?.throwIfAborted();
153
+ const installed = [...(await browser.extensions()).values()]
154
+ .find(candidate => candidate.name === 'Yeaft Browser Runtime');
155
+ if (!installed?.id) throw new BrowserRuntimeError('browser_extension_missing');
156
+
157
+ const pages = await browser.pages();
158
+ const page = pages[0] || await browser.newPage();
159
+ await page.setViewport(resolvedViewport);
160
+ signal?.throwIfAborted();
161
+ await page.goto(String(initialUrl || 'about:blank').slice(0, 4096), {
162
+ waitUntil: 'domcontentloaded',
163
+ timeout: config.maxActionRuntimeMs,
164
+ });
165
+
166
+ signal?.throwIfAborted();
167
+ await prepareExtensionRuntime(browser, installed, page, {
168
+ browserSessionId,
169
+ bridgeUrl,
170
+ targetTabId: page.target()?._targetId || null,
171
+ }, config.maxActionRuntimeMs);
172
+
173
+ return {
174
+ browser,
175
+ page,
176
+ profileDir,
177
+ viewport: resolvedViewport,
178
+ captureMode: 'tab',
179
+ extensionId: installed.id,
180
+ async close() {
181
+ const ownedBrowser = browser;
182
+ browser = null;
183
+ await cleanupBrowser(ownedBrowser, profileDir);
184
+ },
185
+ };
186
+ } catch (error) {
187
+ const ownedBrowser = browser;
188
+ browser = null;
189
+ await cleanupBrowser(ownedBrowser, profileDir);
190
+ throw error;
191
+ }
192
+ }
@@ -69,7 +69,7 @@ export async function handleBrowserCommand(args, dependencies = {}) {
69
69
  const result = configApi.updateBrowserRuntimeSettings({ enabled: action === 'enable' }, yeaftDir);
70
70
  if (result.error) throw new Error(result.error);
71
71
  log(JSON.stringify(result, null, 2));
72
- log('Restart the selected Agent instance to run the startup probe. Phase 0 does not advertise Browser capability.');
72
+ log('Restart the selected Agent instance. A successful supported-platform probe advertises Browser viewer capability.');
73
73
  return;
74
74
  }
75
75
  if (action === 'status') {
@@ -14,5 +14,8 @@
14
14
  "offscreen",
15
15
  "storage",
16
16
  "tabCapture"
17
+ ],
18
+ "host_permissions": [
19
+ "http://127.0.0.1/*"
17
20
  ]
18
21
  }
@@ -1,4 +1,5 @@
1
1
  const PROBE_TIMEOUT_MS = 5_000;
2
+ const sessions = new Map();
2
3
 
3
4
  function sleep(ms) {
4
5
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -22,44 +23,52 @@ async function inboundVideoStats(peer) {
22
23
  return null;
23
24
  }
24
25
 
26
+ async function captureTab(streamId) {
27
+ const stream = await navigator.mediaDevices.getUserMedia({
28
+ audio: false,
29
+ video: {
30
+ mandatory: {
31
+ chromeMediaSource: 'tab',
32
+ chromeMediaSourceId: streamId,
33
+ },
34
+ },
35
+ });
36
+ const track = stream.getVideoTracks()[0];
37
+ if (!track) {
38
+ stream.getTracks().forEach(item => item.stop());
39
+ throw new Error('tab capture returned no video track');
40
+ }
41
+ return { stream, track };
42
+ }
43
+
44
+ function preferVp8(peer, sender) {
45
+ const vp8 = RTCRtpSender.getCapabilities('video')?.codecs
46
+ ?.filter(codec => codec.mimeType?.toLowerCase() === 'video/vp8') || [];
47
+ if (vp8.length === 0) throw new Error('VP8 encoder capability unavailable');
48
+ const transceiver = peer.getTransceivers().find(item => item.sender === sender);
49
+ transceiver?.setCodecPreferences(vp8);
50
+ }
51
+
25
52
  async function runMediaProbe(streamId) {
26
53
  let stream = null;
27
54
  let offerer = null;
28
55
  let answerer = null;
29
56
  try {
30
- stream = await navigator.mediaDevices.getUserMedia({
31
- audio: false,
32
- video: {
33
- mandatory: {
34
- chromeMediaSource: 'tab',
35
- chromeMediaSourceId: streamId,
36
- },
37
- },
38
- });
57
+ ({ stream } = await captureTab(streamId));
39
58
  const track = stream.getVideoTracks()[0];
40
- if (!track) throw new Error('tab capture returned no video track');
41
-
42
59
  offerer = new RTCPeerConnection();
43
60
  answerer = new RTCPeerConnection();
44
61
  offerer.onicecandidate = event => event.candidate && answerer.addIceCandidate(event.candidate);
45
62
  answerer.onicecandidate = event => event.candidate && offerer.addIceCandidate(event.candidate);
46
-
47
63
  const video = document.querySelector('#probe-video');
48
- answerer.ontrack = event => {
49
- video.srcObject = event.streams[0] || new MediaStream([event.track]);
50
- };
64
+ answerer.ontrack = event => { video.srcObject = event.streams[0] || new MediaStream([event.track]); };
51
65
  const sender = offerer.addTrack(track, stream);
52
- const vp8 = RTCRtpSender.getCapabilities('video')?.codecs
53
- ?.filter(codec => codec.mimeType?.toLowerCase() === 'video/vp8') || [];
54
- if (vp8.length === 0) throw new Error('VP8 encoder capability unavailable');
55
- const transceiver = offerer.getTransceivers().find(item => item.sender === sender);
56
- transceiver?.setCodecPreferences(vp8);
66
+ preferVp8(offerer, sender);
57
67
  await offerer.setLocalDescription(await offerer.createOffer());
58
68
  await answerer.setRemoteDescription(offerer.localDescription);
59
69
  await answerer.setLocalDescription(await answerer.createAnswer());
60
70
  await offerer.setRemoteDescription(answerer.localDescription);
61
71
  await video.play().catch(() => {});
62
-
63
72
  const deadline = Date.now() + PROBE_TIMEOUT_MS;
64
73
  let media = null;
65
74
  while (Date.now() < deadline) {
@@ -89,12 +98,179 @@ async function runMediaProbe(streamId) {
89
98
  }
90
99
  }
91
100
 
101
+ function bridgeSend(record, message) {
102
+ if (record.socket?.readyState === WebSocket.OPEN) {
103
+ record.socket.send(JSON.stringify({ ...message, browserSessionId: record.browserSessionId }));
104
+ }
105
+ }
106
+
107
+ function peerRecord(record, message) {
108
+ const peer = record?.peers.get(message?.peerId);
109
+ return peer?.connectionGeneration === message?.connectionGeneration ? peer : null;
110
+ }
111
+
112
+ function closePeer(record, peer, notify = false) {
113
+ if (!record || !peer || record.peers.get(peer.peerId) !== peer) return false;
114
+ record.peers.delete(peer.peerId);
115
+ peer.connection.close();
116
+ if (notify) bridgeSend(record, {
117
+ type: 'peer_closed',
118
+ peerId: peer.peerId,
119
+ connectionGeneration: peer.connectionGeneration,
120
+ });
121
+ return true;
122
+ }
123
+
124
+ function closeSession(record, notify = true) {
125
+ if (!record || record.closed) return;
126
+ record.closed = true;
127
+ if (sessions.get(record.browserSessionId) === record) sessions.delete(record.browserSessionId);
128
+ for (const peer of [...record.peers.values()]) closePeer(record, peer, notify);
129
+ record.stream?.getTracks().forEach(track => track.stop());
130
+ try { record.socket?.close(1000, 'Browser Session closed'); } catch {}
131
+ }
132
+
133
+ async function startRuntime({ browserSessionId, bridgeUrl, streamId }) {
134
+ closeSession(sessions.get(browserSessionId), false);
135
+ const { stream, track } = await captureTab(streamId);
136
+ const socket = new WebSocket(bridgeUrl);
137
+ const record = {
138
+ browserSessionId,
139
+ stream,
140
+ track,
141
+ socket,
142
+ peers: new Map(),
143
+ closed: false,
144
+ };
145
+ sessions.set(browserSessionId, record);
146
+ track.addEventListener('ended', () => bridgeSend(record, { type: 'capture_ended' }), { once: true });
147
+ socket.onopen = () => bridgeSend(record, {
148
+ type: 'runtime_ready',
149
+ captureMode: 'tab',
150
+ settings: track.getSettings?.() || {},
151
+ });
152
+ socket.onmessage = event => {
153
+ let message;
154
+ try { message = JSON.parse(event.data); } catch { return; }
155
+ if (message.browserSessionId !== browserSessionId) return;
156
+ handleBridgeMessage(record, message).catch(error => {
157
+ const peer = peerRecord(record, message);
158
+ if (peer) closePeer(record, peer);
159
+ bridgeSend(record, {
160
+ type: 'peer_error',
161
+ peerId: message.peerId || null,
162
+ connectionGeneration: message.connectionGeneration || null,
163
+ code: error?.name || 'peer_failed',
164
+ safeError: String(error?.message || error).slice(0, 500),
165
+ });
166
+ });
167
+ };
168
+ socket.onclose = () => closeSession(record, false);
169
+ return { ok: true };
170
+ }
171
+
172
+ async function createPeer(record, message) {
173
+ const peerId = message.peerId;
174
+ const connectionGeneration = message.connectionGeneration;
175
+ const existing = record.peers.get(peerId);
176
+ if (existing?.connectionGeneration === connectionGeneration) return;
177
+ if (existing) closePeer(record, existing);
178
+ const connection = new RTCPeerConnection({
179
+ iceServers: Array.isArray(message.iceServers) ? message.iceServers : [],
180
+ iceTransportPolicy: message.iceTransportPolicy === 'relay' ? 'relay' : 'all',
181
+ });
182
+ const peer = {
183
+ peerId,
184
+ connectionGeneration,
185
+ connection,
186
+ pendingCandidates: [],
187
+ offerSent: false,
188
+ localCandidates: [],
189
+ };
190
+ record.peers.set(peerId, peer);
191
+ const isCurrent = () => peerRecord(record, peer) === peer;
192
+ connection.onicecandidate = event => {
193
+ if (!isCurrent()) return;
194
+ const candidateMessage = {
195
+ type: 'peer_ice_candidate',
196
+ peerId,
197
+ connectionGeneration,
198
+ candidate: event.candidate ? event.candidate.toJSON() : null,
199
+ };
200
+ if (!peer.offerSent) peer.localCandidates.push(candidateMessage);
201
+ else bridgeSend(record, candidateMessage);
202
+ };
203
+ connection.onconnectionstatechange = () => {
204
+ if (!isCurrent()) return;
205
+ bridgeSend(record, {
206
+ type: 'peer_state',
207
+ peerId,
208
+ connectionGeneration,
209
+ state: connection.connectionState,
210
+ });
211
+ };
212
+ const sender = connection.addTrack(record.track, record.stream);
213
+ preferVp8(connection, sender);
214
+ const parameters = sender.getParameters();
215
+ if (parameters.encodings?.length) {
216
+ parameters.encodings[0].maxBitrate = Number(message.maxBitrate) || 4_000_000;
217
+ parameters.encodings[0].maxFramerate = Number(message.maxFps) || 30;
218
+ await sender.setParameters(parameters);
219
+ }
220
+ await connection.setLocalDescription(await connection.createOffer());
221
+ if (!isCurrent()) return;
222
+ bridgeSend(record, {
223
+ type: 'peer_prepared',
224
+ peerId,
225
+ connectionGeneration,
226
+ });
227
+ bridgeSend(record, {
228
+ type: 'peer_offer',
229
+ peerId,
230
+ connectionGeneration,
231
+ description: connection.localDescription,
232
+ });
233
+ peer.offerSent = true;
234
+ for (const candidate of peer.localCandidates.splice(0)) bridgeSend(record, candidate);
235
+ }
236
+
237
+ async function handleBridgeMessage(record, message) {
238
+ if (record.closed) return;
239
+ if (message.type === 'session_close') {
240
+ closeSession(record);
241
+ return;
242
+ }
243
+ if (message.type === 'peer_prepare') return createPeer(record, message);
244
+ const peer = peerRecord(record, message);
245
+ if (!peer) return;
246
+ const connection = peer.connection;
247
+ if (message.type === 'peer_answer') {
248
+ await connection.setRemoteDescription(message.description);
249
+ if (peerRecord(record, message) !== peer) return;
250
+ for (const candidate of peer.pendingCandidates.splice(0)) {
251
+ await connection.addIceCandidate(candidate);
252
+ if (peerRecord(record, message) !== peer) return;
253
+ }
254
+ } else if (message.type === 'peer_ice_candidate') {
255
+ if (!message.candidate) return;
256
+ if (!connection.remoteDescription) peer.pendingCandidates.push(message.candidate);
257
+ else await connection.addIceCandidate(message.candidate);
258
+ } else if (message.type === 'peer_close') {
259
+ closePeer(record, peer);
260
+ }
261
+ }
262
+
92
263
  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
93
- if (message?.target !== 'browser_runtime_offscreen'
94
- || message?.type !== 'browser_runtime_probe_media') return undefined;
95
- runMediaProbe(message.streamId).then(sendResponse, error => sendResponse({
264
+ if (message?.target !== 'browser_runtime_offscreen') return undefined;
265
+ const operation = message.type === 'browser_runtime_probe_media'
266
+ ? () => runMediaProbe(message.streamId)
267
+ : message.type === 'browser_runtime_start'
268
+ ? () => startRuntime(message)
269
+ : null;
270
+ if (!operation) return undefined;
271
+ operation().then(sendResponse, error => sendResponse({
96
272
  ok: false,
97
- code: 'media_probe_failed',
273
+ code: message.type === 'browser_runtime_probe_media' ? 'media_probe_failed' : 'browser_runtime_start_failed',
98
274
  safeError: String(error?.message || error).slice(0, 500),
99
275
  }));
100
276
  return true;
@@ -1 +1,4 @@
1
- chrome.runtime.sendMessage({ type: 'browser_runtime_probe_start' }).catch(() => {});
1
+ const bootstrap = new URLSearchParams(location.search).get('bootstrap') === '1';
2
+ chrome.runtime.sendMessage({
3
+ type: bootstrap ? 'browser_runtime_bootstrap' : 'browser_runtime_action',
4
+ }).catch(() => {});
@@ -12,15 +12,20 @@ async function ensureOffscreenDocument() {
12
12
  offscreenCreation = chrome.offscreen.createDocument({
13
13
  url: OFFSCREEN_PATH,
14
14
  reasons: ['USER_MEDIA', 'WEB_RTC'],
15
- justification: 'Capture the controlled tab and validate the Browser Runtime WebRTC media path',
15
+ justification: 'Capture the Agent-owned tab and provide the Browser Runtime WebRTC endpoint',
16
16
  }).finally(() => { offscreenCreation = null; });
17
17
  }
18
18
  await offscreenCreation;
19
19
  }
20
20
 
21
- async function runStartupProbe() {
21
+ async function activeTab() {
22
22
  const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
23
23
  if (!tab?.id) throw new Error('active tab unavailable');
24
+ return tab;
25
+ }
26
+
27
+ async function runStartupProbe() {
28
+ const tab = await activeTab();
24
29
  const streamId = await chrome.tabCapture.getMediaStreamId({ targetTabId: tab.id });
25
30
  await ensureOffscreenDocument();
26
31
  return chrome.runtime.sendMessage({
@@ -30,18 +35,53 @@ async function runStartupProbe() {
30
35
  });
31
36
  }
32
37
 
38
+ async function startRuntime(browserRuntimeLaunch) {
39
+ if (!browserRuntimeLaunch?.browserSessionId || !browserRuntimeLaunch?.bridgeUrl) {
40
+ throw new Error('Browser Runtime launch authorization missing');
41
+ }
42
+ const tab = await activeTab();
43
+ const streamId = await chrome.tabCapture.getMediaStreamId({ targetTabId: tab.id });
44
+ await ensureOffscreenDocument();
45
+ const response = await chrome.runtime.sendMessage({
46
+ target: 'browser_runtime_offscreen',
47
+ type: 'browser_runtime_start',
48
+ browserSessionId: browserRuntimeLaunch.browserSessionId,
49
+ bridgeUrl: browserRuntimeLaunch.bridgeUrl,
50
+ streamId,
51
+ });
52
+ await chrome.storage.session.remove('browserRuntimeLaunch');
53
+ return response;
54
+ }
55
+
33
56
  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
34
- if (message?.type !== 'browser_runtime_probe_start') return undefined;
35
- runStartupProbe().then(async result => {
36
- await chrome.storage.session.set({ browserRuntimeProbe: result });
57
+ if (message?.type === 'browser_runtime_bootstrap') {
58
+ sendResponse({ ok: true });
59
+ return undefined;
60
+ }
61
+ if (message?.type !== 'browser_runtime_probe_start'
62
+ && message?.type !== 'browser_runtime_action') return undefined;
63
+ let runningProbe = message.type === 'browser_runtime_probe_start';
64
+ const operation = async () => {
65
+ if (runningProbe) return runStartupProbe();
66
+ const { browserRuntimeLaunch } = await chrome.storage.session.get('browserRuntimeLaunch');
67
+ if (browserRuntimeLaunch) return startRuntime(browserRuntimeLaunch);
68
+ runningProbe = true;
69
+ return runStartupProbe();
70
+ };
71
+ operation().then(async result => {
72
+ if (runningProbe) {
73
+ await chrome.storage.session.set({ browserRuntimeProbe: result });
74
+ }
37
75
  sendResponse(result);
38
76
  }, async error => {
39
77
  const result = {
40
78
  ok: false,
41
- code: 'capture_probe_failed',
79
+ code: message.type === 'browser_runtime_probe_start' ? 'capture_probe_failed' : 'browser_runtime_start_failed',
42
80
  safeError: String(error?.message || error).slice(0, 500),
43
81
  };
44
- await chrome.storage.session.set({ browserRuntimeProbe: result });
82
+ if (runningProbe) {
83
+ await chrome.storage.session.set({ browserRuntimeProbe: result });
84
+ }
45
85
  sendResponse(result);
46
86
  });
47
87
  return true;
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  const moduleDir = dirname(fileURLToPath(import.meta.url));
7
7
  export const BROWSER_EXTENSION_DIR = join(moduleDir, 'extension');
8
8
  export const BROWSER_EXTENSION_NAME = 'Yeaft Browser Runtime';
9
- export const BROWSER_EXTENSION_SHA256 = '51cc6519ec9f72f86af7a0a98c3511c32e0e8fde6e8e3406886e7048f0c5972e';
9
+ export const BROWSER_EXTENSION_SHA256 = '13279f6bb05975f9de9f92fdc13da28341ff99c161563cda25d27a14d5cdcdd5';
10
10
 
11
11
  async function walkFiles(root, current = root) {
12
12
  const entries = await readdir(current, { withFileTypes: true });
@@ -1,4 +1,7 @@
1
1
  export { BrowserRuntimeService, bootBrowserRuntime, getBrowserRuntime, shutdownBrowserRuntime } from './service.js';
2
+ export { BrowserExtensionBridge } from './local-bridge.js';
3
+ export { launchBrowserSession } from './chromium.js';
4
+ export { handleBrowserRuntimeMessage, BROWSER_MESSAGE_TYPES } from './messages.js';
2
5
  export { probeBrowserRuntime } from './probe.js';
3
6
  export { installManagedBrowser, findManagedBrowser, defaultBrowserCacheDir, BROWSER_RUNTIME_CHROME_BUILD } from './browser-install.js';
4
7
  export { normaliseBrowserRuntimeSection, validateBrowserRuntimeUpdate } from './config.js';