@browserbridge/bbx 1.0.0 → 1.0.1

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.
Files changed (52) hide show
  1. package/README.md +3 -1
  2. package/docs/api-reference.md +33 -33
  3. package/docs/mcp-vs-cli.md +104 -104
  4. package/docs/publishing.md +1 -3
  5. package/docs/quickstart.md +6 -6
  6. package/docs/unpacked-extension.md +72 -0
  7. package/manifest.json +3 -17
  8. package/package.json +44 -42
  9. package/packages/agent-client/src/cli-helpers.js +10 -5
  10. package/packages/agent-client/src/cli.js +65 -135
  11. package/packages/agent-client/src/client.js +37 -17
  12. package/packages/agent-client/src/command-registry.js +101 -69
  13. package/packages/agent-client/src/detect.js +3 -6
  14. package/packages/agent-client/src/install.js +10 -27
  15. package/packages/agent-client/src/mcp-config.js +11 -30
  16. package/packages/agent-client/src/runtime.js +41 -20
  17. package/packages/agent-client/src/setup-status.js +13 -28
  18. package/packages/extension/src/background-helpers.js +51 -36
  19. package/packages/extension/src/background-routing.js +11 -13
  20. package/packages/extension/src/background.js +562 -299
  21. package/packages/extension/src/content-script-helpers.js +17 -16
  22. package/packages/extension/src/content-script.js +175 -109
  23. package/packages/extension/src/sidepanel-helpers.js +3 -1
  24. package/packages/extension/ui/popup.js +39 -20
  25. package/packages/extension/ui/sidepanel.js +108 -191
  26. package/packages/extension/ui/ui.css +2 -1
  27. package/packages/mcp-server/src/handlers.js +546 -250
  28. package/packages/mcp-server/src/server.js +558 -257
  29. package/packages/native-host/bin/bridge-daemon.js +6 -2
  30. package/packages/native-host/bin/install-manifest.js +2 -2
  31. package/packages/native-host/bin/postinstall.js +4 -2
  32. package/packages/native-host/src/config.js +11 -7
  33. package/packages/native-host/src/daemon.js +143 -92
  34. package/packages/native-host/src/install-manifest.js +73 -22
  35. package/packages/native-host/src/native-host.js +55 -40
  36. package/packages/protocol/src/budget.js +3 -7
  37. package/packages/protocol/src/capabilities.js +3 -3
  38. package/packages/protocol/src/errors.js +11 -11
  39. package/packages/protocol/src/protocol.js +104 -71
  40. package/packages/protocol/src/registry.js +300 -45
  41. package/packages/protocol/src/summary.js +249 -106
  42. package/packages/protocol/src/types.js +1 -1
  43. package/skills/browser-bridge/SKILL.md +1 -1
  44. package/skills/browser-bridge/agents/openai.yaml +3 -3
  45. package/skills/browser-bridge/references/interaction.md +33 -11
  46. package/skills/browser-bridge/references/patch-workflow.md +3 -0
  47. package/skills/browser-bridge/references/protocol.md +125 -70
  48. package/skills/browser-bridge/references/tailwind.md +12 -11
  49. package/skills/browser-bridge/references/token-efficiency.md +23 -22
  50. package/skills/browser-bridge/references/ui-workflows.md +8 -0
  51. package/packages/extension/ui/offscreen.html +0 -6
  52. package/packages/extension/ui/offscreen.js +0 -61
@@ -3,7 +3,11 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
 
6
- import { APP_NAME, getManifestInstallDir, SUPPORTED_BROWSERS } from '../../native-host/src/config.js';
6
+ import {
7
+ APP_NAME,
8
+ getManifestInstallDir,
9
+ SUPPORTED_BROWSERS,
10
+ } from '../../native-host/src/config.js';
7
11
  import { resolveDefaultExtensionId } from '../../native-host/src/install-manifest.js';
8
12
  import { methodNeedsTab } from './cli-helpers.js';
9
13
  import { BridgeClient } from './client.js';
@@ -65,7 +69,7 @@ export async function requestBridge(client, method, params = {}, options = {}) {
65
69
  method,
66
70
  params,
67
71
  tabId: methodNeedsTab(method) ? (options.tabId ?? null) : null,
68
- meta: withRequestMeta(options.source, options.tokenBudget)
72
+ meta: withRequestMeta(options.source, options.tokenBudget),
69
73
  });
70
74
  }
71
75
 
