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

@planuze/pack-format

v0.3.12

Published

Formato .plnzpack: manifest Zod, arquivo ZIP, assinaturas Ed25519, criptografia XChaCha20-Poly1305, lockfile e lint.

Readme

@planuze/pack-format

Define e implementa o formato .plnzpack v1 — o contrato de empacotamento dos packs Planuze (templates, extensions e module-packs). É a fonte única do manifest, do layout do arquivo, das assinaturas, da criptografia, do lockfile e do lint. Pacote público (base do SDK de packs, ADR-0429), sem I/O de rede; roda no builder Node (@planuze/pack-publisher), no app desktop (@planuze/pack-runtime) e no scanner server-side.

Referência normativa: docs/specs/plnzpack-v1.md.

Layout do .plnzpack

Um .plnzpack é um ZIP com caminhos POSIX relativos (constantes em archive/constants.ts):

manifest.json              # JSON validado por packManifestSchema
manifest.sig               # assinatura Ed25519 dos bytes de manifest.json
content/
  base.tar.zst             # arquivos base (copiados no scaffold) — cifrado
  generator.tar.zst        # scripts do generator — cifrado
  agent.tar.zst            # opcional — cifrado
pack.lock                  # sha256 de cada entrada hasheável
content.sig                # assinatura Ed25519 dos bytes de pack.lock
docs/<locale>/<arquivo>    # docs EM CLARO (opcional; ADR-0523) — pré-venda

Entradas absolutas, segmentos .., \ e paths com drive Windows são inválidos (archive/path-safety.ts — isSafeArchivePath). Os docs top-level (docs/) são o único conjunto variável e ficam em claro (o registry os serve sem decrypt — pré-venda); tudo mais é um conjunto fechado.

Reader / Writer

  • PackArchiveWriter.write(path, input) (archive/writer.ts) monta o ZIP na ordem canônica via archiver (nível 6). Os listeners de stream são IDisposable e liberados quando a operação settla.
  • PackArchiveReader(path) (archive/reader.ts) lê entrada por entrada via unzipper, com hard caps anti-DoS (≤20 000 arquivos, ≤64 MiB por entrada, ≤128 MiB total), rejeição de paths inseguros e de entradas duplicadas, e memoização do central directory por instância (ADR-0490 — um install lê ~7 entradas e cada leitura reabria o ZIP).

Ambos archiver e unzipper são CJS e carregados via createRequire lazy/memoizado: o barrel de pack-format é importado por Workers (pack-registry), onde import.meta.url é undefined no workerd; o require só dispara quando o Reader/Writer é de fato instanciado (nunca em Worker).

manifest.json

O manifest é validado por packManifestSchema (manifest/schemas.ts, Zod 4). parsePackManifest / parsePackManifestJson (manifest/parse.ts) devolvem Result<PackManifest, AppError> (§4.6, sem throw).

Kinds e install target

O campo kind discrimina o comportamento do pack, e resolveInstallTarget(manifest) resolve onde o generator escreve:

| kind | appliesTo | Install target | |---|---|---| | template (default) | proibido | raiz do projeto (project-root) | | extension | obrigatório | src/modules/<id>/ (project-module) | | module-pack | obrigatório | src/modules/<alvo>/_pkgs/<id>/ (module-subdir; alvo é input do install) |

Type guards isTemplateManifest / isExtensionManifest / isModulePackManifest para call sites. O superRefine do schema enforça a matriz kind↔appliesTo (ex.: extension exige appliesTo, proíbe wizard, e appliesTo.packId ≠ id).

distribution (visibilidade e acesso)

packDistributionSchema carrega license (free/paid/private), channels (stable/beta/canary), tier, requiredCapabilities (gate de acesso por assinatura é a capability, não o tier ordinal — ADR-0419), category/tags (taxonomia validada pelo registry no publish) e a visibilidade:

  • public — todos veem.
  • org-private — só membros das allowedOrgs[] (refine exige allowedOrgs não-vazio; ADR-0335).
  • byMyself — só a conta que publicou (refine proíbe allowedOrgs; multi-device account-scoped).

A partir do ADR-0423 a visibility é controle de acesso do servidor (metadata KV mutável); o valor no manifest é apenas o inicial. A assinatura garante o conteúdo, não a visibility.

declarations — o vocabulário CRUD do pack

packDeclarationsSchema é o coração do modelo declarativo. Cada seção é uma lista com id único (uniqueness enforçada por superRefine) e labelKey i18n:

