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
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.
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>
- Install/enable the extension.
- Reload this page (same tab).
- Make sure this page is not a restricted URL.
- Legacy builds may answer
pongwithout 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
navigatewithfocus: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>
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
- Is the extension installed and enabled?
- Reload the page after installation.
- Is this page a restricted URL (e.g.,
about:)?
- 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).
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 / Localhost | How 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. |
Need help writing safe flows?