@@ -81,9 +85,14 @@ export async function resolveRef(client, refOrSelector, tabId = null, source) {
81
85
  return refOrSelector;
82
86
  }
83
87
 
84
- const response = await requestBridge(client, 'dom.query', {
85
- selector: refOrSelector
86
- }, { tabId, source });
88
+ const response = await requestBridge(
89
+ client,
90
+ 'dom.query',
91
+ {
92
+ selector: refOrSelector,
93
+ },
94
+ { tabId, source }
95
+ );
87
96
 
88
97
  if (!response.ok) {
89
98
  throw new Error(response.error.message);
@@ -184,9 +193,7 @@ export async function checkBrowserManifests() {
184
193
  */
185
194
  export async function getDoctorReport(options = {}) {
186
195
  const manifest = await (options.loadManifest || loadInstalledManifest)();
187
- const allowedOrigins = Array.isArray(manifest?.allowed_origins)
188
- ? manifest.allowed_origins
189
- : [];
196
+ const allowedOrigins = Array.isArray(manifest?.allowed_origins) ? manifest.allowed_origins : [];
190
197
  const manifestInstalled = Boolean(manifest);
191
198
  const defaultExtensionId = options.defaultExtensionIdInfo || resolveDefaultExtensionId();
192
199
 
@@ -208,7 +215,7 @@ export async function getDoctorReport(options = {}) {
208
215
  routeReason: 'access_disabled',
209
216
  issues: [],
210
217
  nextSteps: [],
211
- browserManifests
218
+ browserManifests,
212
219
  };
213
220
 
214
221
  try {
@@ -217,7 +224,8 @@ export async function getDoctorReport(options = {}) {
217
224
  if (!response.ok) {
218
225
  throw new Error(response.error.message);
219
226
  }
220
- const result = /** @type {{ daemon?: string, extensionConnected?: boolean, access?: {
227
+ const result =
228
+ /** @type {{ daemon?: string, extensionConnected?: boolean, access?: {
221
229
  enabled?: boolean,
222
230
  windowId?: number | null,
223
231
  routeTabId?: number | null,
@@ -227,10 +235,13 @@ export async function getDoctorReport(options = {}) {
227
235
  report.daemonReachable = result.daemon === 'ok';
228
236
  report.extensionConnected = result.extensionConnected === true;
229
237
  report.accessEnabled = result.access?.enabled === true;
230
- report.enabledWindowId = typeof result.access?.windowId === 'number' ? result.access.windowId : null;
231
- report.routeTabId = typeof result.access?.routeTabId === 'number' ? result.access.routeTabId : null;
238
+ report.enabledWindowId =
239
+ typeof result.access?.windowId === 'number' ? result.access.windowId : null;
240
+ report.routeTabId =
241
+ typeof result.access?.routeTabId === 'number' ? result.access.routeTabId : null;
232
242
  report.routeReady = result.access?.routeReady === true;
233
- report.routeReason = typeof result.access?.reason === 'string' ? result.access.reason : 'access_disabled';
243
+ report.routeReason =
244
+ typeof result.access?.reason === 'string' ? result.access.reason : 'access_disabled';
234
245
  });
235
246
  } catch {
236
247
  report.daemonReachable = false;
@@ -241,13 +252,17 @@ export async function getDoctorReport(options = {}) {
241
252
 
242
253
  if (!report.manifestInstalled) {
243
254
  report.issues.push('native_host_manifest_missing');
244
- report.nextSteps.push(defaultExtensionId.extensionId
245
- ? 'Run `bbx install` (or `bbx install --all` for all browsers) to install the native host manifest.'
246
- : 'Run `bbx install <extension-id>` (or `bbx install --all`) to install the native host manifest.');
255
+ report.nextSteps.push(
256
+ defaultExtensionId.extensionId
257
+ ? 'Run `bbx install` (or `bbx install --all` for all browsers) to install the native host manifest.'
258
+ : 'Run `bbx install <extension-id>` (or `bbx install --all`) to install the native host manifest.'
259
+ );
247
260
  } else if (browsersWithoutManifest.length > 0) {
248
261
  report.issues.push('native_host_manifest_partial');
249
262
  const missing = browsersWithoutManifest.map((b) => b.browser).join(', ');
250
- report.nextSteps.push(`Manifests missing for: ${missing}. Run \`bbx install --all\` to install for all supported browsers.`);
263
+ report.nextSteps.push(
264
+ `Manifests missing for: ${missing}. Run \`bbx install --all\` to install for all supported browsers.`
265
+ );
251
266
  }
252
267
  if (!report.daemonReachable) {
253
268
  report.issues.push('daemon_offline');
@@ -255,14 +270,20 @@ export async function getDoctorReport(options = {}) {
255
270
  }
256
271
  if (report.daemonReachable && !report.extensionConnected) {
257
272
  report.issues.push('extension_disconnected');
258
- report.nextSteps.push('Open Chrome and make sure the Browser Bridge extension is installed and active.');
273
+ report.nextSteps.push(
274
+ 'Open Chrome and make sure the Browser Bridge extension is installed and active.'
275
+ );
259
276
  }
260
277
  if (report.daemonReachable && report.extensionConnected && !report.accessEnabled) {
261
278
  report.issues.push('access_disabled');
262
- report.nextSteps.push('If a Browser Bridge call returns ACCESS_DENIED, stop requesting access. Ask the user to click Enable for the needed window, then tell you when that window is ready.');
279
+ report.nextSteps.push(
280
+ 'If a Browser Bridge call returns ACCESS_DENIED, stop requesting access. Ask the user to click Enable for the needed window, then tell you when that window is ready.'
281
+ );
263
282
  } else if (report.daemonReachable && report.extensionConnected && !report.routeReady) {
264
283
  report.issues.push(report.routeReason || 'no_routable_active_tab');
265
- report.nextSteps.push('Switch to a supported page in the enabled window, or use an explicit tabId override.');
284
+ report.nextSteps.push(
285
+ 'Switch to a supported page in the enabled window, or use an explicit tabId override.'
286
+ );
266
287
  }
267
288
 
268
289
  return report;
@@ -77,13 +77,9 @@ export async function collectSetupStatus(options = {}) {
77
77
  const access = options.access || fs.promises.access.bind(fs.promises);
78
78
  const readFile = options.readFile || fs.promises.readFile.bind(fs.promises);
79
79
  const detectedMcpClients = new Set(detectMcpClients(options.mcpDetectors));
80
- const detectedSkillTargets = new Set(
81
- detectSkillTargets(options.skillDetectors),
82
- );
80
+ const detectedSkillTargets = new Set(detectSkillTargets(options.skillDetectors));
83
81
  for (const clientName of detectedMcpClients) {
84
- if (
85
- SUPPORTED_TARGETS.includes(/** @type {SupportedTarget} */ (clientName))
86
- ) {
82
+ if (SUPPORTED_TARGETS.includes(/** @type {SupportedTarget} */ (clientName))) {
87
83
  detectedSkillTargets.add(/** @type {SupportedTarget} */ (clientName));
88
84
  }
89
85
  }
@@ -96,7 +92,7 @@ export async function collectSetupStatus(options = {}) {
96
92
  detected: detectedMcpClients.has(clientName),
97
93
  readFile,
98
94
  });
99
- }),
95
+ })
100
96
  );
