npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

conduito

v0.2.0

Published

Conduito — a cooperative frontend API protocol over postMessage. The flagship: @conduito/core (the protocol) + @conduito/frame (iframe), with @conduito/port (Web Worker / MessagePort) at conduito/port.

Readme

conduito

Um protocolo de cooperação para frontends sobre postMessage: um contrato tipado, baseado em capabilities, entre um host (um Editor) e um app embutido — pra introspectar, gerir e dirigir os recursos do app de forma cooperativa, não hackeada.

A tese, em uma frase: assim como o backend expõe uma API, o frontend também pode — só que declarada pelo próprio app, descoberta pelo editor, e segura por contrato.

Diferente de Playwright/CDP, que dirigem de fora e adivinham a semântica do DOM, o conduito coopera de dentro: o app declara o que expõe, e então o editor — e uma IA — sabem o que cada recurso significa.

Este é o flagship — um único install com o protocolo e os dois carriers:

  • @conduito/core — o protocolo (envelope, Endpoint RPC bidirecional + pub/sub, cancelamento, capabilities + discover, cliente tipado). Transport-agnóstico, zero-dep.
  • @conduito/frame — o transporte iframe/postMessage (connect/attach/listen, handshake, reconexão, o gate de segurança).
  • @conduito/port — o transporte MessagePort (Web/Shared Worker, MessageChannel, worker_threads), em conduito/port.
import { defineCapability, connect } from "conduito"; // protocolo + iframe
import { connect as connectWorker } from "conduito/port"; // o mesmo protocolo, outro carrier
pnpm add conduito

A capability (o contrato dos dois lados)

Uma capability é um domínio que o app expõe: métodos chamáveis + eventos. Declare uma vez, num módulo que os dois lados enxergam:

// app/capabilities.ts — mora no APP (os handlers são dele); o host importa só o tipo
import { defineCapability } from "conduito";

export const routesCap = defineCapability({
  name: "routes",
  version: "1.0.0",
  methods: {
    list: (): Route[] => router.getRoutes(),
    add: (route: Route) => router.add(route),
  },
  events: ["changed"],
  meta: { title: "Rotas da aplicação" },
});

App (ser dirigível — estilo Express)

import { listen } from "conduito";
import { routesCap } from "./capabilities";

listen({
  allowedOrigins: ["https://editor.appx.com"], // a fronteira: só estes hosts dirigem
  onConnect(app, hostOrigin) {
    app.expose(routesCap); // registra no manifest E fia os handlers
    app.emit("routes", "changed", router.getRoutes());
  },
});

expose faz as duas coisas de uma vez: entra no manifest (pra ser descoberto) e conecta cada método como handler. Um unexpose devolvido desfaz as duas.

Host (dirigir o app)

import { connect, createClient } from "conduito";
import type { routesCap } from "./capabilities"; // só o TIPO — ver o aviso abaixo

const { endpoint } = await connect({
  url: "https://appx.com/embed",
  container: document.getElementById("stage")!,
  onConnect: async (endpoint) => {
    // roda na primeira conexão E a cada reload do app — refaça a sessão aqui
    const { capabilities } = await endpoint.discover(); // o "db pull"
    renderSidebar(capabilities);
  },
});

// sem tipos: a chamada crua, quando você não tem (ou não quer) a declaração
const raw = await endpoint.request("routes", "list"); // unknown

// com tipos: payload, retorno e nomes de evento saem da declaração do app
const routes = createClient<typeof routesCap>(endpoint, "routes");
const list = await routes.request.list(); // Route[]
routes.notify.add({ path: "/novo" }); // fire-and-forget, assinatura checada
const off = routes.on.changed((r) => refresh(r)); // só os eventos declarados existem

[!WARNING] Importe a declaração no host com import type. Um import normal funciona e não levanta erro nenhum — mas arrasta os handlers junto, e com eles o router, o store e o que mais o app usar, pro bundle do editor. O contrato é o tipo; o código fica no app.

O discover é o retrato de um instante, e o app pode abrir ou fechar domínios enquanto roda. Pra um editor que renderiza UI a partir do manifest, assine o evento que o próprio protocolo publica a cada expose/unexpose:

import { META_NS } from "conduito";

endpoint.on(META_NS, "capabilities", ({ capabilities }) =>
  renderSidebar(capabilities),
);

O app recarregou — e a conexão continua

O gesto central de um editor é "recarrega o canvas sem perder o editor". O transporte sabe disso: o Endpoint é um só pra vida inteira da conexão, e o que o host registrou (handle, on, expose) sobrevive ao reload do app.

  • onConnect dispara de novo a cada reconexão — é ali que o host refaz a sessão do lado do app (discover, push de tema, pedir o snapshot).
  • Um request já enviado quando o app caiu rejeita na hora com EDISCONNECTED, não com um ETIMEOUT trinta segundos depois. Ele pode ter executado uma mutação; o transporte não repete.
  • Um request feito enquanto o app está fora espera na fila e sai na reconexão, em ordem — o timeout dele continua valendo como limite da espera.

