@tabularium/manifest
Pure validator + integrity primitives for the Tabularium plugin registry. No Bun-specific APIs, no DB, no network — runs in Bun, Node ≥ 19, Deno, and the browser. Used by the registry server, the @tabularium/cli tool, and any consumer that wants to verify a release locally.
Install
bun add @tabularium/manifest
npm i @tabularium/manifest
Exports
Validation
import { parseManifest, validateManifest, buildSchema, fetchSchema, sniffSource, type ValidationError } from '@tabularium/manifest'
parseManifest(text, source) — YAML or JSON → object. Throws ParseError. source is 'tabularium.yaml' | 'tabularium.json'; use sniffSource(text) to auto-detect.
validateManifest(parsed, schema, { lenient? }) — ajv-backed JSON Schema 2020-12 validator. Returns { ok: true, normalized, errors: [] } or { ok: false, normalized: null, errors }. lenient: true strips unknown fields via removeAdditional: 'all' instead of erroring.
buildSchema(opts) — merges the core ManifestSchema with operator-defined extension deltas (global + per-kind) into a single JSON Schema.
fetchSchema(url) — GET <url>/manifest.schema.json helper. Includes the response body in HTTP error messages.
ValidationError shape:
type ValidationError = {
path: string
code: string
message: string
expected?: unknown
actual?: unknown
}
Integrity
import { canonicalize, verifyAssetHash, verifyRegistrySignature } from '@tabularium/manifest'
canonicalize(value) — RFC 8785 JSON Canonicalisation Scheme. Lexicographic key order, no whitespace, RFC-compliant number serialisation, rejects NaN/Infinity/undefined. Used to compute deterministic bytes for signing/verification.
verifyAssetHash(stream, expectedHex) — reads a ReadableStream<Uint8Array>, computes SHA-256, compares constant-time. Returns { ok, sha256, size }.
verifyRegistrySignature({ payloadBytes, signature, publicKeyJwk }) — Web Crypto Ed25519 verify. Returns boolean.
The package intentionally does not parse JWS or fetch JWKS — use a JOSE library (jose) for that and feed the raw bytes here. This keeps the dependency surface minimal.
Examples
Validate a manifest
import { parseManifest, validateManifest, fetchSchema } from '@tabularium/manifest'
const text = await Bun.file('./tabularium.yaml').text()
const schema = await fetchSchema('https://registry.example.com')
const parsed = parseManifest(text, 'tabularium.yaml')
const result = validateManifest(parsed, schema)
if (!result.ok) {
for (const e of result.errors) {
console.error(`${e.path} ${e.code} ${e.message}`)
}
process.exit(1)
}
Verify a downloaded asset
import { verifyAssetHash } from '@tabularium/manifest'
const releaseRes = await fetch('https://registry.example.com/api/plugins/my-plugin')
const plugin = await releaseRes.json() as { releases: Array<{ version: string, integrity: { jws: string, assets: Array<{ name: string, sha256: string, size: number }> } | null }> }
const release = plugin.releases[0]
const expected = release.integrity!.assets.find(a => a.name === 'my-plugin.zip')!
const assetRes = await fetch('https://github.com/me/my-plugin/releases/download/.../my-plugin.zip')
const { ok, sha256, size } = await verifyAssetHash(assetRes.body!, expected.sha256)
if (!ok) throw new Error(`hash mismatch: got ${sha256}, expected ${expected.sha256}`)
if (size !== expected.size) throw new Error(`size mismatch`)
Verify the registry signature
import { canonicalize, verifyRegistrySignature } from '@tabularium/manifest'
import { compactVerify, importJWK } from 'jose'
const jwks = await (await fetch('https://registry.example.com/.well-known/registry-key.json')).json() as { keys: Array<{ kid: string, kty: string, crv: string, x: string, alg: string }> }
const { jws } = release.integrity!
const headerB64 = jws.split('.')[0]
const { kid } = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(headerB64.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))))
const jwk = jwks.keys.find(k => k.kid === kid)
if (!jwk) throw new Error(`unknown kid ${kid} — JWKS may be stale`)
const key = await importJWK(jwk, 'EdDSA')
const { payload, protectedHeader } = await compactVerify(jws, key)
const body = JSON.parse(new TextDecoder().decode(payload)) as { plugin_slug: string, release_version: string, assets: Array<{ name: string, sha256: string, size: number }> }
const signedAsset = body.assets.find(a => a.name === 'my-plugin.zip')
if (!signedAsset || signedAsset.sha256 !== expected.sha256) throw new Error('signed payload does not match release JSON')
You now have three layers of trust:
- Content — SHA-256 of the actual download matches.
- Registry — JWS proves the registry committed to that hash for that version.
- (Optional) Author — if the asset has an
attestation_bundle, verify it with a sigstore library to prove the GitHub Actions run that built it.
Stability
This package follows the registry's release cycle. The ManifestSchema shape is versioned via the $schema URL and stays additive within a major version. Breaking changes to validation behaviour (lenient defaults, error codes) are called out in the changelog.