@planuze/pack-publisher
v0.4.9
Published
CLI de autoria, build, inspeção e publicação de packs .plnzpack.
Readme
@planuze/pack-publisher
CLI de autoria, build, inspeção e publicação de packs Planuze (.plnzpack).
Expõe o binário planuze e é a ferramenta que um autor (first-party ou de
comunidade) usa localmente e no CI para chegar do diretório de pack ao registry.
Pacote público (SDK de packs, ADR-0429); orquestra
@planuze/pack-format (formato/assinatura/cripto) e
@planuze/pack-runtime (tar/generator), lendo env
tipada via @planuze/platform-core (getEnv, nunca
process.env direto — §4.10).
Guias para autores: docs/DEVELOPING_PACKS.md e
docs/PUBLISHING_PACKS.md.
Comandos
planuze pack init <pack-id> [--yes] [--force]
[--kind template|extension] [--extends <parent>] [--extends-min-version 1.0.0]
[--modules <id1,id2,id3>]
planuze pack lint [pack-dir]
planuze pack build [pack-dir] --key=<pem> [--escrow-public-key=<base64>] [--out=<file>]
planuze pack publish <pack.plnzpack> [--token=<token>] [--endpoint=<url>] [--ci-mode]
planuze pack release [pack-dir] --key=<pem>
[--token=<token>] [--endpoint=<url>] [--out=<file>] [--ci-mode]
[--legacy-direct-oidc-attestation]
planuze pack inspect <pack.plnzpack> [--json] [--with-files] [--public-key=<pem>]
planuze pack run <pack-dir> [project-dir] [--steps=a,b] [--models=a,b] [--force] [--debug]
planuze pack publisher:keygen [--output=<dir>] [--ci-mode]
planuze pack publisher:register-key [--key=<pem>] [--endpoint=<url>] [--ci-mode]O parser é minimalista (cli.ts); cada comando devolve um exit code
(0 ok, 1 erro de conteúdo, 2 erro de uso/publish) e loga erros via
describeError. Cada comando é também exportado como função pura de
src/index.ts (packBuild, packPublish, …), devolvendo
Result<_, PackPublisherError> — testável sem shell nem rede (fetch injetável).
Fluxo de autoria
flowchart LR
I["init<br/>scaffold"] --> L["lint<br/>manifest/locales/docs"]
L --> B["build<br/>sign + encrypt + escrow"]
B --> P["publish<br/>upload-init → PUT → finalize"]
L --> A["release<br/>snapshot → scan local → build → PUT"]
A --> C["GitHub GitLab Bitbucket<br/>scan central OIDC → finalize"]
P --> R["registry<br/>scan + review"]
C --> Rinit
Scaffolds um pack novo (commands/pack-init.ts):
manifest.json (com publicKeyFingerprint placeholder sha256:0…0), locales/,
base/, generator/. --kind extension exige --extends <parent>; --modules
gera stubs em manifest.modules[]. Rejeita IDs reservados (planuze, core,
auth, …). --yes pula o wizard interativo (nome/descrição/linguagem/…).
lint
Roda lintPack do pack-format (commands/pack-lint.ts):
manifest válido pelo schema, locales/*.json cobrindo todas as labelKey,
generator.entrypoint existente, placeholders de rota conhecidos, aviso para fingerprint
placeholder e o gate de docs por locale (ADR-0303/0723 — toda doc declarada
existe em docs/<locale>/ + fallback en-US, sem stub). Exit 1 se houver erro.
build
packBuild(dir, options) (commands/pack-build.ts)
produz o .plnzpack assinado. Sempre roda lintPack() antes e aborta se houver
erro de lint. Depois:
- Lê
manifest.jsone a chave de assinatura Ed25519 (--key, default./planuze-pack-signing.pem). O manifest-fonte portátil mantém o placeholder canônicosha256:+ 64 zeros; o build o substitui pelo fingerprint real apenas dentro do artefato assinado. Se o source declara um pin não-zero, ele precisa coincidir com a chave ou o build falha antes do archive. O fingerprint da antiga root Planuze não deve ser copiado para o source. - Gera uma content key aleatória por pack (32 bytes, XChaCha20 — modelo único
ADR-0439; sem master
key global) e a lacra por escrow (ECIES X25519) para a pubkey de escrow do
servidor, gravando
contentKeyEscrowno manifest assinado. Só a privkey de escrow (custodiada no servidor) desembrulha — o CLI nunca conhece o segredo. - Cifra
base/e ogenerator/bundlado (ver abaixo) e oagent/opcional com a content key (AAD${id}@${version}), computa opack.lock(SHA-256 de cada entrada), assinamanifest.json(manifest.sig) epack.lock(content.sig) e grava o ZIP no layout normativo dedocs/specs/plnzpack-v1.md. - Copia
docs/<locale>/*em claro (pré-venda; ADR-0523). Forcing function (ADR-0572): se o manifest declara um doc sem o arquivo correspondente, o build falha — impede shippar pack que promete doc inexistente (drawer "Sem documentação" silencioso).
Bundle do generator (internal/bundle-generator.ts):
um pack materializado não tem node_modules, então o build inlina com esbuild toda
a árvore de deps do generator (handlebars/node-plop/prettier +
@planuze/pack-runtime/generator-tools) — ESM, platform: node, preservando a
estrutura de arquivos (multi-entry + code-splitting em chunks/), porque o
generator resolve assets via import.meta.url relativo. Assets em runtime (.hbs,
defaults.json) são copiados verbatim; testes ficam de fora. Um banner injeta
createRequire para deps CJS que fazem require dinâmico (ADR-0516).
publish
packPublish(file, options) (commands/pack-publish.ts)
envia o .plnzpack ao registry em três chamadas (ADR-0124/0384):
POST /admin/packs/upload-init(Authorization: Bearer <token>) → devolveuploadUrl+uploadId+ o header de upload esperado.PUT <uploadUrl>com os bytes (application/octet-stream); o token de upload vai por header, nunca na URL.POST /admin/packs/upload-finalizecom ouploadId. O escrow da content key viaja no manifest assinado — o registry o lê de lá no finalize (o publish não manda header de escrow).
Quando chamado pelo pack release canônico, há uma etapa obrigatória entre PUT e finalize. A CLI
cria um job central, aguarda o workflow Planuze escanear o mesmo blob recebido e só prossegue depois
do atestado OIDC correlacionado. O PUT sempre carrega x-planuze-artifact-sha256; qualquer falha ou
timeout impede o finalize. Sem attestation, o contrato histórico de packPublish permanece igual.
Retry (ADR-0546): 429/5xx são retryados com backoff exponencial (o registry
pode responder 503 num blip/cold-start do licensing que verifica o token — um
token válido não pode falhar por isso); 4xx (401/403/409/…) é determinístico,
nunca retryado; erro de rede é surfaced imediatamente. Falhas de fetch são
mapeadas para o erro de rede mais específico (timeout/offline/DNS) para diagnóstico
útil. --ci-mode emite o resultado como JSON (sucesso em stdout, erro em stderr)
para a GitHub Action parsear.
release
packRelease(dir, options) (commands/pack-release.ts) é o
boundary atômico de publicação (ADR-0882/0890): coleta base/, agent/, docs e o generator já
bundlado uma única vez; roda @planuze/pack-scanner localmente como fail-fast usando somente
manifest.stack.requires; status fail não produz archive. Em sucesso, cifra/assina o snapshot,
confere que o arquivo não mudou pelo SHA-256 esperado e faz PUT. Por padrão o release sempre
aguarda o scan central independente (waitForCentralScan) — o registry NUNCA pula esse re-scan.
O atestado direto legado só ativa com a flag --legacy-direct-oidc-attestation explícita nesta
invocação: a mera presença de ACTIONS_ID_TOKEN_REQUEST_URL/ACTIONS_ID_TOKEN_REQUEST_TOKEN no
ambiente nunca é suficiente sozinha (qualquer job GitHub Actions com permissions: id-token: write
os expõe, inclusive de um publisher adversarial rodando este SDK público na própria CI — ADR-0890/
0893). Sem os dois envs presentes, a flag falha cedo com usage error antes de build/upload. Os
exemplos oficiais (Portal Publisher / pack-boilerplate) nunca passam essa flag e solicitam o scan
central do blob exato em GitHub, GitLab e Bitbucket, aguardando seu atestado antes do finalize.
Ausência de readiness, falha ou timeout fecham o release sem fallback inseguro. O checkout é
tratado como dado: não roda npm install, lifecycle scripts, testes ou o generator do pack.
O subcomando interno pack scan-upload pertence somente ao reusable central e não integra a API
pública do SDK nem o help da CLI. Como o runner recomendado vive num repositório público, sua saída
é deliberadamente mínima: sucesso informa apenas completed; falhas viram
central_scan_failed. Paths, pack id, manifest, findings, snippets, versão do scanner, content key
e upload id nunca são serializados em stdout/stderr. O reusable executa apenas o comando estático
planuze pack scan-upload --ci-mode. O Registry
resolve upload e checksum pelo run_id do token OIDC já vinculado ao dispatch; nenhum UUID,
checksum, endpoint ou audience passa por input, with:, env ou argv do runner público.
inspect
packInspect(file, options) (commands/pack-inspect.ts)
lê apenas manifest, lock, entradas do ZIP e tamanhos — nunca decifra
conteúdo. Com --public-key, valida a assinatura (via PackVerifier em modo
direto) sem expor o payload cifrado, reportando valid/invalid/not_requested.
--json e --with-files controlam a saída.
run
packRun(packDir, projectDir, options) (commands/pack-run.ts)
executa o generator de um pack de fonte (via handleFromFilesystem +
GeneratorEngine) contra um diretório de projeto — para desenvolver/testar steps
localmente. --steps/--models filtram o que roda; --debug imprime cada evento
NDJSON. Exit 1 se o generator emitir run.finished {ok:false}.
Chaves de publisher (chain-of-trust)
publisher:keygen(commands/publisher-keygen.ts) gera o keypair Ed25519 de assinatura localmente — a privkey nunca sai da máquina (§4.9), salva em~/.planuze/publisher.key(modo0600) +.pub(0644), e imprime o fingerprint. O comando recusa a operação se qualquer um desses caminhos já existir; nunca use a geração de chave para substituir uma identidade cadastrada.--helpapenas exibe o uso e não acessa o filesystem.publisher:register-key(commands/publisher-register-key.ts) registra a pubkey via challenge-response: pede um nonce, assina-o com a privkey local (provando posse sem expor a chave) e faz o register. A chave entra emawaiting-verificationaté o staff root-assiná-la (ADR-0179/0405) — fechando a cadeiapack ← publisher ← rootque o consume verifica.
Segredos e configuração
Lidos por flag (precedência) ou env tipada via @planuze/platform-core.getEnv:
| Env / flag | Uso |
|---|---|
| --key / ./planuze-pack-signing.pem | privkey Ed25519 de assinatura do pack (build) |
| --escrow-public-key / PLANUZE_ESCROW_PUBLIC_KEY | override técnico da pubkey X25519 de escrow (base64). O fluxo normal não configura esse valor: o build busca <registry>/escrow-public-key automaticamente (ADR-0445) |
| --token / PLANUZE_PUBLISHER_TOKEN | token consumido pela CLI. Os snippets públicos recebem o secret PLANUZE_PUBLISH_TOKEN e o mapeiam para esta env sem logá-lo |
| --endpoint / PLANUZE_REGISTRY_URL | base do registry; fallback canônico https://registry.planuze.com/v1 |
| audiência OIDC | derivada internamente de <endpoint>/scan-attestations; não aceita input separado |
| --legacy-direct-oidc-attestation (release) | opt-in EXPLÍCITO por invocação para o atestado direto legado; sem a flag, release sempre usa o scan central. Exige ACTIONS_ID_TOKEN_REQUEST_URL + ACTIONS_ID_TOKEN_REQUEST_TOKEN (falha cedo com usage error se ausentes) |
| ACTIONS_ID_TOKEN_REQUEST_URL + ACTIONS_ID_TOKEN_REQUEST_TOKEN | envs que o GitHub injeta em jobs com permissions: id-token: write; só têm efeito quando combinados com --legacy-direct-oidc-attestation — a mera presença deles nunca seleciona o atestado direto sozinha (ADR-0890/0893). O fluxo público provider-neutral remove essa flag e usa o scan central |
| PLANUZE_USER_SESSION_TOKEN | sessão para publisher:register-key |
| PLANUZE_API_URL | base do api-gateway (registro de chave) |
A privkey de assinatura e o token de publish são secrets — nunca commitados.
No caller GitHub, os nomes públicos são exatamente PLANUZE_SIGNING_KEY e
PLANUZE_PUBLISH_TOKEN. PLANUZE_REGISTRY_URL não é necessária para produção.
Não confundir a pubkey de escrow (pública por design — só lacra a content key,
nunca a abre) com a privkey de assinatura.
CI/CD
GitHub Actions, GitLab CI e Bitbucket Pipelines executam planuze pack release pelo mesmo scan
central. Os pins imutáveis do runtime e da CLI são rotacionados em conjunto e, por isso, não são
duplicados neste README do pacote. Copie sempre o snippet exibido pelo Portal Publisher ou use o
pack-boilerplate como fonte canônica. Em
todos os casos, a CLI seleciona o scan central automaticamente e não recebe token GitHub, OIDC
manual, SCAN_RUNNER_URL nem secrets de infraestrutura.
O Portal só exibe qualquer snippet depois que o runtime público confirma central_oidc/ready,
incluindo o gate operacional pós-canário. Os três providers exigem no repositório apenas
PLANUZE_PUBLISH_TOKEN e a chave de assinatura. GitHub/GitLab usam PLANUZE_SIGNING_KEY;
Bitbucket usa a mesma PEM em Base64 numa única linha como PLANUZE_SIGNING_KEY_B64 e a
decodifica/valida num arquivo temporário antes do release.
O exemplo completo está em
docs/PUBLISHING_PACKS.md. O comando legado
pack publish permanece uma API de transporte, não uma alternativa ao release
atestado quando a autoridade OIDC está ativa.
Boundaries e build
- Gate
check:cruiser(regrapack-publisher-*): depende de@planuze/core-kit,@planuze/platform-core,@planuze/pack-format,@planuze/pack-runtime,@planuze/pack-scanner(+esbuild,node-plop). - Toda operação com falha esperada devolve
Result<_, PackPublisherError>(§4.6).
pnpm --filter @planuze/pack-publisher build # tsup → dist/ (+ bin/planuze.js)
pnpm --filter @planuze/pack-publisher test # vitest run
pnpm --filter @planuze/pack-publisher typecheck # tsc --noEmit