101
97
  const skillTargets = await Promise.all(
102
98
  SUPPORTED_TARGETS.map(async (target) => {
@@ -107,7 +103,7 @@ export async function collectSetupStatus(options = {}) {
107
103
  access,
108
104
  readFile,
109
105
  });
110
- }),
106
+ })
111
107
  );
112
108
 
113
109
  return {
@@ -138,12 +134,8 @@ async function collectMcpClientStatus(clientName, options) {
138
134
  });
139
135
  const entries = await Promise.all(
140
136
  configPaths.map(async (configPath) => {
141
- return readBrowserBridgeMcpEntry(
142
- clientName,
143
- configPath,
144
- options.readFile,
145
- );
146
- }),
137
+ return readBrowserBridgeMcpEntry(clientName, configPath, options.readFile);
138
+ })
147
139
  );
148
140
  const preferredEntry =
149
141
  entries.find((entry) => entry.configured) ||
@@ -188,20 +180,17 @@ async function collectSkillTargetStatus(target, options) {
188
180
  skillName,
189
181
  sentinelFilename,
190
182
  options.access,
191
- options.readFile,
183
+ options.readFile
192
184
  );
193
- }),
185
+ })
194
186
  );
195
187
  const skillByName = new Map(skills.map((skill) => [skill.name, skill]));
196
188
  const coreSkill = skillByName.get(coreSkillName) || null;
197
189
  const coreInstalled = Boolean(coreSkill?.exists);