| Seção | Declara | |---|---| | columnTypes | tipos de coluna do editor: valueType, parameters[] (inputs dinâmicos: integer/enum/string/boolean), flags[] (optional/unique/encrypted/hidden/indexed), defaultValue, supports | | relationTypes | cardinalidade + requiresFkColumn (default por cardinality via relationRequiresFkColumn; m:n usa join table) | | routeTypes | defaultPath (inicia com /), allowedTargets, supports (auth/where/link/request.{body,query,params,projection}/upload + chaves custom booleanas) | | validatorMethods | appliesTo[] (deve referenciar columnTypes[].id), parameters[], kind (standard/passthrough) | | lambdaTypes, triggers, integrations, iamPolicies | packs non-CRUD (AWS Lambdas etc., ADR-0177); lambdaTypes[].requiredIamPolicyIds referenciam iamPolicies[].id |

Cross-validations do superRefine: uniqueness de todos os ids, validatorMethods.appliesTo → columnTypes (exceto extension, resolvida pelo composer), lambda→iamPolicy, e uniqueness de props/steps por módulo.

toolkit — vocabulário de runtime/persistência

packToolkitSchema é o espelho de declarations para o runtime gerado (o que o core/templates não pode conhecer, §4.18/ADR-0241). Inclui columnTypes[].mapsTo (tipo Prisma alvo), whereOperators[].prismaOp (o pack mapeia id → operador Prisma; o core nunca conhece o vocabulário Prisma), validators com nameHints (auto-complete por token de nome de coluna) e tipos de param extra collection-ref/column-ref (FK por campo do body, ADR-0274), e defaultColumns (colunas semeadas em toda coleção: id/timestamps/auth-state).

Outras seções

modules[] (sub-features ativáveis por projeto — ex.: auth com jwt/oauth/session, com props/envVars/declarations/steps/augmentsAuthCollection), generator (entrypoint, steps com dependsOn/condition, promptPatterns, postCreateCommands), agent (opcional), wizard, uploadModes, crossRelationTypes, actions/collectionActions/moduleActions/ projectActions, runtimeDeps (deps npm do projeto gerado, common + byModule).

Campos setados no build: publicKeyFingerprint (sha256:<hex>), publisherFingerprint (chain-of-trust per-publisher), contentKeyEscrow (ECIES), signatureAlgorithm: 'ed25519'.

Assinaturas (Ed25519)

signing/ed25519.ts implementa o esquema Ed25519 puro do Node 24 (sign(null, bytes, key) / verify(null, bytes, key, sig) — nunca createSign('SHA512')). Todas as operações devolvem Result<_, PackFormatError>:

  • generateEd25519KeyPair / exportEd25519KeyPairPem (spki/pkcs8) / import*Pem.
  • fingerprintEd25519PublicKey — sha256:<hex> do DER(spki) (forma canônica cross-runtime).
  • signBuffer / verifyBuffer.
  • createSignedEnvelope / verifySignedEnvelope — payload + assinatura sobre um JSON stável (chaves ordenadas), para metadados assinados fora do pack.
  • createPublisherSigningKey (par + fingerprint, sem I/O) e signChallengeNonceHex (registro de chave por challenge-response).

manifest.sig assina os bytes exatos de manifest.json; content.sig assina os bytes exatos de pack.lock (que por sua vez hasheia o resto — ver Lock).

Chain-of-trust per-publisher

signing/chain.ts implementa a cadeia da comunidade (ADR-0021 / ADR-0363 §8): cada publisher tem keypair próprio; quando o publisher é verificado, a root assina o DER(spki) da pubkey do publisher. verifyPackChain(packBytes, packSig, publisherKey, rootPubKeyPem) encadeia duas assinaturas — (1) pack ← pubkey do publisher, (2) pubkey ← root — e devolve um erro discriminado por step (pack-sig / root-sig / key-unverified). signPublicKeyWithRoot é o root-sign usado pelo admin.

Criptografia

Dois envelopes assimétricos/simétricos distintos:

Blobs de conteúdo — XChaCha20-Poly1305 (encryption/xchacha.ts, via @noble/ciphers): chave de 32 bytes, nonce de 24 bytes aleatórios, AAD recomendado = ${packId}@${version}. encryptContent/decryptContent operam sobre EncryptedContent; packEncryptedEnvelope/unpackEncryptedEnvelope serializam para/de o JSON { algorithm, nonce, ciphertext, aad? }. derivePackContentKey(masterKey, packId, version) é a fonte única da derivação HKDF-SHA256 (salt ${packId}@${version}, info CONTENT_KEY_INFO) — publish, consume e escrow DEVEM derivar idêntico, senão a key não decifra.

