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:
Você testa no seu computador com o SDK e o supervisor.
Envia em miblo.ai/apps/enviar: a ficha do app, as telas e, se tiver, o pacote de código.
A análise automática confere o pacote na hora e recusa o que não pode passar, dizendo o motivo.
A revisão por IA escreve um relatório de segurança do código (minutos depois).
Uma pessoa aprova. Nenhum app entra na loja sem aprovação humana.
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
| Tipo | O que leva | O que roda no computador de quem instala |
|---|---|---|
| Pacote de tela | Um card, até 4 frames e as configurações | Nada: o Miblo aplica a tela como um programa faria |
| App com código | Tudo 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
| Campo | Regras |
|---|---|
Endereço (id) | Letras minúsculas, números e -. É o endereço do app na loja e não muda depois. |
| Nome | 2 a 40 caracteres, limpo como os textos de um card. |
| Categoria | Meu computador, Dinheiro, Dia a dia, Notícias e dev, Esporte, Delícias ou Outros. |
| Versão | 1.0 ou 1.0.0 (até três números). |
| Descrição | Em português e em inglês, até 2000 caracteres cada. |
| Instruções | Como usar e configurar, até 2000 caracteres. |
| O que mudou | Até 500 caracteres: vira o changelog da versão na página do app. |
| Link do programa | Opcional, até 300 caracteres. |
| Ícone | PNG de até 64 KB, refeito em 128 px. |
| Capturas | Até 6 PNGs de até 512 KB, refeitos em até 960 px. |
| Card | O card da tela, validado com as regras do aparelho (A tela App). |
| Frames | Até 4 PNGs (slots 0 a 3) de até 1 MB, convertidos para o formato do Miblo (até 60000 bytes de imagem). |
| Configurações | O esquema das configurações (Apps); nenhuma pode pedir chave, token ou senha. |
| Código | Opcional: 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
{
"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" }] }
}| Campo | Regras |
|---|---|
id | O mesmo endereço do envio. |
name | { pt, en }. |
category, promise, preview | Como em app.declare() (Apps). |
settings | O esquema das configurações: é o que a pessoa preenche no app Miblo. |
network | Os ú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. |
intervalMs | De quanto em quanto tempo run() é chamado: de 10 s (10000) a 24 h. |
main.js
// 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 é |
|---|---|
settings | Os valores das configurações (padrões mais o que a pessoa salvou). |
state | Um objeto que continua entre uma chamada e outra (na memória). |
lang, locale | pt 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). |
stateDir | A pasta onde o app pode gravar arquivos. |
app | O 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,.jsone.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.jsone semnode_modules: o runtime do Miblo é o único "pacote".O zip até 1 MB, tudo descompactado até 1 MB, cada script até 512 KB.
main.jsprecisa exportarrun(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_modulesoupackage.json;eval,new Functione equivalentes (texto emsetTimeout,.constructor(...)),import()ourequire()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 escritohttps: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.jssemrun(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:
baixa
miblo.ai/apps/<id>/package.jsone confere a assinatura com a chave dos apps (separada da chave das atualizações), recusando um pacote sem assinatura válida;baixa o zip por https (sem redirecionamentos, nunca mais de 1 MB) e confere o SHA-256 assinado;
confere cada arquivo, descompacta em
<dados do Miblo>/apps/<id>/pkge 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:
// 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));
}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 agoraEsse 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,
bigaté 10,textaté 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().networklista exatamente os hosts que o código usa ([]se for offline), todos emhttps://.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 logslimpo.Ícone, capturas, descrição e o "o que mudou" preenchidos.