tab-runtime.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. (function attachBackgroundTabRuntime(root, factory) {
  2. root.MultiPageBackgroundTabRuntime = factory();
  3. })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundTabRuntimeModule() {
  4. function createTabRuntime(deps = {}) {
  5. const {
  6. addLog,
  7. chrome,
  8. getSourceLabel,
  9. getState,
  10. isLocalhostOAuthCallbackUrl,
  11. isRetryableContentScriptTransportError,
  12. LOG_PREFIX,
  13. matchesSourceUrlFamily,
  14. setState,
  15. STOP_ERROR_MESSAGE,
  16. throwIfStopped,
  17. } = deps;
  18. const pendingCommands = new Map();
  19. async function getTabRegistry() {
  20. const state = await getState();
  21. return state.tabRegistry || {};
  22. }
  23. async function registerTab(source, tabId) {
  24. const registry = await getTabRegistry();
  25. registry[source] = { tabId, ready: true };
  26. await setState({ tabRegistry: registry });
  27. console.log(LOG_PREFIX, `Tab registered: ${source} -> ${tabId}`);
  28. }
  29. async function isTabAlive(source) {
  30. const registry = await getTabRegistry();
  31. const entry = registry[source];
  32. if (!entry) return false;
  33. try {
  34. await chrome.tabs.get(entry.tabId);
  35. return true;
  36. } catch {
  37. registry[source] = null;
  38. await setState({ tabRegistry: registry });
  39. return false;
  40. }
  41. }
  42. async function getTabId(source) {
  43. const registry = await getTabRegistry();
  44. return registry[source]?.tabId || null;
  45. }
  46. async function rememberSourceLastUrl(source, url) {
  47. if (!source || !url) return;
  48. const state = await getState();
  49. const sourceLastUrls = { ...(state.sourceLastUrls || {}) };
  50. sourceLastUrls[source] = url;
  51. await setState({ sourceLastUrls });
  52. }
  53. async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
  54. const { excludeTabIds = [] } = options;
  55. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  56. const state = await getState();
  57. const lastUrl = state.sourceLastUrls?.[source];
  58. const referenceUrls = [currentUrl, lastUrl].filter(Boolean);
  59. if (!referenceUrls.length) return;
  60. const tabs = await chrome.tabs.query({});
  61. const matchedIds = tabs
  62. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  63. .filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
  64. .map((tab) => tab.id);
  65. if (!matchedIds.length) return;
  66. await chrome.tabs.remove(matchedIds).catch(() => { });
  67. const registry = await getTabRegistry();
  68. if (registry[source]?.tabId && matchedIds.includes(registry[source].tabId)) {
  69. registry[source] = null;
  70. await setState({ tabRegistry: registry });
  71. }
  72. await addLog(`已关闭 ${matchedIds.length} 个旧的${getSourceLabel(source)}标签页。`, 'info');
  73. }
  74. function isLocalhostOAuthCallbackTabMatch(callbackUrl, candidateUrl) {
  75. if (!isLocalhostOAuthCallbackUrl(callbackUrl) || !isLocalhostOAuthCallbackUrl(candidateUrl)) {
  76. return false;
  77. }
  78. const callback = new URL(callbackUrl);
  79. const candidate = new URL(candidateUrl);
  80. return callback.origin === candidate.origin
  81. && callback.pathname === candidate.pathname
  82. && callback.searchParams.get('code') === candidate.searchParams.get('code')
  83. && callback.searchParams.get('state') === candidate.searchParams.get('state');
  84. }
  85. async function closeLocalhostCallbackTabs(callbackUrl, options = {}) {
  86. if (!isLocalhostOAuthCallbackUrl(callbackUrl)) return 0;
  87. const { excludeTabIds = [] } = options;
  88. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  89. const tabs = await chrome.tabs.query({});
  90. const matchedIds = tabs
  91. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  92. .filter((tab) => isLocalhostOAuthCallbackTabMatch(callbackUrl, tab.url))
  93. .map((tab) => tab.id);
  94. if (!matchedIds.length) return 0;
  95. await chrome.tabs.remove(matchedIds).catch(() => { });
  96. const registry = await getTabRegistry();
  97. if (registry['signup-page']?.tabId && matchedIds.includes(registry['signup-page'].tabId)) {
  98. registry['signup-page'] = null;
  99. await setState({ tabRegistry: registry });
  100. }
  101. await addLog(`已关闭 ${matchedIds.length} 个匹配当前 OAuth callback 的 localhost 残留标签页。`, 'info');
  102. return matchedIds.length;
  103. }
  104. function buildLocalhostCleanupPrefix(rawUrl) {
  105. if (!isLocalhostOAuthCallbackUrl(rawUrl)) return '';
  106. const parsed = new URL(rawUrl);
  107. const segments = parsed.pathname.split('/').filter(Boolean);
  108. if (!segments.length) return parsed.origin;
  109. return `${parsed.origin}/${segments[0]}`;
  110. }
  111. async function closeTabsByUrlPrefix(prefix, options = {}) {
  112. if (!prefix) return 0;
  113. const { excludeTabIds = [], excludeUrls = [], excludeLocalhostCallbacks = false } = options;
  114. const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
  115. const excludedUrls = new Set((Array.isArray(excludeUrls) ? excludeUrls : []).filter(Boolean));
  116. const tabs = await chrome.tabs.query({});
  117. const matchedIds = tabs
  118. .filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
  119. .filter((tab) => typeof tab.url === 'string' && !excludedUrls.has(tab.url))
  120. .filter((tab) => !(excludeLocalhostCallbacks && isLocalhostOAuthCallbackUrl(tab.url)))
  121. .filter((tab) => typeof tab.url === 'string' && tab.url.startsWith(prefix))
  122. .filter((tab) => !isLocalhostOAuthCallbackUrl(tab.url))
  123. .map((tab) => tab.id);
  124. if (!matchedIds.length) return 0;
  125. await chrome.tabs.remove(matchedIds).catch(() => { });
  126. await addLog(`已关闭 ${matchedIds.length} 个匹配 ${prefix} 的 localhost 残留标签页。`, 'info');
  127. return matchedIds.length;
  128. }
  129. async function pingContentScriptOnTab(tabId) {
  130. if (!Number.isInteger(tabId)) return null;
  131. try {
  132. return await chrome.tabs.sendMessage(tabId, {
  133. type: 'PING',
  134. source: 'background',
  135. payload: {},
  136. });
  137. } catch {
  138. return null;
  139. }
  140. }
  141. async function waitForTabUrlFamily(source, tabId, referenceUrl, options = {}) {
  142. const { timeoutMs = 15000, retryDelayMs = 400 } = options;
  143. const start = Date.now();
  144. while (Date.now() - start < timeoutMs) {
  145. try {
  146. const tab = await chrome.tabs.get(tabId);
  147. if (matchesSourceUrlFamily(source, tab.url, referenceUrl)) {
  148. return tab;
  149. }
  150. } catch {
  151. return null;
  152. }
  153. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  154. }
  155. return null;
  156. }
  157. async function waitForTabUrlMatch(tabId, matcher, options = {}) {
  158. const { timeoutMs = 15000, retryDelayMs = 400 } = options;
  159. const start = Date.now();
  160. while (Date.now() - start < timeoutMs) {
  161. try {
  162. const tab = await chrome.tabs.get(tabId);
  163. if (matcher(tab.url || '', tab)) {
  164. return tab;
  165. }
  166. } catch {
  167. return null;
  168. }
  169. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  170. }
  171. return null;
  172. }
  173. async function ensureContentScriptReadyOnTab(source, tabId, options = {}) {
  174. const {
  175. inject = null,
  176. injectSource = null,
  177. timeoutMs = 30000,
  178. retryDelayMs = 700,
  179. logMessage = '',
  180. } = options;
  181. const start = Date.now();
  182. let lastError = null;
  183. let logged = false;
  184. let attempt = 0;
  185. console.log(
  186. LOG_PREFIX,
  187. `[ensureContentScriptReadyOnTab] start ${source} tab=${tabId}, timeout=${timeoutMs}ms, inject=${Array.isArray(inject) ? inject.join(',') : 'none'}`
  188. );
  189. while (Date.now() - start < timeoutMs) {
  190. attempt += 1;
  191. const pong = await pingContentScriptOnTab(tabId);
  192. if (pong?.ok && (!pong.source || pong.source === source)) {
  193. console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
  194. await registerTab(source, tabId);
  195. return;
  196. }
  197. if (!inject || !inject.length) {
  198. throw new Error(`${getSourceLabel(source)} 内容脚本未就绪,且未提供可用的注入文件。`);
  199. }
  200. const registry = await getTabRegistry();
  201. if (registry[source]) {
  202. registry[source].ready = false;
  203. await setState({ tabRegistry: registry });
  204. }
  205. try {
  206. if (injectSource) {
  207. await chrome.scripting.executeScript({
  208. target: { tabId },
  209. func: (injectedSource) => {
  210. window.__MULTIPAGE_SOURCE = injectedSource;
  211. },
  212. args: [injectSource],
  213. });
  214. }
  215. await chrome.scripting.executeScript({
  216. target: { tabId },
  217. files: inject,
  218. });
  219. } catch (err) {
  220. lastError = err;
  221. console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] inject attempt ${attempt} failed for ${source} tab=${tabId}: ${err?.message || err}`);
  222. }
  223. const pongAfterInject = await pingContentScriptOnTab(tabId);
  224. if (pongAfterInject?.ok && (!pongAfterInject.source || pongAfterInject.source === source)) {
  225. console.log(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ready after inject ${source} tab=${tabId} on attempt ${attempt} after ${Date.now() - start}ms`);
  226. await registerTab(source, tabId);
  227. return;
  228. }
  229. if (logMessage && !logged) {
  230. console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ${source} tab=${tabId} still not ready after ${Date.now() - start}ms`);
  231. await addLog(logMessage, 'warn');
  232. logged = true;
  233. }
  234. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  235. }
  236. throw lastError || new Error(`${getSourceLabel(source)} 内容脚本长时间未就绪。`);
  237. }
  238. function getContentScriptResponseTimeoutMs(message) {
  239. if (!message || typeof message !== 'object') return 30000;
  240. if (message.type === 'EXECUTE_STEP' && Number(message.step) === 6) return 75000;
  241. if (message.type === 'POLL_EMAIL') {
  242. const maxAttempts = Math.max(1, Number(message.payload?.maxAttempts) || 1);
  243. const intervalMs = Math.max(0, Number(message.payload?.intervalMs) || 0);
  244. return Math.max(45000, maxAttempts * intervalMs + 25000);
  245. }
  246. if (message.type === 'FILL_CODE') return Number(message.step) === 7 ? 45000 : 30000;
  247. if (message.type === 'PREPARE_SIGNUP_VERIFICATION') return 45000;
  248. return 30000;
  249. }
  250. function getMessageDebugLabel(source, message, tabId = null) {
  251. const parts = [source || 'unknown', message?.type || 'UNKNOWN'];
  252. if (Number.isInteger(message?.step)) parts.push(`step=${message.step}`);
  253. if (Number.isInteger(tabId)) parts.push(`tab=${tabId}`);
  254. return parts.join(' ');
  255. }
  256. function summarizeMessageResultForDebug(result) {
  257. if (result === undefined) return 'undefined';
  258. if (result === null) return 'null';
  259. if (typeof result !== 'object') return JSON.stringify(result);
  260. const summary = {};
  261. for (const key of ['ok', 'error', 'stopped', 'source', 'step']) {
  262. if (key in result) summary[key] = result[key];
  263. }
  264. if (result.payload && typeof result.payload === 'object') {
  265. summary.payloadKeys = Object.keys(result.payload);
  266. }
  267. return JSON.stringify(summary);
  268. }
  269. function sendTabMessageWithTimeout(tabId, source, message, responseTimeoutMs = getContentScriptResponseTimeoutMs(message)) {
  270. return new Promise((resolve, reject) => {
  271. let settled = false;
  272. const startedAt = Date.now();
  273. const debugLabel = getMessageDebugLabel(source, message, tabId);
  274. console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] dispatch ${debugLabel}, timeout=${responseTimeoutMs}ms`);
  275. const timer = setTimeout(() => {
  276. if (settled) return;
  277. settled = true;
  278. const seconds = Math.ceil(responseTimeoutMs / 1000);
  279. console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] timeout ${debugLabel} after ${Date.now() - startedAt}ms`);
  280. reject(new Error(`Content script on ${source} did not respond in ${seconds}s. Try refreshing the tab and retry.`));
  281. }, responseTimeoutMs);
  282. chrome.tabs.sendMessage(tabId, message)
  283. .then((value) => {
  284. const elapsed = Date.now() - startedAt;
  285. if (settled) return;
  286. settled = true;
  287. clearTimeout(timer);
  288. console.log(LOG_PREFIX, `[sendTabMessageWithTimeout] response ${debugLabel} after ${elapsed}ms: ${summarizeMessageResultForDebug(value)}`);
  289. resolve(value);
  290. })
  291. .catch((error) => {
  292. const elapsed = Date.now() - startedAt;
  293. if (settled) return;
  294. settled = true;
  295. clearTimeout(timer);
  296. console.warn(LOG_PREFIX, `[sendTabMessageWithTimeout] rejection ${debugLabel} after ${elapsed}ms: ${error?.message || error}`);
  297. reject(error);
  298. });
  299. });
  300. }
  301. function queueCommand(source, message, timeout = 15000) {
  302. return new Promise((resolve, reject) => {
  303. const timer = setTimeout(() => {
  304. pendingCommands.delete(source);
  305. reject(new Error(`Content script on ${source} did not respond in ${timeout / 1000}s. Try refreshing the tab and retry.`));
  306. }, timeout);
  307. pendingCommands.set(source, { message, resolve, reject, timer });
  308. console.log(LOG_PREFIX, `Command queued for ${source} (waiting for ready)`);
  309. });
  310. }
  311. function flushCommand(source, tabId) {
  312. const pending = pendingCommands.get(source);
  313. if (pending) {
  314. clearTimeout(pending.timer);
  315. pendingCommands.delete(source);
  316. sendTabMessageWithTimeout(tabId, source, pending.message).then(pending.resolve).catch(pending.reject);
  317. console.log(LOG_PREFIX, `Flushed queued command to ${source} (tab ${tabId})`);
  318. }
  319. }
  320. function cancelPendingCommands(reason = STOP_ERROR_MESSAGE) {
  321. for (const [source, pending] of pendingCommands.entries()) {
  322. clearTimeout(pending.timer);
  323. pending.reject(new Error(reason));
  324. pendingCommands.delete(source);
  325. console.log(LOG_PREFIX, `Cancelled queued command for ${source}`);
  326. }
  327. }
  328. async function reuseOrCreateTab(source, url, options = {}) {
  329. const alive = await isTabAlive(source);
  330. if (alive) {
  331. const tabId = await getTabId(source);
  332. await closeConflictingTabsForSource(source, url, { excludeTabIds: [tabId] });
  333. const currentTab = await chrome.tabs.get(tabId);
  334. const sameUrl = currentTab.url === url;
  335. const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
  336. const registry = await getTabRegistry();
  337. if (sameUrl) {
  338. await chrome.tabs.update(tabId, { active: true });
  339. if (shouldReloadOnReuse) {
  340. if (registry[source]) registry[source].ready = false;
  341. await setState({ tabRegistry: registry });
  342. await chrome.tabs.reload(tabId);
  343. await new Promise((resolve) => {
  344. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  345. const listener = (tid, info) => {
  346. if (tid === tabId && info.status === 'complete') {
  347. chrome.tabs.onUpdated.removeListener(listener);
  348. clearTimeout(timer);
  349. resolve();
  350. }
  351. };
  352. chrome.tabs.onUpdated.addListener(listener);
  353. });
  354. }
  355. if (options.inject) {
  356. if (registry[source]) registry[source].ready = false;
  357. await setState({ tabRegistry: registry });
  358. if (options.injectSource) {
  359. await chrome.scripting.executeScript({
  360. target: { tabId },
  361. func: (injectedSource) => {
  362. window.__MULTIPAGE_SOURCE = injectedSource;
  363. },
  364. args: [options.injectSource],
  365. });
  366. }
  367. await chrome.scripting.executeScript({
  368. target: { tabId },
  369. files: options.inject,
  370. });
  371. await new Promise((resolve) => setTimeout(resolve, 500));
  372. }
  373. await rememberSourceLastUrl(source, url);
  374. return tabId;
  375. }
  376. if (registry[source]) registry[source].ready = false;
  377. await setState({ tabRegistry: registry });
  378. await chrome.tabs.update(tabId, { url, active: true });
  379. await new Promise((resolve) => {
  380. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  381. const listener = (tid, info) => {
  382. if (tid === tabId && info.status === 'complete') {
  383. chrome.tabs.onUpdated.removeListener(listener);
  384. clearTimeout(timer);
  385. resolve();
  386. }
  387. };
  388. chrome.tabs.onUpdated.addListener(listener);
  389. });
  390. if (options.inject) {
  391. if (options.injectSource) {
  392. await chrome.scripting.executeScript({
  393. target: { tabId },
  394. func: (injectedSource) => {
  395. window.__MULTIPAGE_SOURCE = injectedSource;
  396. },
  397. args: [options.injectSource],
  398. });
  399. }
  400. await chrome.scripting.executeScript({
  401. target: { tabId },
  402. files: options.inject,
  403. });
  404. }
  405. await new Promise((resolve) => setTimeout(resolve, 500));
  406. await rememberSourceLastUrl(source, url);
  407. return tabId;
  408. }
  409. await closeConflictingTabsForSource(source, url);
  410. const tab = await chrome.tabs.create({ url, active: true });
  411. if (options.inject) {
  412. await new Promise((resolve) => {
  413. const timer = setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
  414. const listener = (tabId, info) => {
  415. if (tabId === tab.id && info.status === 'complete') {
  416. chrome.tabs.onUpdated.removeListener(listener);
  417. clearTimeout(timer);
  418. resolve();
  419. }
  420. };
  421. chrome.tabs.onUpdated.addListener(listener);
  422. });
  423. if (options.injectSource) {
  424. await chrome.scripting.executeScript({
  425. target: { tabId: tab.id },
  426. func: (injectedSource) => {
  427. window.__MULTIPAGE_SOURCE = injectedSource;
  428. },
  429. args: [options.injectSource],
  430. });
  431. }
  432. await chrome.scripting.executeScript({
  433. target: { tabId: tab.id },
  434. files: options.inject,
  435. });
  436. }
  437. await rememberSourceLastUrl(source, url);
  438. return tab.id;
  439. }
  440. async function sendToContentScript(source, message, options = {}) {
  441. throwIfStopped();
  442. const { responseTimeoutMs = getContentScriptResponseTimeoutMs(message) } = options;
  443. const registry = await getTabRegistry();
  444. const entry = registry[source];
  445. if (!entry || !entry.ready) {
  446. throwIfStopped();
  447. return queueCommand(source, message);
  448. }
  449. const alive = await isTabAlive(source);
  450. throwIfStopped();
  451. if (!alive) {
  452. return queueCommand(source, message);
  453. }
  454. throwIfStopped();
  455. return sendTabMessageWithTimeout(entry.tabId, source, message, responseTimeoutMs);
  456. }
  457. async function sendToContentScriptResilient(source, message, options = {}) {
  458. const { timeoutMs = 30000, retryDelayMs = 600, logMessage = '' } = options;
  459. const start = Date.now();
  460. let lastError = null;
  461. let logged = false;
  462. let attempt = 0;
  463. while (Date.now() - start < timeoutMs) {
  464. throwIfStopped();
  465. attempt += 1;
  466. try {
  467. return await sendToContentScript(source, message);
  468. } catch (err) {
  469. const retryable = isRetryableContentScriptTransportError(err);
  470. if (!retryable) {
  471. throw err;
  472. }
  473. lastError = err;
  474. if (logMessage && !logged) {
  475. await addLog(logMessage, 'warn');
  476. logged = true;
  477. }
  478. await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  479. }
  480. }
  481. throw lastError || new Error(`等待 ${getSourceLabel(source)} 重新就绪超时。`);
  482. }
  483. async function sendToMailContentScriptResilient(mail, message, options = {}) {
  484. const { timeoutMs = 45000, maxRecoveryAttempts = 2 } = options;
  485. const start = Date.now();
  486. let lastError = null;
  487. let recoveries = 0;
  488. let logged = false;
  489. while (Date.now() - start < timeoutMs) {
  490. throwIfStopped();
  491. try {
  492. return await sendToContentScript(mail.source, message);
  493. } catch (err) {
  494. if (!isRetryableContentScriptTransportError(err)) {
  495. throw err;
  496. }
  497. lastError = err;
  498. if (!logged) {
  499. await addLog(`步骤 ${message.step}:${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn');
  500. logged = true;
  501. }
  502. if (recoveries >= maxRecoveryAttempts) {
  503. break;
  504. }
  505. recoveries += 1;
  506. await reuseOrCreateTab(mail.source, mail.url, {
  507. inject: mail.inject,
  508. injectSource: mail.injectSource,
  509. reloadIfSameUrl: true,
  510. });
  511. await new Promise((resolve) => setTimeout(resolve, 800));
  512. }
  513. }
  514. throw lastError || new Error(`${mail.label} 页面未能重新就绪。`);
  515. }
  516. return {
  517. buildLocalhostCleanupPrefix,
  518. cancelPendingCommands,
  519. closeConflictingTabsForSource,
  520. closeLocalhostCallbackTabs,
  521. closeTabsByUrlPrefix,
  522. ensureContentScriptReadyOnTab,
  523. flushCommand,
  524. getContentScriptResponseTimeoutMs,
  525. getMessageDebugLabel,
  526. getTabId,
  527. getTabRegistry,
  528. isLocalhostOAuthCallbackTabMatch,
  529. isTabAlive,
  530. pingContentScriptOnTab,
  531. queueCommand,
  532. registerTab,
  533. rememberSourceLastUrl,
  534. reuseOrCreateTab,
  535. sendTabMessageWithTimeout,
  536. sendToContentScript,
  537. sendToContentScriptResilient,
  538. sendToMailContentScriptResilient,
  539. summarizeMessageResultForDebug,
  540. waitForTabUrlFamily,
  541. waitForTabUrlMatch,
  542. };
  543. }
  544. return {
  545. createTabRuntime,
  546. };
  547. });