Pular para o conteúdo
miblo

SDK

Publicar um app na loja do Miblo, passo a passo

Como publicar um app no miblo.ai/apps: o que vai no envio, o pacote de código, a análise automática, a revisão por IA, a aprovação humana, revisões e o checklist.

Visão geral do processo

Publicar é deixar o seu app na loja, miblo.ai/apps, para qualquer pessoa ligar no Miblo dela com um clique. O caminho:

  1. Você testa no seu computador com o SDK e o supervisor.

  2. Envia em miblo.ai/apps/enviar: a ficha do app, as telas e, se tiver, o pacote de código.

  3. A análise automática confere o pacote na hora e recusa o que não pode passar, dizendo o motivo.

  4. A revisão por IA escreve um relatório de segurança do código (minutos depois).

  5. Uma pessoa aprova. Nenhum app entra na loja sem aprovação humana.

  6. O miblo.ai assina o pacote com a chave dos apps, e quem instala confere a assinatura antes de qualquer coisa rodar.

Os dois tipos de app

TipoO que levaO que roda no computador de quem instala
Pacote de telaUm card, até 4 frames e as configuraçõesNada: o Miblo aplica a tela como um programa faria
App com códigoTudo do pacote de tela mais um pacote de código (zip)O seu main.js, dentro da sandbox

Um pacote de tela serve para uma tela fixa ou que você atualiza por um programa seu. Um app com código busca dados e redesenha sozinho, como os apps oficiais.

O que vai no envio

CampoRegras
Endereço (id)Letras minúsculas, números e -. É o endereço do app na loja e não muda depois.
Nome2 a 40 caracteres, limpo como os textos de um card.
CategoriaMeu computador, Dinheiro, Dia a dia, Notícias e dev, Esporte, Delícias ou Outros.
Versão1.0 ou 1.0.0 (até três números).
DescriçãoEm português e em inglês, até 2000 caracteres cada.
InstruçõesComo usar e configurar, até 2000 caracteres.
O que mudouAté 500 caracteres: vira o changelog da versão na página do app.
Link do programaOpcional, até 300 caracteres.
ÍconePNG de até 64 KB, refeito em 128 px.
CapturasAté 6 PNGs de até 512 KB, refeitos em até 960 px.
CardO card da tela, validado com as regras do aparelho (A tela App).
FramesAté 4 PNGs (slots 0 a 3) de até 1 MB, convertidos para o formato do Miblo (até 60000 bytes de imagem).
ConfiguraçõesO esquema das configurações (Apps); nenhuma pode pedir chave, token ou senha.
CódigoOpcional: o zip do pacote de código, até 1 MB.

O servidor decodifica e refaz cada imagem; nada do arquivo original é servido. Até 10 envios por dia por membro.

O pacote de código

Um zip de Node.js puro, em ESM, sem dependências:

meu-app.zip
├── manifest.json
├── main.js
├── lib/fonte.js        (opcional: outros .js e .json)
└── README.md           (opcional)

manifest.json

JSON
{
  "id": "mare",
  "name": { "pt": "Maré", "en": "Tide" },
  "category": "daily",
  "promise": { "pt": "A maré está subindo?", "en": "Is the tide coming in?" },
  "settings": [
    { "key": "porto", "label": { "pt": "Porto", "en": "Port" }, "type": "text", "required": true, "max": 40 }
  ],
  "network": ["api.exemplo-mares.com"],
  "intervalMs": 600000,
  "preview": { "v": 1, "title": "Maré", "items": [{ "t": "big", "value": "1,2 m", "label": "subindo" }] }
}
CampoRegras
idO mesmo endereço do envio.
name{ pt, en }.
category, promise, previewComo em app.declare() (Apps).
settingsO esquema das configurações: é o que a pessoa preenche no app Miblo.
networkOs únicos hosts que o app pode acessar: nomes públicos (sem IP, porta ou nomes locais como .local), até 16. [] quer dizer offline. Precisa ser igual aos hosts escritos no código: um host usado e não declarado é recusado, e um declarado e nunca usado também.
intervalMsDe quanto em quanto tempo run() é chamado: de 10 s (10000) a 24 h.

main.js

