Executando verificação de segurança...
1

Como abri meu launcher para plugins de terceiros (e por que foi mais difícil do que parecia)

No post anterior eu expliquei como construí a Bely — um launcher para Windows com 70+ ferramentas usando Tauri 2 e React. Agora quero contar sobre uma decisão que tomei essa semana: abrir o app para plugins externos.

Por que plugins

A Bely já tem muita coisa built-in. Mas eu sei que em algum momento vou precisar que outras pessoas consigam estender o app sem depender de mim pra cada feature nova. Integração com API interna de empresa, lookup de pacote npm, ferramentas de nicho — são coisas que fazem sentido pra alguém mas não pra todo mundo.

Ao invés de esperar essa demanda aparecer, decidi construir a infraestrutura agora enquanto o projeto ainda é pequeno e eu consigo iterar rápido. Se der errado, pelo menos aprendi. Se der certo, o app já está pronto pra escalar.

O problema do iframe sandbox

A primeira decisão foi: como isolar o código de terceiros? Eu não posso simplesmente executar JS arbitrário no contexto do meu app — isso daria acesso ao filesystem, clipboard, e todas as APIs do Tauri.

A solução foi usar iframes com sandbox="allow-scripts". O plugin roda num documento HTML separado, carregado via blob URL, sem acesso ao DOM pai. Toda comunicação passa por postMessage.

Parece simples, mas tem uma cascata de problemas:

1. O iframe não herda o tema. O plugin renderiza num contexto CSS completamente isolado. Tive que coletar todas as CSS variables do tema glass (--glass-bg, --glass-text, --accent, etc — são mais de 40) e injetar como inline style no HTML do iframe. Se o usuário troca o tema, preciso mandar uma mensagem pro iframe atualizar.

2. O iframe rouba o foco da janela. Esse foi o bug mais irritante. No Windows, quando o acrylic blur está ativo e o usuário clica dentro do iframe (num input de busca do plugin, por exemplo), o window do documento pai dispara um evento blur. O app interpretava isso como "janela perdeu foco" e trocava o background de translúcido para opaco — matando o efeito glass.

A correção foi trocar o listener de window.blur/focus do DOM pela API nativa do Tauri (getCurrentWindow().onFocusChanged()), que detecta foco no nível do sistema operacional. O iframe está na mesma janela do OS, então o foco continua "ativo" mesmo quando o usuário interage com o plugin.

3. CORS mata tudo. Os plugins precisam fazer fetch pra APIs externas (npm registry, GitHub, etc). Mas o fetch roda no contexto do host app que está em http://tauri.localhost. A maioria das APIs bloqueia por CORS.

A solução foi criar um proxy no Rust. O plugin manda uma mensagem { type: "api-call", method: "fetch", args: [url, options] }, o host repassa pro backend Rust que faz o request via reqwest (sem restrição de CORS) e devolve o resultado.

O SDK

Não dá pra esperar que alguém escreva HTML cru com postMessage. Criei um SDK (@usebely/sdk) que abstrai tudo:

import { mount, List, useFetch } from "@usebely/sdk";

function App() {
  const [query, setQuery] = useState("");
  const { data, loading } = useFetch(
    query.length >= 2
      ? `https://registry.npmjs.org/-/v1/search?text=${query}`
      : null
  );

  return (
    <List searchBarPlaceholder="Buscar pacotes..." onSearchChange={setQuery} isLoading={loading}>
      {data?.objects?.map((obj) => (
        <List.Item
          key={obj.package.name}
          title={obj.package.name}
          subtitle={obj.package.description}
        />
      ))}
    </List>
  );
}

mount(<App />);

O SDK fornece componentes que já seguem o design system glass da Bely: List, Detail, Form, Grid, Action, ActionPanel. Todos usam as CSS variables injetadas, então o plugin automaticamente respeita o tema do usuário.

O useFetch tem debounce de 300ms e roteia pelo proxy Rust. O mount() cuida de montar o React, receber mensagens do host, e notificar quando o plugin está pronto.

Permissões

Cada plugin declara no manifest quais APIs precisa:

{
  "permissions": ["fetch", "clipboard.write"]
}

Se o plugin tenta chamar algo que não declarou, o host bloqueia. As permissões disponíveis são: fetch, clipboard.read, clipboard.write, fs.read, fs.write, system.info. O usuário vê as permissões antes de instalar.

A Plugin Store

O fluxo:

  1. Dev builda o plugin e abre Plugin Store > Enviar na Bely
  2. Preenche metadados (nome, ícone, categoria) e seleciona a pasta do plugin
  3. O app cria um .tar.gz com o código completo (incluindo source pra review) e faz upload pro S3
  4. Plugin fica pendente até ser aprovado no painel admin
  5. Quando aprovado, aparece na store pra instalar

Versionamento: versão aprovada não pode ser editada — precisa submeter uma nova. Versões pendentes podem ser editadas antes da aprovação. Bundle tem limite de 5MB.

No app, quando um plugin instalado tem versão nova na store, o card mostra "Atualizar" em vez de "Instalado". O detail view mostra o changelog de cada versão.

O que aprendi

Isolamento é caro. O iframe sandbox resolve segurança mas cria uma barreira de comunicação. Cada feature que funciona "de graça" no app principal (tema, foco, fetch, clipboard) precisa de uma ponte explícita pro plugin.

O SDK é o produto. Se for difícil criar um plugin, ninguém vai criar. Investir tempo nos componentes e na DX valeu mais do que qualquer feature da store.

Review manual é o certo por enquanto. Hoje eu baixo o bundle, leio o source, testo. Não escala, mas com zero plugins de terceiros no momento, é melhor garantir qualidade do que automatizar algo que ninguém está usando.

Glass theme em iframes é sofrimento. A quantidade de edge cases entre transparência, composição de rgba, e comportamento diferente entre Windows 10 e 11 é absurda. Se eu fosse começar de novo, talvez considerasse web components no mesmo documento ao invés de iframes.

Status

O sistema de plugins está na v1.26.0. O SDK é funcional e já tem um plugin de exemplo (NPM Lookup) na store. A documentação está em https://bely.my/docs/plugins

A Bely está em whitelist beta: https://bely.my

Feedback técnico é bem-vindo — especialmente de quem já lidou com plugin systems ou iframe sandboxing.

Carregando publicação patrocinada...