Escrow da content-key — ECIES (X25519 + HKDF + XChaCha20) (encryption/escrow.ts): wrapContentKey(contentKey, escrowPubKey) lacra a content-key aleatória do pack para a pubkey de escrow do servidor, no formato ecies:v1:<ephPub>:<nonce>:<ct>. Só a privkey de escrow (custodiada no servidor) desembrulha com unwrapContentKey (ADR-0370). Assim o CLI do publisher (máquina não-confiável) nunca conhece o segredo de escrow. generateEscrowKeyPair / escrowPublicKeyFromPrivate completam o par.

pack.lock

lock/compute.ts computa o lock: um mapa arquivo → sha256(conteúdo) (packLockSchema em lock/schemas.ts):

{ "version": 1, "algorithm": "sha256", "files": { "manifest.json": "…", "content/base.tar.zst": "…" } }
  • Entradas hasheadas: manifest.json, manifest.sig, content/base.tar.zst, content/generator.tar.zst, content/agent.tar.zst (se existir) e cada doc top-level em claro (ADR-0523 — assinada via content.sig, integridade no consume).
  • pack.lock e content.sig não entram no lock (content.sig assina o lock; incluí-lo criaria ciclo).
  • computePackLockFromEntries rejeita entradas inesperadas (fail-closed).
  • verifyPackLock(reader) recomputa e compara conjunto de arquivos + hash por arquivo (file_set_mismatch / hash_mismatch).
  • serializePackLock emite JSON com chaves ordenadas (determinístico → assinatura estável).

Lint

lintPack(packDir) (lint/lint-pack.ts) roda antes de publicar e devolve um LintReport (issues[] com severity error/warning, errorCount, warningCount; cada issue tem messageKey i18n + path no manifest). Regras:

| Regra | Arquivo | Severity | |---|---|---| | manifest.json existe, é JSON e passa no schema | lint/manifest.ts | error | | generator.entrypoint é path relativo seguro e existe no disco | lint/generator.ts | error | | locales/*.json cobre todas as labelKey usadas pelo manifest | lint/locales.ts | error | | routeTypes.defaultPath sem placeholders desconhecidos | lint/routes.ts | warning | | publicKeyFingerprint ainda é o placeholder portátil sha256:0…0 (o build injeta o valor real no artefato assinado) | lint/fingerprint.ts | warning | | docs por locale — cada doc declarada existe e não é stub | lint/docs.ts | error |

Gate de documentação por locale (ADR-0303 + ADR-0723)

lintPackDocs (lint/docs.ts) é a forcing function do ADR-0723: toda doc declarada (manifest.doc de overview + modules[].doc) precisa existir em docs/<locale>/<doc> para cada locale suportado (derivado de locales/*.json) mais o fallback en-US (o resolvedor de docs cai em en-US quando o locale específico falta — ADR-0303; sem a doc en-US o fallback resolve vazio). Além da existência, exige um piso de conteúdo (MIN_DOC_CONTENT_LINES = 10 linhas não-vazias) — existência sozinha passaria com um stub praticamente vazio. Docs faltando ou stub viram LintIssue de severity error (bloqueiam o build).

Erros

PackFormatError (errors.ts) estende AppError com kinds de domínio: pack/invalid_archive_entry, pack/missing_archive_entry, pack/invalid_signature (target), pack/invalid_lock (entryPath, reason) e pack/crypto_failed (com CryptoOperation discriminando encrypt/decrypt/ sign/verify/derive_key/generate_key/import_key/export_key). Cada um tem messageKey: LabelKey para resolução i18n na UI.

API pública (subpath exports)

| Import | Conteúdo | |---|---| | @planuze/pack-format | tudo (barrel raiz — src/index.ts) | | @planuze/pack-format/manifest | schemas + parse do manifest | | @planuze/pack-format/encryption | XChaCha + escrow |

Boundaries e build

  • Gate check:cruiser (regra pack-format-*): só depende de @planuze/core-kit (+ @noble/*, archiver, unzipper, zod).
  • attw valida package.json#exports.
pnpm --filter @planuze/pack-format build       # tsc modular → dist/
pnpm --filter @planuze/pack-format test        # vitest run
pnpm --filter @planuze/pack-format typecheck   # tsc --noEmit