btcp-browser-agent 0.1.7 → 0.1.9

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,230 @@
1
+ /**
2
+ * @btcp/core - Assertions Module
3
+ *
4
+ * Provides verification utilities for DOM actions with async waiting support.
5
+ * Returns structured results for consistent error handling.
6
+ */
7
+ // ============================================================================
8
+ // CORE ASSERTIONS
9
+ // ============================================================================
10
+ /**
11
+ * Assert that an element is still connected to the DOM
12
+ */
13
+ export function assertConnected(element) {
14
+ const connected = element.isConnected;
15
+ return {
16
+ success: connected,
17
+ error: connected ? null : 'Element is not connected to DOM',
18
+ description: 'Element should be connected to DOM',
19
+ expected: true,
20
+ actual: connected,
21
+ context: {
22
+ tagName: element.tagName,
23
+ },
24
+ };
25
+ }
26
+ /**
27
+ * Assert that an input/textarea value contains the expected text
28
+ */
29
+ export function assertValueContains(element, text) {
30
+ const value = element.value;
31
+ const contains = value.includes(text);
32
+ return {
33
+ success: contains,
34
+ error: contains ? null : `Value "${value}" does not contain "${text}"`,
35
+ description: 'Value should contain expected text',
36
+ expected: text,
37
+ actual: value,
38
+ context: {
39
+ tagName: element.tagName,
40
+ inputType: element instanceof HTMLInputElement ? element.type : 'textarea',
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * Assert that an input/textarea value equals the expected value exactly
46
+ */
47
+ export function assertValueEquals(element, value) {
48
+ const actualValue = element.value;
49
+ const equals = actualValue === value;
50
+ return {
51
+ success: equals,
52
+ error: equals ? null : `Value "${actualValue}" does not equal "${value}"`,
53
+ description: 'Value should equal expected value',
54
+ expected: value,
55
+ actual: actualValue,
56
+ context: {
57
+ tagName: element.tagName,
58
+ inputType: element instanceof HTMLInputElement ? element.type : 'textarea',
59
+ },
60
+ };
61
+ }
62
+ /**
63
+ * Assert that a checkbox/radio is in the expected checked state
64
+ */
65
+ export function assertChecked(element, expected) {
66
+ const checked = element.checked;
67
+ const matches = checked === expected;
68
+ return {
69
+ success: matches,
70
+ error: matches ? null : `Element is ${checked ? 'checked' : 'unchecked'}, expected ${expected ? 'checked' : 'unchecked'}`,
71
+ description: expected ? 'Element should be checked' : 'Element should be unchecked',
72
+ expected,
73
+ actual: checked,
74
+ context: {
75
+ tagName: element.tagName,
76
+ inputType: element.type,
77
+ },
78
+ };
79
+ }
80
+ /**
81
+ * Assert that a select element has the expected values selected
82
+ */
83
+ export function assertSelected(element, values) {
84
+ const selectedValues = Array.from(element.selectedOptions).map(opt => opt.value);
85
+ const missingValues = values.filter(v => !selectedValues.includes(v));
86
+ const success = missingValues.length === 0;
87
+ return {
88
+ success,
89
+ error: success ? null : `Options not selected: ${missingValues.join(', ')}`,
90
+ description: 'Select should have expected options selected',
91
+ expected: values,
92
+ actual: selectedValues,
93
+ context: {
94
+ tagName: element.tagName,
95
+ multiple: element.multiple,
96
+ missingValues: missingValues.length > 0 ? missingValues : undefined,
97
+ },
98
+ };
99
+ }
100
+ /**
101
+ * Assert that a URL matches the expected origin
102
+ */
103
+ export function assertUrlOrigin(actualUrl, expectedUrl) {
104
+ try {
105
+ const actual = new URL(actualUrl);
106
+ const expected = new URL(expectedUrl);
107
+ const success = actual.origin === expected.origin;
108
+ return {
109
+ success,
110
+ error: success ? null : `Origin mismatch: expected ${expected.origin}, got ${actual.origin}`,
111
+ description: 'URL origin should match expected',
112
+ expected: expected.origin,
113
+ actual: actual.origin,
114
+ context: {
115
+ actualUrl,
116
+ expectedUrl,
117
+ },
118
+ };
119
+ }
120
+ catch (error) {
121
+ // URL parsing failed
122
+ return {
123
+ success: false,
124
+ error: `URL parsing failed: ${error instanceof Error ? error.message : String(error)}`,
125
+ description: 'URL origin should match expected',
126
+ expected: expectedUrl,
127
+ actual: actualUrl,
128
+ context: {
129
+ error: error instanceof Error ? error.message : String(error),
130
+ },
131
+ };
132
+ }
133
+ }
134
+ /**
135
+ * Assert that an element is visible (or hidden)
136
+ */
137
+ export function assertVisible(element, win, expected = true) {
138
+ const style = win.getComputedStyle(element);
139
+ const visible = style.display !== 'none' &&
140
+ style.visibility !== 'hidden' &&
141
+ style.opacity !== '0';
142
+ const success = visible === expected;
143
+ return {
144
+ success,
145
+ error: success ? null : `Element is ${visible ? 'visible' : 'hidden'}, expected ${expected ? 'visible' : 'hidden'}`,
146
+ description: expected ? 'Element should be visible' : 'Element should be hidden',
147
+ expected,
148
+ actual: visible,
149
+ context: {
150
+ tagName: element.tagName,
151
+ display: style.display,
152
+ visibility: style.visibility,
153
+ opacity: style.opacity,
154
+ },
155
+ };
156
+ }
157
+ /**
158
+ * Assert that an element is enabled (or disabled)
159
+ */
160
+ export function assertEnabled(element, expected = true) {
161
+ const disabled = element.disabled ?? false;
162
+ const enabled = !disabled;
163
+ const success = enabled === expected;
164
+ return {
165
+ success,
166
+ error: success ? null : `Element is ${enabled ? 'enabled' : 'disabled'}, expected ${expected ? 'enabled' : 'disabled'}`,
167
+ description: expected ? 'Element should be enabled' : 'Element should be disabled',
168
+ expected,
169
+ actual: enabled,
170
+ context: {
171
+ tagName: element.tagName,
172
+ disabled,
173
+ },
174
+ };
175
+ }
176
+ // ============================================================================
177
+ // ASYNC WAITING
178
+ // ============================================================================
179
+ /**
180
+ * Wait for an assertion to pass within a timeout period
181
+ *
182
+ * Polls the assertion function at regular intervals until it passes
183
+ * or the timeout is reached.
184
+ *
185
+ * @param assertFn - Function that returns an AssertionResult
186
+ * @param options - Timeout and interval options
187
+ * @returns WaitResult with success status and timing information
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * const result = await waitForAssertion(
192
+ * () => assertValueEquals(input, 'hello'),
193
+ * { timeout: 1000, interval: 50 }
194
+ * );
195
+ *
196
+ * if (!result.success) {
197
+ * throw createVerificationError('fill', result, selector);
198
+ * }
199
+ * ```
200
+ */
201
+ export async function waitForAssertion(assertFn, options = {}) {
202
+ const { timeout = 1000, interval = 50 } = options;
203
+ const startTime = Date.now();
204
+ let attempts = 0;
205
+ let lastResult;
206
+ while (Date.now() - startTime < timeout) {
207
+ attempts++;
208
+ lastResult = assertFn();
209
+ if (lastResult.success) {
210
+ return {
211
+ success: true,
212
+ elapsed: Date.now() - startTime,
213
+ attempts,
214
+ result: lastResult,
215
+ };
216
+ }
217
+ // Wait before next attempt
218
+ await new Promise(resolve => setTimeout(resolve, interval));
219
+ }
220
+ // Final attempt after timeout
221
+ attempts++;
222
+ lastResult = assertFn();
223
+ return {
224
+ success: lastResult.success,
225
+ elapsed: Date.now() - startTime,
226
+ attempts,
227
+ result: lastResult,
228
+ };
229
+ }
230
+ //# sourceMappingURL=assertions.js.map
@@ -4,6 +4,7 @@
4
4
  * Provides structured error types with machine-readable codes and
5
5
  * actionable suggestions to help AI agents self-correct.
6
6
  */
7
+ import type { WaitResult } from './assertions.js';
7
8
  /**
8
9
  * Machine-readable error codes for programmatic error handling
9
10
  */
@@ -27,7 +28,9 @@ export declare enum ErrorCode {
27
28
  /** Element is not in the expected state */
28
29
  INVALID_STATE = "INVALID_STATE",
29
30
  /** Network or navigation error */
30
- NAVIGATION_ERROR = "NAVIGATION_ERROR"
31
+ NAVIGATION_ERROR = "NAVIGATION_ERROR",
32
+ /** Action verification failed after completion */
33
+ VERIFICATION_FAILED = "VERIFICATION_FAILED"
31
34
  }
32
35
  /**
33
36
  * Structured context information for errors
@@ -135,4 +138,23 @@ export declare function createTimeoutError(selector: string | undefined, expecte
135
138
  * Helper function to create invalid parameters error
136
139
  */
137
140
  export declare function createInvalidParametersError(message: string, conflictingParams: string[], suggestion: string): DetailedError;
141
+ /**
142
+ * Helper function to create verification error with assertion details
143
+ *
144
+ * @param action - The action that failed verification (e.g., 'fill', 'type', 'check')
145
+ * @param waitResult - The result from waitForAssertion
146
+ * @param selector - Optional selector for the element
147
+ *
148
+ * @example
149
+ * ```typescript
150
+ * const result = await waitForAssertion(
151
+ * () => assertValueEquals(element, value)
152
+ * );
153
+ *
154
+ * if (!result.success) {
155
+ * throw createVerificationError('fill', result, selector);
156
+ * }
157
+ * ```
158
+ */
159
+ export declare function createVerificationError(action: string, waitResult: WaitResult, selector?: string): DetailedError;
138
160
  //# sourceMappingURL=errors.d.ts.map
@@ -29,6 +29,8 @@ export var ErrorCode;
29
29
  ErrorCode["INVALID_STATE"] = "INVALID_STATE";
30
30
  /** Network or navigation error */
31
31
  ErrorCode["NAVIGATION_ERROR"] = "NAVIGATION_ERROR";
32
+ /** Action verification failed after completion */
33
+ ErrorCode["VERIFICATION_FAILED"] = "VERIFICATION_FAILED";
32
34
  })(ErrorCode || (ErrorCode = {}));
33
35
  /**
34
36
  * Detailed error with structured data for AI agents
@@ -154,4 +156,51 @@ export function createInvalidParametersError(message, conflictingParams, suggest
154
156
  conflictingParams,
155
157
  }, [suggestion]);
156
158
  }
159
+ /**
160
+ * Helper function to create verification error with assertion details
161
+ *
162
+ * @param action - The action that failed verification (e.g., 'fill', 'type', 'check')
163
+ * @param waitResult - The result from waitForAssertion
164
+ * @param selector - Optional selector for the element
165
+ *
166
+ * @example
167
+ * ```typescript
168
+ * const result = await waitForAssertion(
169
+ * () => assertValueEquals(element, value)
170
+ * );
171
+ *
172
+ * if (!result.success) {
173
+ * throw createVerificationError('fill', result, selector);
174
+ * }
175
+ * ```
176
+ */
177
+ export function createVerificationError(action, waitResult, selector) {
178
+ const { result, elapsed, attempts } = waitResult;
179
+ const suggestions = [];
180
+ // Add context-specific suggestions based on the assertion
181
+ if (result.description.includes('value')) {
182
+ suggestions.push('The element value may have been modified by event handlers.', 'Check for input masking, formatting, or validation that transforms the value.');
183
+ }
184
+ if (result.description.includes('checked')) {
185
+ suggestions.push('The checkbox/radio state may be controlled by JavaScript.', 'Verify the element is not disabled or read-only.');
186
+ }
187
+ if (result.description.includes('selected')) {
188
+ suggestions.push('Some options may not exist in the select element.', 'Use snapshot() to verify available option values.');
189
+ }
190
+ if (result.description.includes('origin')) {
191
+ suggestions.push('The page may have redirected to a different domain.', 'Check for authentication redirects or HTTPS upgrades.');
192
+ }
193
+ suggestions.push(`Verification waited ${elapsed}ms with ${attempts} attempts.`);
194
+ const message = selector
195
+ ? `${action} verification failed for ${selector}: ${result.description}`
196
+ : `${action} verification failed: ${result.description}`;
197
+ return new DetailedError(ErrorCode.VERIFICATION_FAILED, message, {
198
+ selector,
199
+ expected: result.expected,
200
+ actual: result.actual,
201
+ elapsed,
202
+ attempts,
203
+ ...result.context,
204
+ }, suggestions);
205
+ }
157
206
  //# sourceMappingURL=errors.js.map
@@ -20,6 +20,7 @@
20
20
  import type { Command, Response, RefMap } from './types.js';
21
21
  export * from './types.js';
22
22
  export * from './errors.js';
23
+ export * from './assertions.js';
23
24
  export { createSnapshot } from './snapshot.js';
24
25
  export { createRefMap, createSimpleRefMap } from './ref-map.js';
25
26
  export { DOMActions, generateCommandId } from './actions.js';
@@ -21,6 +21,7 @@ import { DOMActions } from './actions.js';
21
21
  import { createRefMap, createSimpleRefMap } from './ref-map.js';
22
22
  export * from './types.js';
23
23
  export * from './errors.js';
24
+ export * from './assertions.js';
24
25
  export { createSnapshot } from './snapshot.js';
25
26
  export { createRefMap, createSimpleRefMap } from './ref-map.js';
26
27
  export { DOMActions, generateCommandId } from './actions.js';
@@ -119,8 +119,10 @@ export declare class BackgroundAgent {
119
119
  switchTab(tabId: number): Promise<void>;
120
120
  /**
121
121
  * Navigate to a URL (only in session tabs)
122
+ * Always waits for page to be idle before returning.
123
+ * Verifies navigation completed to the expected origin.
122
124
  */
123
- navigate(url: string, options?: {
125
+ navigate(url: string, _options?: {
124
126
  waitUntil?: 'load' | 'domcontentloaded';
125
127
  }): Promise<void>;
126
128
  /**
@@ -205,6 +207,11 @@ export declare class BackgroundAgent {
205
207
  private isExtensionCommand;
206
208
  private executeExtensionCommand;
207
209
  private waitForTabLoad;
210
+ /**
211
+ * Wait for the page to become idle (no pending network requests or JS execution)
212
+ * Uses requestIdleCallback in the content script to detect when the browser is idle.
213
+ */
214
+ private waitForIdle;
208
215
  }
209
216
  /**
210
217
  * Get or create the BackgroundAgent singleton
@@ -12,6 +12,7 @@
12
12
  * - Routing DOM commands to ContentAgents in target tabs
13
13
  */
14
14
  import { SessionManager } from './session-manager.js';
15
+ import { assertUrlOrigin, createVerificationError } from '../../core/dist/index.js';
15
16
  // Command ID counter for auto-generated IDs
16
17
  let bgCommandIdCounter = 0;
17
18
  /**
@@ -282,8 +283,10 @@ export class BackgroundAgent {
282
283
  // ============================================================================
283
284
  /**
284
285
  * Navigate to a URL (only in session tabs)
286
+ * Always waits for page to be idle before returning.
287
+ * Verifies navigation completed to the expected origin.
285
288
  */
286
- async navigate(url, options) {
289
+ async navigate(url, _options) {
287
290
  const tabId = this.activeTabId ?? (await this.getActiveTab())?.id;
288
291
  if (!tabId)
289
292
  throw new Error('No active tab');
@@ -295,20 +298,40 @@ export class BackgroundAgent {
295
298
  await new Promise((resolve) => {
296
299
  chrome.tabs.update(tabId, { url }, () => resolve());
297
300
  });
298
- if (options?.waitUntil) {
299
- await this.waitForTabLoad(tabId);
300
- // Clear refs and highlights after navigation completes
301
- try {
302
- await this.sendToContentAgent({
303
- id: `nav_clear_${Date.now()}`,
304
- action: 'clearHighlight'
305
- }, tabId);
301
+ // Always wait for tab to load and become idle
302
+ await this.waitForTabLoad(tabId);
303
+ await this.waitForIdle(tabId);
304
+ // Verify navigation completed to the expected origin
305
+ const tab = await chrome.tabs.get(tabId);
306
+ const finalUrl = tab.url || '';
307
+ // Use assertion module for origin verification
308
+ const result = assertUrlOrigin(finalUrl, url);
309
+ if (!result.success) {
310
+ // Only throw for actual origin mismatches, not URL parsing errors
311
+ if (result.context?.error) {
312
+ // URL parsing failed (e.g., chrome:// pages) - skip verification
313
+ console.log('[BackgroundAgent] Skipping URL verification for special page:', finalUrl);
306
314
  }
307
- catch (error) {
308
- // Ignore errors - content script might not be ready yet
309
- console.log('[BackgroundAgent] Failed to clear highlights after navigation:', error);
315
+ else {
316
+ throw createVerificationError('navigate', {
317
+ success: false,
318
+ elapsed: 0,
319
+ attempts: 1,
320
+ result,
321
+ });
310
322
  }
311
323
  }
324
+ // Clear refs and highlights after navigation completes
325
+ try {
326
+ await this.sendToContentAgent({
327
+ id: `nav_clear_${Date.now()}`,
328
+ action: 'clearHighlight'
329
+ }, tabId);
330
+ }
331
+ catch (error) {
332
+ // Ignore errors - content script might not be ready yet
333
+ console.log('[BackgroundAgent] Failed to clear highlights after navigation:', error);
334
+ }
312
335
  }
313
336
  /**
314
337
  * Go back in history
@@ -494,9 +517,9 @@ export class BackgroundAgent {
494
517
  if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://')) {
495
518
  return false; // Can't inject into chrome:// or extension pages
496
519
  }
497
- // Execute content script
520
+ // Execute content script (main frame only)
498
521
  await chrome.scripting.executeScript({
499
- target: { tabId },
522
+ target: { tabId, frameIds: [0] },
500
523
  files: ['content.js'],
501
524
  });
502
525
  console.log(`[Recovery] Successfully re-injected content script into tab ${tabId}`);
@@ -694,6 +717,42 @@ export class BackgroundAgent {
694
717
  checkTab();
695
718
  });
696
719
  }
720
+ /**
721
+ * Wait for the page to become idle (no pending network requests or JS execution)
722
+ * Uses requestIdleCallback in the content script to detect when the browser is idle.
723
+ */
724
+ async waitForIdle(tabId, timeout = 10000) {
725
+ const startTime = Date.now();
726
+ // Poll until the page reports idle or timeout
727
+ while (Date.now() - startTime < timeout) {
728
+ try {
729
+ const results = await chrome.scripting.executeScript({
730
+ target: { tabId, frameIds: [0] }, // Only inject into main frame
731
+ func: () => {
732
+ return new Promise((resolve) => {
733
+ // Use requestIdleCallback to detect when browser is idle
734
+ if ('requestIdleCallback' in window) {
735
+ requestIdleCallback(() => resolve(true), { timeout: 1000 });
736
+ }
737
+ else {
738
+ // Fallback: wait a short period
739
+ setTimeout(() => resolve(true), 500);
740
+ }
741
+ });
742
+ },
743
+ });
744
+ if (results?.[0]?.result) {
745
+ return;
746
+ }
747
+ }
748
+ catch (error) {
749
+ // Content script may not be ready yet, wait and retry
750
+ await new Promise(resolve => setTimeout(resolve, 200));
751
+ }
752
+ }
753
+ // Timeout reached, continue anyway
754
+ console.log('[BackgroundAgent] waitForIdle timeout reached, continuing');
755
+ }
697
756
  }
698
757
  // ============================================================================
699
758
  // MESSAGE LISTENER SETUP
@@ -7,6 +7,12 @@
7
7
  import { createContentAgent } from '../../core/dist/index.js';
8
8
  let agent = null;
9
9
  let isContentScriptReady = false;
10
+ // Track injected scripts by scriptId
11
+ const injectedScripts = new Map();
12
+ // Track pending script commands waiting for ack
13
+ const pendingScriptCommands = new Map();
14
+ // Counter for generating unique command IDs
15
+ let scriptCommandCounter = 0;
10
16
  /**
11
17
  * Get or create the ContentAgent instance for this page
12
18
  */
@@ -28,9 +34,113 @@ function isCoreCommand(command) {
28
34
  'navigate', 'back', 'forward', 'reload',
29
35
  'getUrl', 'getTitle', 'screenshot',
30
36
  'tabNew', 'tabClose', 'tabSwitch', 'tabList',
37
+ 'scriptInject', 'scriptSend',
31
38
  ];
32
39
  return !extensionActions.includes(command.action);
33
40
  }
41
+ /**
42
+ * Inject a script into the page's main world
43
+ */
44
+ function handleScriptInject(command) {
45
+ const id = command.id || 'unknown';
46
+ const scriptId = command.scriptId || 'default';
47
+ try {
48
+ // Remove existing script with same ID if present
49
+ const existing = injectedScripts.get(scriptId);
50
+ if (existing) {
51
+ existing.element.remove();
52
+ injectedScripts.delete(scriptId);
53
+ }
54
+ // Create script element
55
+ const script = document.createElement('script');
56
+ script.textContent = command.code;
57
+ script.setAttribute('data-btcp-script-id', scriptId);
58
+ // Inject into page's main world by appending to document
59
+ (document.head || document.documentElement).appendChild(script);
60
+ // Track the injected script
61
+ injectedScripts.set(scriptId, {
62
+ element: script,
63
+ injectedAt: Date.now(),
64
+ });
65
+ console.log(`[ContentScript] Injected script: ${scriptId}`);
66
+ return {
67
+ id,
68
+ success: true,
69
+ data: { scriptId, injected: true },
70
+ };
71
+ }
72
+ catch (error) {
73
+ return {
74
+ id,
75
+ success: false,
76
+ error: error instanceof Error ? error.message : String(error),
77
+ errorCode: 'INJECTION_FAILED',
78
+ };
79
+ }
80
+ }
81
+ /**
82
+ * Send a command to an injected script and wait for acknowledgment
83
+ */
84
+ function handleScriptSend(command) {
85
+ const id = command.id || 'unknown';
86
+ const scriptId = command.scriptId || 'default';
87
+ const timeout = command.timeout ?? 30000;
88
+ const commandId = `script_cmd_${Date.now()}_${scriptCommandCounter++}`;
89
+ return new Promise((resolve) => {
90
+ // Set up timeout
91
+ const timeoutId = setTimeout(() => {
92
+ pendingScriptCommands.delete(commandId);
93
+ resolve({
94
+ id,
95
+ success: false,
96
+ error: `Script command timed out after ${timeout}ms`,
97
+ errorCode: 'SCRIPT_TIMEOUT',
98
+ });
99
+ }, timeout);
100
+ // Track pending command
101
+ pendingScriptCommands.set(commandId, { resolve, timeoutId });
102
+ // Send command to page script via postMessage
103
+ const message = {
104
+ type: 'btcp:script-command',
105
+ commandId,
106
+ scriptId,
107
+ payload: command.payload,
108
+ };
109
+ window.postMessage(message, '*');
110
+ });
111
+ }
112
+ /**
113
+ * Listen for script acknowledgments from page scripts
114
+ */
115
+ window.addEventListener('message', (event) => {
116
+ if (event.source !== window)
117
+ return;
118
+ const msg = event.data;
119
+ if (msg?.type !== 'btcp:script-ack')
120
+ return;
121
+ const pending = pendingScriptCommands.get(msg.commandId);
122
+ if (!pending)
123
+ return;
124
+ // Clear timeout and remove from pending
125
+ clearTimeout(pending.timeoutId);
126
+ pendingScriptCommands.delete(msg.commandId);
127
+ // Resolve with response
128
+ if (msg.error) {
129
+ pending.resolve({
130
+ id: msg.commandId,
131
+ success: false,
132
+ error: msg.error,
133
+ errorCode: 'SCRIPT_ERROR',
134
+ });
135
+ }
136
+ else {
137
+ pending.resolve({
138
+ id: msg.commandId,
139
+ success: true,
140
+ data: { result: msg.result },
141
+ });
142
+ }
143
+ });
34
144
  /**
35
145
  * Handle a command from the background script
36
146
  */
@@ -54,6 +164,10 @@ async function handleCommand(command) {
54
164
  success: true,
55
165
  data: { title: document.title },
56
166
  };
167
+ case 'scriptInject':
168
+ return handleScriptInject(command);
169
+ case 'scriptSend':
170
+ return handleScriptSend(command);
57
171
  default:
58
172
  // Forward to background script
59
173
  return {