Plugin / Docs / Detect the Extension

Detect the Extension — Step by Step

We’ll verify three things: the extension is installed, the message channel works, and your domain is allow-listed.

Designed for beginners • copy-paste snippets • clear error messages

v7.10.1
Before you start: Install the extension from AMO and open this page. In the extension popup, add your admin domain to the Allow-list.
Note: content scripts do not run on restricted pages like about:, chrome:, or addons:.

1) Add this tiny request helper

Canonical envelope (RTO_REQUEST/RTO_RESPONSE) with requestId + timeout. Keep once, reuse everywhere.

<script>
// Who talks to whom
const ADMIN_ORIGIN = 'admin';
const EXT_ORIGIN   = 'extension';

// Pair each request with a unique id and a waiter
let _seq = 0;
const _waiters = new Map();

// Listen for replies coming back from the extension
addEventListener('message', (e) => {
  const d = e.data; 
  if (!d || typeof d !== 'object') return;
  if (d.origin !== EXT_ORIGIN) return;       // ignore anything not from the extension
  if (d.requestId && _waiters.has(d.requestId)) {
    _waiters.get(d.requestId)(d);             // resolve the waiting Promise
    _waiters.delete(d.requestId);
  }
});

// Send a request and wait for the matching reply (or timeout)
function ask(action, args = {}, timeoutMs = 12000){
  const requestId = 'r' + (++_seq);
  postMessage({ origin: ADMIN_ORIGIN, type:'RTO_REQUEST', requestId, action, args }, '*');
  return new Promise((resolve, reject) => {
    const t = setTimeout(() => { 
      _waiters.delete(requestId); 
      reject(Object.assign(new Error('timeout'), { code:'TIMEOUT' })); 
    }, timeoutMs);
    _waiters.set(requestId, (resp) => { clearTimeout(t); resolve(resp); });
  });
}
</script>

Already have a helper? Ensure it sets type:"RTO_REQUEST", tags origin:"admin", and pairs a requestId.

2) Detect the extension (and read its version)

We try both the modern and the legacy handshake. You’ll see a clear dot + message.

Checking…
Copy-paste detector
<script type="module">
// Tiny UI helpers (status dot + text)
const dotEl = document.getElementById('rto-dot');
const txtEl = document.getElementById('rto-text');
const ui = {
  pending(){ dotEl.style.background = '#999'; txtEl.textContent = 'Checking…'; },
  ok(v){ dotEl.style.background = '#21a65b'; txtEl.textContent = v ? `Extension detected ✅ v${v}` : 'Extension detected ✅'; },
  err(m){ dotEl.style.background = '#c12f2f'; txtEl.textContent = m || 'Not detected ❌'; }
};

// Generate a simple unique id
function makeId(){
  const b = new Uint8Array(16); (crypto||window.crypto||{}).getRandomValues?.(b);
  return [...b].map((x,i)=>(i===6?(x&0x0f)|0x40:i===8?(x&0x3f)|0x80:x).toString(16).padStart(2,'0')).join('');
}

// Try modern + legacy detection, accept whichever answers first
function detectVersion(timeoutMs = 6000){
  const requestId = makeId();
  return new Promise((resolve) => {
    let detected = false, version = null;
    const t = setTimeout(() => { cleanup(); resolve({ ok: detected, version }); }, timeoutMs);

    function cleanup(){ clearTimeout(t); removeEventListener('message', onMsg); }
    function onMsg(ev){
      const msg = ev?.data || {};
      if (msg.origin !== 'extension') return;

      // Legacy: { type:'pong', version? }
      if (msg.type === 'pong' && typeof msg.version === 'string') {
        cleanup(); return resolve({ ok:true, version: msg.version });
      }

      // Modern: { type:'RTO_RESPONSE', requestId, ok, data:{version?} }
      if (msg.type === 'RTO_RESPONSE' && msg.requestId === requestId) {
        const v = (msg.ok && msg.data && typeof msg.data.version === 'string') ? msg.data.version : null;
        cleanup(); return resolve({ ok:true, version: v });
      }

      // Any valid message from the extension proves presence
      detected = true;
    }

    addEventListener('message', onMsg);
    // Send both probes
    postMessage({ origin:'admin', type:'RTO_REQUEST', requestId, action:'detect' }, '*'); // modern
    postMessage({ origin:'admin', type:'ping' }, '*');                                     // legacy
  });
}

(async function main(){
  ui.pending();
  try {
    const res = await detectVersion(6000);
    if (!res.ok) return ui.err('Not detected ❌');
    ui.ok(res.version || null);
  } catch {
    ui.err('Not detected ❌');
  }
})();
</script>
If it says “Not detected”
  • Install/enable the extension.
  • Reload this page (same tab).
  • Make sure this page is not a restricted URL.