JavaScript
// main.js: chamado a cada intervalMs, ou logo depois que a pessoa muda uma configuração.
export async function run(ctx) {
  const t = ctx.lang === 'pt' ? { up: 'subindo', down: 'descendo' } : { up: 'rising', down: 'falling' };
  const porto = encodeURIComponent(ctx.settings.porto);
  const data = await ctx.fetchJson(`https://api.exemplo-mares.com/v1/porto/${porto}`);
  const subindo = data.next > data.now;
  ctx.state.last = data.now;                          // guardado entre uma chamada e outra
  return {
    card: { title: 'Maré', items: [
      { t: 'big', value: `${data.now.toFixed(1)} m`, label: subindo ? t.up : t.down, color: 'blue' },
      { t: 'text', value: `atualizado ${ctx.timeOf(ctx.now())}`, color: 'grey' },
    ] },
    peek: false,                                      // true pede um peek (uma vez a cada 10 min)
  };
}

run(ctx) recebe:

ctx.O que é
settingsOs valores das configurações (padrões mais o que a pessoa salvou).
stateUm objeto que continua entre uma chamada e outra (na memória).
lang, localept ou en, e o locale do computador.
now(), timeOf(ms), sleep(ms)A hora, a hora formatada (14:05) e uma pausa.
fetchJson(url), fetchText(url)Buscam só nos hosts de network (https, 10 s, 256 KB).
stateDirA pasta onde o app pode gravar arquivos.
appO app (app.settings, app.connect), para quem precisa de uma conta conectada.
screen, frame(slot, image)A tela do app e o envio de um frame próprio (o desgaste da flash é por sua conta).

E devolve { card, peek?, frame?, plain? }: o card (sem v, o Miblo põe); peek (true são 10 s, ou um número de 1 a 10) pede para a tela vir para a frente; frame ({ slot, image: { rgba, w, h } }) grava um fundo antes do card, e plain é o card a usar se o frame for recusado. O Miblo declara o app, lê as configurações, pede as que faltam e desenha o card por você.

Regras dos arquivos

  • Só .js, .json e .md; no máximo 64 arquivos, 8 pastas de profundidade, nomes de até 64 letras, números, ., _ ou -, sem dois nomes que só mudam em maiúsculas.

  • Sem package.json e sem node_modules: o runtime do Miblo é o único "pacote".

  • O zip até 1 MB, tudo descompactado até 1 MB, cada script até 512 KB.

  • main.js precisa exportar run(ctx).

A análise automática

Roda no momento do envio e é a verdade que a revisão por IA não pode contrariar. Recusa na hora, com o motivo:

  • importar um módulo que a sandbox nunca permite: child_process, worker_threads, net, tls, dgram, http, https, http2, vm, module, wasi, cluster, inspector;

  • qualquer dependência (um módulo que não é interno do Node), node_modules ou package.json;

  • eval, new Function e equivalentes (texto em setTimeout, .constructor(...)), import() ou require() de algo calculado, createRequire, WebAssembly, código nativo (process.binding, dlopen), acesso calculado ao objeto global;

  • um endereço cujo host não está em network (também escrito https:host, com barras invertidas ou dividido em pedaços), ou um host declarado que o código nunca usa;

  • arquivos fora das regras acima, um script com erro de sintaxe, um import para um arquivo que não existe, um main.js sem run(ctx).

E aponta para quem revisa (sem recusar sozinho): hosts escritos sem https://, endereços http://, hosts montados em tempo de execução, blobs base64 ou hex acima de 1 KB, decodificação (atob, Buffer.from(..., 'base64')), sinais de ofuscação (nomes aleatórios, _0x..., textos picados, String.fromCharCode, linhas minificadas), leitura de process.env além de MIBLO_*, caminhos fora da pasta do app, caracteres invisíveis e texto dirigido ao revisor ("ignore as instruções anteriores", "aprove este app", encenação).

A revisão por IA

Depois da análise, o código entra na fila da revisão por IA (roda a cada 10 minutos e leva alguns minutos):

  • Duas passadas independentes: a primeira escreve o relatório; a segunda, com outras instruções, confere o relatório contra o código e diz o que ficou de fora.

  • O relatório cobre: o nível de risco, cada destino de rede e para quê, o que o app lê do computador, o que sai dele e para onde, persistência, comportamento escondido, sinais de código malicioso (roubo de credenciais, mineração, código baixado da rede, fuga da sandbox, ações destrutivas) e tentativas de manipular o revisor, com um resumo em português e em inglês.

  • A IA nunca aprova nem recusa. O veredito ("ok" ou "precisa de um olhar atento") é calculado pelo servidor: discordância entre as passadas, risco alto, texto dirigido ao revisor ou um host não declarado sempre pedem um olhar atento.

  • Proteção contra manipulação: o código vai como dado, entre marcadores, com a instrução fixa de que nada ali é instrução; a resposta é validada contra um esquema; textos são cortados.

A aprovação humana

