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/frame

v0.1.1

Published

The iframe / postMessage transport for Conduito — parent/child handshake, origin-locking, and the origin-allowlist security gate.

Readme

@conduito/frame

O transporte iframe / postMessage do Conduito. Conecta um host (o Editor) a um app embutido num iframe cross-origin, faz o handshake, tranca a origem, reconecta a cada reload do app, e devolve um Endpoint pronto do @conduito/core — com o gate de segurança que os wrappers de postMessage clássicos não têm.

Filosofia: este é um carrier — o da topologia "app embutido ⇄ host", que é a era dos editores online e dos agentes de IA. O contrato (RPC, capabilities, eventos) mora no @conduito/core; aqui só vive o postMessage, o handshake, o ciclo de vida do iframe, e a fronteira de confiança.

Instalação

pnpm add @conduito/frame   # traz @conduito/core junto

Uso

Host (o Editor cria e dirige o app)

import { connect } from "@conduito/frame";

const { endpoint, frame, origin, dispose } = await connect({
  url: "https://appx.com/embed",
  container: document.getElementById("stage")!,
  onConnect: async (endpoint) => {
    // a cada conexão — a primeira E depois de cada reload do app: refaça a sessão aqui
    const { capabilities } = await endpoint.discover();
    endpoint.notify("theme", "apply", currentTheme);
  },
});

// agora é só @conduito/core:
const routes = await endpoint.request("routes", "list");
endpoint.on("appx", "saved", (e) => toast(e));
// dispose() → fecha o endpoint + remove o iframe

connect cria o iframe, faz o handshake (com retry) até o app responder, tranca a origem do app, e resolve com a conexão. Rejeita com um ConduitoError coded — EHANDSHAKE (sem resposta) ou EURL (url inválida); nunca lança síncrono. Depois de resolvida, a conexão continua viva através dos reloads do app (ver "Ciclo de vida").

Host (o iframe já é seu — attach)

Quando você renderiza o iframe (o preview fica visível mesmo se o app não cooperar), attach dirige o elemento existente — nunca o cria nem remove:

import { attach } from "@conduito/frame";

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

attach nunca desiste: pinga até o app aparecer, e pinga de novo a cada load. A origem é resolvida do src do iframe (ou passe origin — obrigatório se o src ainda não foi setado; sem os dois, lança EURL).

App (ser dirigível — instala e abre a API)

import { listen } from "@conduito/frame";

listen({
  // A FRONTEIRA: só estes hosts podem dirigir este app. Use "*" só em dev.
  allowedOrigins: ["https://editor.appx.com"],
  onConnect(endpoint, hostOrigin) {
    endpoint.handle("routes", "list", () => router.getRoutes());
    endpoint.handle("routes", "add", (r: Route) => router.add(r));
    endpoint.emit("appx", "saved", { at: Date.now() });
  },
});

listen espera um handshake de uma origem permitida, responde, tranca, e entrega o Endpoint.

Ciclo de vida

O app embutido recarrega (F5 no canvas, location.reload(), o host manda recarregar depois de um erro). É o gesto central de um editor, e o transporte sabe disso:

  • O Endpoint é um só, pra vida inteira da conexão. O que o host registrou (handle, on, expose) é estado do host e sobrevive. O que morre com o realm é o estado do app — por isso onConnect dispara de novo a cada conexão: é ali que o host refaz a sessão (discover, push de tema, snapshot).
  • Request já enviado quando o app caiu rejeita na hora com EDISCONNECTED — não trinta segundos depois com ETIMEOUT. Ele pode ter executado (uma mutação); o transporte não repete.
  • Request ainda não enviado (feito enquanto o app está fora) espera na fila e sai na próxima conexão, em ordem — sem whenConnected no consumidor. O timeout do próprio request limita a espera; se o app não voltar a tempo, ETIMEOUT. A fila tem teto (queueLimit, default 100); além dele o send falha com EQUEUEFULL.
  • onDisconnect("reload") avisa a queda; connected diz o estado atual.

API

| export | o quê | | ------------------------------------ | --------------------------------------------------------------------------------------- | | attach(frame, opts?): Attachment | host: dirige um iframe existente + reconecta a cada load (nunca cria/remove o iframe) | | connect(opts): Promise<Connection> | host: cria iframe + attach + resolve na primeira conexão; dispose remove o iframe | | listen(opts): Listener | app: aceita um host permitido + retorna { dispose } | | resolveOrigin(url): string | a origem de uma URL | | isAllowed(origin, policy): boolean | a checagem da allowlist | | type OriginPolicy | readonly string[] | (origin) => boolean | "*" | | type DisconnectReason | "reload" | "dispose" |

AttachOptions: { origin?, handshakeInterval?, queueLimit?, endpoint?, onConnect?, onDisconnect? } — endpoint são as EndpointOptions do core (defaultTimeout, onError). ConnectOptions: AttachOptions & { url, container?, name?, className?, maxAttempts? }. ListenOptions: { allowedOrigins, onConnect, endpoint? }. Attachment / Connection: { endpoint, frame, origin, connected, dispose }.

maxAttempts (default 5, a cada handshakeInterval de 500ms) vale só pra primeira conexão do connect, e conta a partir do load do iframe — antes disso o app não tem como responder. Um app que instala o listen sob demanda (um módulo de DEV carregado depois, num site de produção) pode passar dos 2,5s e ver o iframe removido com EHANDSHAKE: suba maxAttempts (Infinity nunca desiste). Reconexões não têm teto. Com attach a pergunta não existe.

Segurança

O listen exige allowedOrigins — o app nunca responde a um host arbitrário. É o conserto do furo clássico (o child do postmate responde a qualquer parent → um evil.com embutiria seu app logado e o dirigiria). Depois de trancar, o app segue um host só: o mesmo host (mesma origem, mesma janela) pode fazer handshake de novo e recebe HELLO de novo — idempotente, o endpoint é o mesmo; qualquer outra origem continua ignorada. Combine com frame-ancestors (CSP) no app (quem pode embutir) e consent por escopo. As três perguntas — origem, escopo, sessão — em docs/ARCHITECTURE.md.

Internals: docs/ARCHITECTURE.md (no repositório).