If version is empty
  • Legacy builds may answer pong without a version.
  • That’s OK; presence is confirmed.

3) Quick feature probes (safe even without a remote tab)

These confirm the two-way channel works. They won’t break anything.

3.1 Read current title (from the controlled tab, if any)

You should see something in your browser console (F12).

<script>
ask('getTitle', {}, 4000)
  .then(r => {
    if (r.ok) console.log('[RTO] title =>', r.data?.title || '(unknown)');
    else console.warn('[RTO] getTitle error =>', r.error?.code, r.error?.message);
  })
  .catch(err => console.warn('Probe failed', err));
</script>
3.2 Read current URL (may be empty now — that’s fine)

We’re only checking that messages flow both ways.

<script>
ask('getUrl', {}, 4000)
  .then(r => {
    if (r.ok) console.log('[RTO] remote URL =>', r.data?.url || '(none yet)');
    else console.warn('[RTO] getUrl error =>', r.error?.code, r.error?.message);
  })
  .catch(err => console.warn('Probe failed', err));
</script>

4) Be nice when a domain isn’t allowed (deny-by-default)

If the target domain isn’t in the user’s allow-list, show a friendly prompt and let them retry.

Strategy
  • Try a harmless navigate with focus:false.
  • On DOMAIN_NOT_ALLOWED, reveal a banner naming the host.
  • Add a “Try again” button (it can simply reload).
Copy-paste banner
<script>
async function openTarget(url){
  try{
    const r = await ask('navigate', { url, focus:false }, 8000);
    if (r.ok) return true;
    throw r.error || new Error('navigate failed');
  }catch(e){
    const msg = String(e?.message || '');
    if (e.code === 'DOMAIN_NOT_ALLOWED' || msg.includes('DOMAIN_NOT_ALLOWED')){
      const host = new URL(url).host;
      const box = document.getElementById('allow-box');
      if (box){
        box.classList.remove('d-none');
        box.querySelector('.domain').textContent = host;
      }
    }
    return false;
  }
}
</script>
Allow-list required: open the extension popup and add this-domain. Then click .

5) Progressive UI (works without the extension, better with it)

Hide “remote-only” buttons by default. Reveal them when detection succeeds.

Toggle a class on <html>
<script>
(async function(){
  // Quick boolean: do we get a 'detect' reply within 2.5s?
  const has = await (async()=>{
    try {
      const r = await ask('detect', {}, 2500);
      return !!r?.ok;
    } catch {
      return false;
    }
  })();
  document.documentElement.classList.toggle('rto-on', has);
})();
</script>

/* CSS idea:
:root .rto-only { display:none; }
:root.rto-on .rto-only { display:initial; }
*/

Tip: mark your remote actions with .rto-only so they appear only when usable.

Troubleshooting — quick checklist

No detection?
  • Is the extension installed and enabled?
  • Reload the page after installation.
  • Is this page a restricted URL (e.g., about:)?
Domain not allowed?
  • Add the exact host in the popup allow-list.
  • Retry (or refresh).

Accessibility tip

  • Use an ARIA live region for status text (e.g., “Extension detected”).
  • Keep everything keyboard reachable (Tab → Button → Enter).
Need help writing safe flows?
Try the ChatGPT helper: Remote Tab Opener Copilot.

Next Pages

Page Description
Start Overview, how it works, setup, quick start, and links to all sections.
Detect the Extension Detection contract (ping/pong), install prompts, and graceful fallbacks when the extension isn’t present.
Allow-list & Permissions Deny-by-default model, domain allow-list via popup, and suggested UX to guide users to enable hosts.
LAN / LocalhostHow to safely enable intranet/localhost (flags + allow-list).
Remote Tab Control Open/navigate/focus the controlled tab, read URL/title, select an existing tab, lifecycle & restore patterns.
DOM & Automation Action catalog (type, setValue, click, select, submit, waitFor) and safe interaction tips.
Recipes & Flows Copy-paste flows: resilient login, dashboard checks, table waits, filters, playlists, and allow-list banners.
Events Event stream reference, subscription helpers, live logger widget, and basic metrics/durations collection.
Diagnostics Inline console, error cookbook, QA self-check, and performance tips for troubleshooting flows quickly.
Full API Reference Canonical message envelopes, request/response schemas, error codes, and return shapes for every action.
Compatibility Supported browsers/versions, permissions nuances, CSP/iframe notes, and known limitations or workarounds.
Advanced Patterns Idempotent flows, retries/backoff, state restore after reload, playlist strategies, and robust selectors.
Favorites Star and group URLs, quick actions (open/focus/navigate), export/import sets, and playlist runs from favorites.
Real-World Flows (E2E) Run end-to-end flows from your admin page: helper setup, modern message contract, copy-paste journeys (login, multi-tab, extract), lightweight assertions, best practices, and troubleshooting.