198
190
  const coreManaged = Boolean(coreSkill?.exists && coreSkill.managed);
199
- const installedVersion = getInstalledSkillBundleVersion(
200
- coreSkill ? [coreSkill] : [],
201
- );
191
+ const installedVersion = getInstalledSkillBundleVersion(coreSkill ? [coreSkill] : []);
202
192
  const updateAvailable =
203
- coreManaged &&
204
- isManagedVersionOutdated(coreSkill?.version || null, currentVersion);
193
+ coreManaged && isManagedVersionOutdated(coreSkill?.version || null, currentVersion);
205
194
 
206
195
  return {
207
196
  key: target,
@@ -230,15 +219,13 @@ async function collectInstalledSkillStatus(
230
219
  skillName,
231
220
  sentinelFilename,
232
221
  access,
233
- readFile,
222
+ readFile
234
223
  ) {
235
224
  const skillPath = path.join(basePath, skillName);
236
225
  const exists = await pathExists(skillPath, access);
237
226
  const sentinelPath = path.join(skillPath, sentinelFilename);
238
227
  const managed = exists && (await pathExists(sentinelPath, access));
239
- const version = managed
240
- ? await readManagedSkillVersion(sentinelPath, readFile)
241
- : null;
228
+ const version = managed ? await readManagedSkillVersion(sentinelPath, readFile) : null;
242
229
 
243
230
  return {
244
231
  name: skillName,
@@ -261,9 +248,7 @@ function getInstalledSkillBundleVersion(skills) {
261
248
  if (!first || typeof first.version !== 'string') {
262
249
  return null;
263
250
  }
264
- return skills.every((skill) => skill.version === first.version)
265
- ? first.version
266
- : null;
251
+ return skills.every((skill) => skill.version === first.version) ? first.version : null;
267
252
  }
268
253
 
269
254
  /**
@@ -14,9 +14,22 @@ import {
14
14
  /** @typedef {import('../../protocol/src/types.js').ErrorCode} ErrorCode */
15
15
 
16
16
  const INTERACTIVE_AX_ROLES = new Set([
17
- 'button', 'link', 'textbox', 'checkbox', 'radio', 'combobox',
18
- 'listbox', 'menuitem', 'tab', 'switch', 'slider', 'spinbutton',
19
- 'searchbox', 'menuitemcheckbox', 'menuitemradio', 'option'
17
+ 'button',
18
+ 'link',
19
+ 'textbox',
20
+ 'checkbox',
21
+ 'radio',
22
+ 'combobox',
23
+ 'listbox',
24
+ 'menuitem',
25
+ 'tab',
26
+ 'switch',
27
+ 'slider',
28
+ 'spinbutton',
29
+ 'searchbox',
30
+ 'menuitemcheckbox',
31
+ 'menuitemradio',
32
+ 'option',
20
33
  ]);
21
34
 
22
35
  /**
@@ -31,7 +44,7 @@ export function summarizeTabResult(tab, method) {
31
44
  windowId: typeof tab.windowId === 'number' ? tab.windowId : null,
32
45
  url: tab.url ?? '',
33
46
  title: tab.title ?? '',
34
- status: tab.status ?? 'unknown'
47
+ status: tab.status ?? 'unknown',
35
48
  };
36
49
  }
37
50
 
@@ -84,7 +97,7 @@ export function simplifyAXNode(node) {
84
97
  checked: axTristateValue(node.checked),
85
98
  disabled: axBool(node.disabled),
86
99
  interactive: INTERACTIVE_AX_ROLES.has(role) || axBool(node.focusable),
87
- childIds: Array.isArray(node.childIds) ? node.childIds.map(String) : []
100
+ childIds: Array.isArray(node.childIds) ? node.childIds.map(String) : [],
88
101
  };
89
102
  }
90
103
 
@@ -99,7 +112,7 @@ export function shouldLogAction(method) {
99
112
  'skill.get_runtime_context',
100
113
  'setup.get_status',
101
114
  'setup.install',
102
- 'tabs.list'
115
+ 'tabs.list',
103
116
  ].includes(method);
104
117
  }
105
118
 
@@ -130,9 +143,10 @@ export function summarizeActionResult(response) {
130
143
  return response.error.message;
131
144
  }
132
145
 
133
- const result = response.result && typeof response.result === 'object'
134
- ? /** @type {Record<string, unknown>} */ (response.result)
135
- : {};
146
+ const result =
147
+ response.result && typeof response.result === 'object'
148
+ ? /** @type {Record<string, unknown>} */ (response.result)
149
+ : {};
136
150
 
137
151
  if (typeof result.patchId === 'string') {
138
152
  return `Patch ${result.patchId} applied.`;
@@ -165,24 +179,19 @@ export function summarizeActionResult(response) {
165
179
  * }}
166
180
  */
167
181
  export function estimateResponseTokens(response) {
168
- const payload = response.ok
169
- ? response.result
170
- : { error: response.error };
182
+ const payload = response.ok ? response.result : { error: response.error };
171
183
  const estimate = estimateJsonPayloadCost(payload);
172
184
  const responseBytes = estimate.bytes;
173
- const result = response.ok && response.result && typeof response.result === 'object'
174
- ? /** @type {Record<string, unknown>} */ (response.result)
175
- : null;
185
+ const result =
186
+ response.ok && response.result && typeof response.result === 'object'
187
+ ? /** @type {Record<string, unknown>} */ (response.result)
188
+ : null;
176
189
  const hasScreenshot = result != null && typeof result.image === 'string';
177
190
  const nodeCount = result != null && Array.isArray(result.nodes) ? result.nodes.length : null;
178
- const textPayload = hasScreenshot && result != null
179
- ? omitScreenshotImage(result)
180
- : payload;
191
+ const textPayload = hasScreenshot && result != null ? omitScreenshotImage(result) : payload;
181
192
  const textEstimate = estimateJsonPayloadCost(textPayload);
182
193
  const imageTransportBytes = Math.max(0, responseBytes - textEstimate.bytes);
183
- const imageBytes = hasScreenshot && result != null
184
- ? estimateInlineImageBytes(result.image)
185
- : 0;
194
+ const imageBytes = hasScreenshot && result != null ? estimateInlineImageBytes(result.image) : 0;
186
195
 
187
196
  return {
188
197
  responseBytes,
@@ -258,11 +267,7 @@ function estimateInlineImageBytes(image) {
258
267
  return 0;
259
268
  }
260
269
 
261
- const padding = base64.endsWith('==')
262
- ? 2
263
- : base64.endsWith('=')
264
- ? 1
265
- : 0;
270
+ const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
266
271
  return Math.max(0, Math.floor((base64.length * 3) / 4) - padding);
267
272
  }
268
273
 
@@ -277,7 +282,12 @@ function estimateInlineImageBytes(image) {
277
282
  * @returns {BridgeResponse}
278
283
  */
279
284
  export function enforceTokenBudget(method, response, tokenBudget) {
280
- if (!response.ok || typeof tokenBudget !== 'number' || !Number.isFinite(tokenBudget) || tokenBudget <= 0) {
285
+ if (
286
+ !response.ok ||
287
+ typeof tokenBudget !== 'number' ||
288
+ !Number.isFinite(tokenBudget) ||
289
+ tokenBudget <= 0
290
+ ) {
281
291
  return response;
282
292
  }
283
293
 
@@ -299,7 +309,11 @@ export function enforceTokenBudget(method, response, tokenBudget) {
299
309
  let truncated = false;
300
310
  let iterations = 0;
301
311
  const MAX_BUDGET_ITERATIONS = 100;
302
- while (estimateJsonPayloadCost(cloned).bytes > maxBytes && shrinkForBudget(cloned) && iterations < MAX_BUDGET_ITERATIONS) {
312
+ while (
313
+ estimateJsonPayloadCost(cloned).bytes > maxBytes &&
314
+ shrinkForBudget(cloned) &&
315
+ iterations < MAX_BUDGET_ITERATIONS
316
+ ) {
303
317
  truncated = true;
304
318
  iterations += 1;
305
319
  }
@@ -355,9 +369,10 @@ function shrinkForBudget(value) {
355
369
 
356
370
  for (const key of ['image', 'html', 'text', 'value']) {
357
371
  if (typeof value[key] === 'string' && value[key].length > 64) {
358
- value[key] = key === 'image'
359
- ? '[omitted image over token budget]'
360
- : `${value[key].slice(0, Math.max(32, Math.floor(value[key].length * 0.75) - 1))}\u2026`;
372
+ value[key] =
373
+ key === 'image'
374
+ ? '[omitted image over token budget]'
375
+ : `${value[key].slice(0, Math.max(32, Math.floor(value[key].length * 0.75) - 1))}\u2026`;
361
376
  if (typeof value.truncated !== 'boolean') {
362
377
  value.truncated = true;
363
378
  }
@@ -419,9 +434,7 @@ export function getErrorMessage(error) {
419
434
  * @returns {string}
420
435
  */
421
436
  export function normalizeRuntimeErrorMessage(message) {
422
- return /^No tab with id[: ]/i.test(message)
423
- ? ERROR_CODES.TAB_MISMATCH
424
- : message;
437
+ return /^No tab with id[: ]/i.test(message) ? ERROR_CODES.TAB_MISMATCH : message;
425
438
  }
426
439
 
427
440
  /**
@@ -434,7 +447,7 @@ export function normalizeCropRect(rect = {}) {
434
447
  x: Math.max(0, Math.round((rect.x || 0) * scale)),
435
448
  y: Math.max(0, Math.round((rect.y || 0) * scale)),
436
449
  width: Math.max(1, Math.round((rect.width || 1) * scale)),
437
- height: Math.max(1, Math.round((rect.height || 1) * scale))
450
+ height: Math.max(1, Math.round((rect.height || 1) * scale)),
438
451
  };
439
452
  }
440
453
 
@@ -455,5 +468,7 @@ export function safeOrigin(url) {
455
468
  * @returns {Capability | null}
456
469
  */
457
470
  export function inferCapability(method) {
458
- return getMethodCapability(/** @type {import('../../protocol/src/types.js').BridgeMethod} */ (method));
471
+ return getMethodCapability(
472
+ /** @type {import('../../protocol/src/types.js').BridgeMethod} */ (method)
473
+ );
459
474
  }
@@ -16,7 +16,9 @@ import { ERROR_CODES } from '../../protocol/src/index.js';
16
16
  * @returns {boolean}
17
17
  */
18
18
  export function isRestrictedAutomationUrl(url) {
19
- return /^(about:|chrome:|chrome-extension:|chrome-search:|devtools:|edge:|brave:|moz-extension:|view-source:)/i.test(url);
19
+ return /^(about:|chrome:|chrome-extension:|chrome-search:|devtools:|edge:|brave:|moz-extension:|view-source:)/i.test(
20
+ url
21
+ );
20
22
  }
21
23
 
22
24
  /**
@@ -40,11 +42,7 @@ export function selectRequestTabCandidate(requestTabId, explicitTab, activeTab)
40
42
  */
41
43
  export function resolveWindowScopedTab(tab, enabledWindowId, options = {}) {
42
44
  const requireScriptable = options.requireScriptable !== false;
43
- if (
44
- typeof tab?.id !== 'number'
45
- || !Number.isFinite(tab.id)
46
- || typeof tab.windowId !== 'number'
47
- ) {
45
+ if (typeof tab?.id !== 'number' || !Number.isFinite(tab.id) || typeof tab.windowId !== 'number') {
48
46
  throw new Error(ERROR_CODES.TAB_MISMATCH);
49
47
  }
50
48
  if (tab.windowId !== enabledWindowId) {
@@ -61,7 +59,7 @@ export function resolveWindowScopedTab(tab, enabledWindowId, options = {}) {
61
59
  tabId: tab.id,
62
60
  windowId: tab.windowId,
63
61
  title: tab.title ?? '',
64
- url: tab.url
62
+ url: tab.url,
65
63
  };
66
64
  }
67
65
 
@@ -71,11 +69,11 @@ export function resolveWindowScopedTab(tab, enabledWindowId, options = {}) {
71
69
  */
72
70
  export function normalizeRequestedAccessTab(tab) {
73
71
  if (
74
- typeof tab?.id !== 'number'
75
- || !Number.isFinite(tab.id)
76
- || typeof tab.windowId !== 'number'
77
- || typeof tab.url !== 'string'
78
- || !tab.url
72
+ typeof tab?.id !== 'number' ||
73
+ !Number.isFinite(tab.id) ||
74
+ typeof tab.windowId !== 'number' ||
75
+ typeof tab.url !== 'string' ||
76
+ !tab.url
79
77
  ) {
80
78
  return null;
81
79
  }
@@ -86,6 +84,6 @@ export function normalizeRequestedAccessTab(tab) {
86
84
  tabId: tab.id,
87
85
  windowId: tab.windowId,
88
86
  title: tab.title ?? '',
89
- url: tab.url
87
+ url: tab.url,
90
88
  };
91
89
  }