Remote Tab Opener — Developer Documentation
Open and control one or more tabs from your admin page. Safe actions, local only, with user consent.
Minimal permissions • HTTPS-only • No SOP bypass • Local only • v7.10.1
SDK & Downloads
Official client to integrate your web admin page with Remote Tab Opener via window.postMessage.
Typed API, timeouts, request/response correlation, and live tabStatus events.
Download
Version: v7.10.1
dist/ & examples/playground.html)
examples/ over HTTP(S),
add your origin to the allow-list in the extension popup, and you're good to go.
tsup)
@rto/sdk: contains src/, tests/, package.json,
tsconfig.json, README, CHANGELOG, and build scripts (ESM/CJS/IIFE + d.ts).
What’s inside
RtoClient—detect,open,focus,navigate,close- DOM actions —
domClick,domType,submit,highlight,getHtml runJs— tiny snippets (keep small, deterministic)- Events —
onTabStatus() - MV2→MV3 note — SDK protocol-agnostic (message contracts unchanged)
Import & Quick Start
<!-- Serve examples/ over HTTP(S), then in playground.html -->
<script src="../dist/index.iife.js"></script>
<script>
const rto = RTO_SDK.createRtoClient({ timeoutMs: 15000 });
(async () => {
await rto.detect(); // throws if not detected by default
await rto.open({ url: "https://www.google.com/", tabKey: "google", focus: true, newTab: true, singleton: true });
await rto.domType({ tabKey: "google", selector: 'input[name="q"]', text: "Remote Tab Opener", focus: true, replace: true });
const html = await rto.getHtml({ tabKey: "google", selector: "#search .tF2Cxc", property: "outerHTML" });
console.log(html);
})();
</script>
// after npm i (or local import after unzip)
import { createRtoClient } from "@rto/sdk"; // or "./src/index" if local
const rto = createRtoClient({ timeoutMs: 15000 });
await rto.detect();
await rto.open({ url: "https://www.google.com/", tabKey: "google", focus: true, newTab: true, singleton: true });
await rto.domType({ tabKey: "google", selector: 'input[name="q"]', text: "Remote Tab Opener", focus: true, replace: true });
const html = await rto.getHtml({ tabKey: "google", selector: "#search .tF2Cxc", property: "outerHTML" });
console.log(html);
// optional: live tab status
const off = rto.onTabStatus(push => console.log("tabStatus", push));
Notes:
- Serve over HTTP(S) (avoid
file://) and add your origin to the extension allow-list. - For localhost, enable the LAN/localhost flag and keep the host in allow-list.
- Restricted schemes (e.g.
chrome:) don’t inject content scripts. - Avoid sensitive fields; prefer named DOM actions over big arbitrary
runJs.
Demo (prebuilt) → for a quick test: unzip, serve
examples/, allow-list your origin, done.Source/NPM scaffold → for teams and CI: TypeScript sources, tests (Vitest), build scripts (ESM/CJS/IIFE + d.ts).
focusaction promoted to “first-class”: brings the target tab to the front.- Security banner now on demand and minimizable to a movable pill.
getHtmlresponses are trimmed for safety and always echo yourrequestId.
What’s new in v7.10.0
- Multi-tab control — target tabs with
tabKey(friendly name) ortabId. New actions:listTabs,adoptTab,releaseTab. - LAN / Localhost (opt-in) — disabled by default. Enable flags in the popup and keep hosts in the allow-list (double barrier).
- Security banner — still present; now shows
tabKeywhen applicable. - Backward compatibility — legacy messages remain supported; prefer the canonical
ask()helper.
7.9.x brought deny-by-default, allow-list UX, and safe DOM actions. 7.10.0 builds on that with multi-tab management.
HTTPS only; restricted schemes (e.g.
about:, chrome:) never inject content scripts.
2) How it Works — the short version
- Your admin page posts messages (
window.postMessage) or calls the tinyask()helper. - The background script opens/navigates/focuses a specific tab (selected by
tabKey/tabId). - The content script runs safe DOM actions inside the target page if the host is allowed.
- You receive a structured response with
ok/dataorok:false,error:{code,...}.
Everything runs locally. SOP is respected because actions execute inside the target page with explicit host permissions.
See our full working examples.
3) Setup
- Install the extension from AMO.
- Open the popup and add your domain(s) to the Allow-list.
- (Optional) Enable LAN/localhost flags if you target intranet/dev.
- Add the messaging helper below to your admin page.
about:, chrome:, or addons:.
4) Quick Start (copy & paste)
4.1 Minimal message helper
Request/response with requestId and timeouts. Keep it once, reuse everywhere.
<script>
const ADMIN_ORIGIN='admin', EXT_ORIGIN='extension';
let _seq=0, _waiters=new Map();
addEventListener('message', (e) => {
const d=e.data; if(!d||typeof d!=='object') return;
if(d.origin!==EXT_ORIGIN) return;
if(d.requestId && _waiters.has(d.requestId)) { _waiters.get(d.requestId)(d); _waiters.delete(d.requestId); }
});
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>
Aliases: openTab ≈ open, getCurrentUrl ≈ getUrl (kept for backward compatibility).
4.2 Detect the extension
Works with modern builds; legacy ping/pong still OK.
<script>
(async () => {
const det = await ask('detect');
console.log('RTO present?', det.ok);
})();
</script>
4.3 Open two tabs and work on one
Use a friendly tabKey per tab (e.g. "auth", "monitor").
<script type="module">
const det = await ask('detect'); if (!det.ok) throw new Error('RTO not detected');
// Open 2 tabs (HTTPS only)
await ask('openTab', { url:'https://example.org/login', tabKey:'auth', focus:true });
await ask('openTab', { url:'https://status.example', tabKey:'monitor', focus:false });
// Work on "auth" (avoid sensitive fields like passwords)
await ask('domType', { selector:'#email', value:'user@example.org', tabKey:'auth' });
await ask('domClick', { selector:'form button[type=submit]', tabKey:'auth' });
// Read URL from "monitor"
const u = await ask('getCurrentUrl', { tabKey:'monitor' });
console.log('Monitor at:', u.ok ? u.data.url : '(unknown)');
</script>
Reading or writing sensitive fields (e.g., password values) is blocked by policy.
5) Targeting (choose the tab)
Goal: tell RTO which tab to act on.
tabKey— a friendly name you choose (easiest for beginners). Reuse it in every action that targets that tab.tabId— a numeric browser id, returned bylistTabs. Useful to “adopt” an existing tab.
// Create two named tabs
await ask('openTab', { url:'https://example.com', tabKey:'sales' });
await ask('openTab', { url:'https://status.io', tabKey:'monitor' });
// Navigate only "sales"
await ask('navigate', { url:'https://example.com/app', tabKey:'sales' });
// Adopt an existing tab by id, give it a key, then release
const list = await ask('listTabs'); // -> { ok, data:{ items:[{tabId, tabKey, url, title, focused, createdAt, lastSeenAt}] } }
const first = list.ok && list.data.items[0];
if (first) {
await ask('adoptTab', { tabId:first.tabId, tabKey:'ops' });
await ask('navigate', { url:'https://example.org/tools', tabKey:'ops' });
await ask('releaseTab', { tabKey:'ops' });
}
tabKey.
6) Messaging API (canonical)
Requests are posted from your admin page; replies mirror the requestId. Recommended envelope:
/* Request (Admin → Extension) */
{
origin: "admin",
type: "RTO_REQUEST",
requestId: "<uuid>",
action: "openTab" | "open" | "navigate" | "focus" | "getCurrentUrl" | "getUrl" | "listTabs" | "adoptTab" | "releaseTab" | "close" | "domClick" | "domType" | "runJs",
args: { tabKey?, tabId?, ... }
}
/* Response (Extension → Admin) */
{
origin: "extension",
type: "RTO_RESPONSE",
requestId: "<uuid>",
ok: true,
data: { ... } /* or: ok:false, error:{ code, message, details? } */
}
{type:'ping'} → {type:'pong'}. Prefer the canonical envelope for typed errors and better tooling.
7) Functions — What you can do
7.1 Open / Navigate
Open a remote tab or navigate it to a new URL. HTTPS-only (no http://).
await ask('openTab', { url:'https://example.com', tabKey:'sales' });
await ask('navigate', { url:'https://example.com/app', tabKey:'sales' });
7.2 Focus
await ask('focus', { tabKey:'sales' });
7.3 Read current URL / title
const u = await ask('getCurrentUrl', { tabKey:'sales' }); // alias: getUrl
console.log('URL:', u.data?.url);
7.4 Manage tabs
const list = await ask('listTabs'); // inspect list.data.items
await ask('adoptTab', { tabId:123, tabKey:'ops' });
await ask('releaseTab', { tabKey:'ops' });
await ask('close', { tabKey:'ops' }); // close the controlled tab
8) DOM Actions — Interact with the page
DOM actions run inside the remote page (content script context). They are safe and respect SOP.
| Action | Args | What it does |
|---|---|---|
domType | { selector, value, clear? } + tabKey? | Type into an input/textarea (optionally clear first). |
domClick | { selector } + tabKey? | Click a visible element. |
focusElement | { selector } | Move focus (tries to avoid scroll jumps). |
submit | { selector } | Submit form (requestSubmit if available). |
selectSetValue | { selector, value } | Change <select> and dispatch events. |
domSetStyle | { selector, style:{...} } | Apply whitelisted CSS (outline/background/border/box-shadow). |
highlight | { selector, color?, ms? } | Temporary halo (auto-restore). |
getHtml (read-only) | { selector, property } | Sanitized content (no secrets). |
8.1 Examples
// Focus then type a name on "sales"
await ask('focusElement', { selector:'#name', tabKey:'sales' });
await ask('domType', { selector:'#name', value:'Francesco', tabKey:'sales' });
// Change <select> and give a visual cue
await ask('selectSetValue', { selector:'#op', value:'longest_word', tabKey:'sales' });
await ask('highlight', { selector:'#op', color:'#FFCC00', ms:1200, tabKey:'sales' });
// Read back values (read-only)
const textOuter = await ask('getHtml', { selector:'#text', property:'outerHTML', tabKey:'sales' });
console.log('Outer HTML:', textOuter?.data);
If the target is cross-domain and content scripts cannot inject, DOM actions and the security banner are unavailable.
9) runJs — Run tiny code in the remote page
Use this for quick reads or small visual cues. Keep snippets tiny and self-contained. Prefer named DOM actions for anything sensitive or complex.
const title = await ask('runJs', { code:'document.title', tabKey:'sales' });
console.log('Title:', title?.data);
For security, arbitrary evaluation is limited. Don’t fetch remote scripts or rely on page-specific globals.
10) Events & Responses
Minimal logger to help during development:
addEventListener('message', (e) => {
const d = e.data; if(!d||typeof d!=='object'||d.origin!=='extension') return;
const tag = d.status==='error' ? 'warn' : 'log';
console[tag]('[RTO]', d.type||d.action||'evt', d.status||'ok', d.data||d.error||'');
});
11) Errors & Troubleshooting
| Code | When | Fix |
|---|---|---|
DOMAIN_NOT_ALLOWED | Any remote action | Add domain in popup allow-list (LAN/localhost require flags + allow-list). |
NO_CONTROLLED_TAB | Targeting/focus/action | Open or adopt a tab first (openTab/adoptTab). |
INVALID_URL | openTab/navigate | Use absolute https:// URL. |
ELEMENT_NOT_FOUND | DOM action | Fix selector; wait for SPA rendering. |
TIMEOUT | Slow pages | Increase timeouts; add retries/backoff. |
EXT_NOT_DETECTED | detect | Install extension; check origin and permissions. |
12) Recipes (beginner friendly)
12.1 Login flow (resilient)
await ask('openTab', { url:'https://app.example.com/login', tabKey:'auth', focus:true });
await ask('domType', { selector:'#email', value:'user@example.com', tabKey:'auth' });
await ask('domClick', { selector:'button[type=submit]', tabKey:'auth' });
await ask('focus', { tabKey:'auth' });
12.2 Open by playlist
for (const path of ['/a','/b','/c']) {
await ask('navigate', { url:'https://example.com'+path, tabKey:'sales' });
}
12.3 Round-robin across 2 tabs
for (const key of ['sales','monitor']) {
await ask('focus', { tabKey:key });
await ask('highlight', { selector:'body', ms:500, tabKey:key });
}
13) FAQ
Does this bypass Same-Origin Policy? No. The extension runs code inside the target page on domains you allow.
How do I control 2 tabs? Give each tab a tabKey and pass it to each action; or use listTabs + adoptTab/releaseTab.
LAN / Localhost? Enable the flags in the popup and keep hosts in the allow-list (both required).
Can I read the remote HTML? Keep to small reads (title/counters). Full scraping is out of scope.
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?