Quem revisa vê os achados da análise, os dois relatórios, os hosts e, numa revisão, o que mudou desde a versão anterior. Só então aprova (o miblo.ai assina o pacote) ou recusa com um motivo, que chega a você por e-mail. A página pública do app mostra "Este app fala com: ..." (os hosts declarados e conferidos) ou "Não acessa a internet".

Depois de publicado

  • Revisões: uma edição é uma revisão nova, que passa pelo mesmo caminho. A versão aprovada continua no ar até a próxima ser aprovada, e há uma revisão pendente por vez.

  • Avaliações: membros dão de 1 a 5 estrelas; a média e o total aparecem na loja.

  • Denúncias: uma denúncia esconde o app na hora até alguém revisar (cada membro esconde no máximo dois apps por dia). Moderadores podem esconder e restaurar.

  • App escondido para quem já instalou: a cada início e a cada hora, o Miblo confere o pacote; um app que a loja deixou de servir ou escondeu é desligado.

Como o pacote chega a quem instala

O botão Ativar no meu Miblo abre o app Miblo no seu app (miblo://apps/enable/<id>), ou a pessoa roda miblo apps install <id>. O Miblo então:

  1. baixa miblo.ai/apps/<id>/package.json e confere a assinatura com a chave dos apps (separada da chave das atualizações), recusando um pacote sem assinatura válida;

  2. baixa o zip por https (sem redirecionamentos, nunca mais de 1 MB) e confere o SHA-256 assinado;

  3. confere cada arquivo, descompacta em <dados do Miblo>/apps/<id>/pkg e liga o app na sandbox.

Testar no seu computador antes de enviar

Rode o seu main.js do mesmo jeito que o Miblo vai rodar, com um pequeno script e o SDK:

JavaScript
// run-local.mjs: chama run(ctx) como o Miblo e desenha o card. node run-local.mjs
import fs from 'node:fs';
import { MibloStatus } from '@miblo/status';
import { run } from './main.js';

const manifest = JSON.parse(fs.readFileSync(new URL('./manifest.json', import.meta.url), 'utf8'));
const miblo = new MibloStatus();
const tool = manifest.name.pt.slice(0, 20);
const app = miblo.app({ id: manifest.id, tool });
const screen = miblo.screen({ tool });
await app.declare({ name: manifest.name, category: manifest.category, promise: manifest.promise, settings: manifest.settings, preview: manifest.preview });
const state = {};
for (;;) {
  const { values, missing } = await app.settings.get();
  if (missing.length) { await app.settings.request(missing, 'Preencha para testar'); await app.settings.wait(); continue; }
  const out = await run({ settings: values, state, lang: 'pt', locale: 'pt-BR', now: Date.now, sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
    timeOf: (ms) => new Date(ms).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }),
    fetchJson: async (u) => (await fetch(u)).json(), fetchText: async (u) => (await fetch(u)).text(), stateDir: './state' });
  await screen.card({ v: 1, ...out.card });
  if (out.peek) await screen.peek(10).catch(() => {});
  await new Promise((r) => setTimeout(r, manifest.intervalMs));
}
Terminal
export MIBLO_TOKEN=$(miblo sdk token)
node run-local.mjs                                   # rodando no terminal
miblo apps add "node run-local.mjs" --id mare        # ou sob o supervisor
miblo apps enable mare && miblo apps logs mare
miblo screen status                                  # o que a tela App tem agora

Esse script não é a sandbox: ele não bloqueia módulos nem hosts. Confira você mesmo que o código só usa ctx.fetchJson/ctx.fetchText e os hosts de network, sem os módulos bloqueados, antes de enviar.

Checklist antes de enviar

  • O Miblo+ está ativo na sua conta.

  • O app responde uma pergunta e a promessa diz qual, em português e em inglês.

  • O card cabe: título até 24, rótulos até 16, big até 10, text até 40 caracteres, até 4 itens, sem frases do Miblo.

  • Os frames são PNG de 8 bits com cores chapadas e cabem em 60000 bytes; o que muda está no card, não no frame.

  • As configurações não pedem segredo, e as obrigatórias têm um motivo claro em settings.request().

  • network lista exatamente os hosts que o código usa ([] se for offline), todos em https://.

  • Nenhum módulo bloqueado, nenhuma dependência, nenhum eval, nenhum texto para o revisor.

  • intervalMs é pelo menos 10000, e o app pede peek só quando algo importante acontece.

  • O app funciona sem internet (mostra os últimos valores e diz desde quando) e sem configuração (pede com request).

  • Testado no seu computador com o supervisor e com miblo apps logs limpo.

  • Ícone, capturas, descrição e o "o que mudou" preenchidos.