Plugin / Docs

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

✅ Demo (prebuilt) — ZIP (includes dist/ & examples/playground.html)
Use this if you want to test immediately: no build step, just serve examples/ over HTTP(S), add your origin to the allow-list in the extension popup, and you're good to go.
🧩 Source / NPM scaffold — ZIP (TypeScript + tests + build via tsup)
Use this if you plan to bundle or publish @rto/sdk: contains src/, tests/, package.json, tsconfig.json, README, CHANGELOG, and build scripts (ESM/CJS/IIFE + d.ts).

What’s inside

  • RtoClientdetect, 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
A) Demo (prebuilt)
<!-- 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>
B) Source / bundler
// 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.
Which file should I use?
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).
Also new in v7.10.1 (quick note for beginners):
  • focus action promoted to “first-class”: brings the target tab to the front.
  • Security banner now on demand and minimizable to a movable pill.
  • getHtml responses are trimmed for safety and always echo your requestId.

See our full working examples.

What’s new in v7.10.0

  • Multi-tab control — target tabs with tabKey (friendly name) or tabId. 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 tabKey when 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.

LAN / Localhost: intranet and localhost are off by default. To allow them, tick the flags in the popup and keep the host in the allow-list (both are required).
HTTPS only; restricted schemes (e.g. about:, chrome:) never inject content scripts.

Read the LAN / Localhost page

Need help writing safe flows?
Try the ChatGPT helper: Remote Tab Opener Copilot.

2) How it Works — the short version

  1. Your admin page posts messages (window.postMessage) or calls the tiny ask() helper.
  2. The background script opens/navigates/focuses a specific tab (selected by tabKey/tabId).
  3. The content script runs safe DOM actions inside the target page if the host is allowed.
  4. You receive a structured response with ok/data or ok: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

  1. Install the extension from AMO.
  2. Open the popup and add your domain(s) to the Allow-list.
  3. (Optional) Enable LAN/localhost flags if you target intranet/dev.
  4. Add the messaging helper below to your admin page.
Important: content scripts don’t run on restricted URLs such as 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: openTabopen, getCurrentUrlgetUrl (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 by listTabs. 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' });
}
Rule of thumb: if you don’t pass any target, RTO uses the default controlled tab (compatibility mode). For multi-tab apps, always pass a 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? } */
}
Legacy compatibility: older builds accept short messages like {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.

ActionArgsWhat 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

CodeWhenFix
DOMAIN_NOT_ALLOWEDAny remote actionAdd domain in popup allow-list (LAN/localhost require flags + allow-list).
NO_CONTROLLED_TABTargeting/focus/actionOpen or adopt a tab first (openTab/adoptTab).
INVALID_URLopenTab/navigateUse absolute https:// URL.
ELEMENT_NOT_FOUNDDOM actionFix selector; wait for SPA rendering.
TIMEOUTSlow pagesIncrease timeouts; add retries/backoff.
EXT_NOT_DETECTEDdetectInstall 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.

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.