Quando o iframe é seu (você renderiza, o preview fica visível mesmo se o app não cooperar), troque connect por attach(frameEl, opts): mesma coisa, sem criar nem remover o elemento.

import { attach } from "conduito";

const conn = attach(document.querySelector("iframe")!, {
  onConnect: (endpoint) => endpoint.notify("theme", "apply", currentTheme),
  onDisconnect: (reason) => setStatus(reason), // "reload" | "dispose"
});
conn.connected; // o app está conectado agora?

O outro carrier: worker

O contrato não muda quando o carrier muda. A mesma capability, o mesmo Endpoint e o mesmo cliente tipado atravessam uma thread em vez de uma origem — só o jeito de abrir a conexão é outro:

// main thread
import { connect } from "conduito/port";

const { endpoint } = connect(
  new Worker(new URL("./app.js", import.meta.url), { type: "module" }),
  {
    onConnect: (endpoint) => endpoint.discover(),
  },
);
const routes = await endpoint.request("routes", "list"); // idêntico ao iframe
// dentro do worker
import { listen } from "conduito/port";
import { routesCap } from "./capabilities";

listen({ onConnect: (app) => app.expose(routesCap) }); // a MESMA declaração

Serve Web Worker, SharedWorker, MessageChannel e o worker_threads do Node — o pacote não importa nada de runtime, então o mesmo código roda nos dois mundos. Worker não recarrega, morre: rebind(novaPorta) aponta a conexão pro substituto mantendo o mesmo Endpoint, e transfer(payload) move um buffer em vez de copiá-lo.

connect/listen existem nos dois transportes, com assinaturas e garantias diferentes — é por isso que o worker mora num subpath em vez de entrar no mesmo barril. A diferença que mais importa está na seção seguinte: o iframe tem origem pra checar, o worker não.

Erros e cancelamento

Todo erro que o conduito emite é um ConduitoError com .code estável — nunca uma string pra casar por regex:

import { isConduitoError } from "conduito";

try {
  await endpoint.request("routes", "list");
} catch (e) {
  if (isConduitoError(e) && e.code === "ENOHANDLER") offerInstall();
}

ENOHANDLER · EHANDLER · ETIMEOUT · EABORTED · EDISCONNECTED · EDISPOSED · ESEND · EREMOTE · ERESERVED · EQUEUEFULL · EHANDSHAKE · EURL · EPORT · ETRANSFER. Um handler pode lançar um ConduitoError com code próprio, e ele cruza o wire.

Trabalho longo é cancelável dos dois lados: o host passa um AbortSignal, e o handler do app recebe o signal correspondente — que também dispara se o request expirar ou o par sumir.

// host — o usuário mudou de ideia: o app para de trabalhar, o await rejeita EABORTED
const ctrl = new AbortController();
const building = endpoint.request("reports", "build", query, {
  signal: ctrl.signal,
});
cancelButton.onclick = () => ctrl.abort();

// app — o signal dispara pelo abort do host, pelo timeout do request, ou se o par sumir
app.handle("reports", "build", async (query, { signal }) =>
  build(query, { signal }),
);

Segurança

Toda mensagem que chega é respondida com base em três perguntas:

  1. Quem é o par? — no iframe, allowedOrigins é obrigatório no listen: o app nunca responde a um host arbitrário (o furo clássico dos wrappers de postMessage, onde o child responde a qualquer parent). Use "*" só em dev, e combine com frame-ancestors (CSP) no app, que barra o próprio embed. Num worker essa pergunta é outra: não há origem pra checar, e a confiança vem de quem instanciou o carrier — o conduito/port não finge um gate que não existe.
  2. O que está exposto? — só o que o app registra com expose existe pro host. Escopo read-vs-mutate e consent por escopo são a próxima camada (roadmap).
  3. Tem sessão? — fora do transporte: o app decide, por handler, o que exige credencial.

Onde ler o resto

Este README é o caminho feliz. A API completa e a filosofia de cada metade estão nos READMEs de @conduito/core (protocolo, Endpoint, capabilities, cliente tipado, como escrever um Transport) e @conduito/frame (handshake, ciclo de vida do iframe, allowlist de origem).

Precisa de só uma parte? Instale o pacote direto: @conduito/core é o protocolo puro (escreva o seu Transport pra qualquer carrier), @conduito/frame é o transporte iframe e @conduito/port o de worker.