New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@forestrie/forestrie-cli

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@forestrie/forestrie-cli

forestrie — participant CLI for forestrie transparency logs (SCITT / COSE receipts)

latest
Source
npmnpm
Version
0.9.1
Version published
Weekly downloads
324
2.21%
Maintainers
1
Weekly downloads
 
Created
Source

forestrie-cli

forestrie — the participant CLI for forestrie transparency logs (SCITT / COSE receipts). Ships two ways — an npm package that runs on Node (npx @forestrie/forestrie-cli) and a dependency-free static binary on every GitHub release — both hosting the subcommands a participant needs to deploy, sign, register, and verify against a forestrie log:

SubcommandWhat it does
deployDeploy a univocity instance (ES256 bootstrap is the paved path)
sign-statementProduce a SCITT signed statement (plain COSE Sign1 with CWT claims)
registerRegister a signed statement via SCRAPI, download the receipt
register-grantAuthorize a signer for a child/data log (one grant per signer)
complete-grantSelf-create the Forestrie-Grant header from a checkpoint
resolve-receiptProduce or freshen a COSE receipt — from tiles, or tile-free from a .sth/calldata chain (alias: create-receipt)
decode-receiptDecode a COSE receipt — it is just COSE: Sign1 + MMR inclusion
verifyVerify a receipt offline — the same closer for every other subcommand
fetch-accumulatorCache the on-chain accumulator as a --known-accumulator snapshot

Status: subcommands not yet implemented declare their real argument surface and parse it, then exit non-zero with a structured not_implemented error. --json emits that report as JSON on stdout. Implementations land per-subcommand.

Install from npm

npx @forestrie/forestrie-cli --help          # no install
npm install -g @forestrie/forestrie-cli      # then: forestrie --help
forestrie verify --help

Runtime: the npm package is a Node program — Node >= 20.10 and nothing else. Bun is the development and static-binary toolchain, not a runtime requirement: nothing under src/ uses a Bun-only API, and dist/cli.js is built with --target node. If you would rather have no runtime at all, take the static binary below.

Install from a release

Every GitHub release attaches prebuilt static binaries — forestrie-darwin-arm64 and forestrie-linux-x64 — each with a .sha256 sidecar. No runtime is needed; just download, verify, and run:

target=darwin-arm64   # or: linux-x64
base=https://github.com/forestrie/forestrie-cli/releases/latest/download
curl -fsSLO "${base}/forestrie-${target}"
curl -fsSLO "${base}/forestrie-${target}.sha256"
shasum -a 256 -c "forestrie-${target}.sha256"
chmod +x "forestrie-${target}"
./forestrie-${target} --help

Releases are cut by pushing a v* tag; the workflow fails closed unless the tag matches the package.json version (release.yml). The same tag publishes the npm package (publish.yml), with npm trusted publishing and provenance attestation.

Install from source

Requires Bun (pinned in mise.toml; mise install works). All dependencies install tokenless from public npmjs — no registry auth, no .npmrc:

bun install
bun run typecheck && bun test
bun run dev -- --help          # run from source

Build the static binary

bun run build:binary            # host platform → dist/forestrie
bun run build:binary:linux-x64  # → dist/forestrie-linux-x64
bun run build:binary:darwin-arm64
./dist/forestrie --help

Conventions

  • Human output by default; --json for machine-readable output on stdout.
  • -v / --verbosity per @forestrie/cli-kit reporting (-1 silences stderr feedback; stdout stays pipeable).
  • Env fallbacks: FORESTRIE_BASE_URL, RPC_URL, GRANT_B64, OWNER_ADDRESS, DEPLOYER_KEY, FORESTRIE_CONFIG; any string flag also accepts ${env:VAR} / ${env} whole-value templates.
  • ES256 is the paved path end-to-end; KS256 appears only in the Safe / ERC-1271 aside.

Usage

Minimal example per subcommand. $FORESTRIE_BASE_URL, $RPC_URL, and $GRANT_B64 are the env fallbacks from Conventions.

deploy

forestrie deploy \
  --bootstrap-alg es256 \
  --bootstrap-es256-generate --bootstrap-es256-pem-out bootstrap.es256.pem \
  --owner-address 0xYourDeployer \
  --rpc-url "$RPC_URL" \
  --out deployment.json
ImutableUnivocity deployed at: 0xAbC…123
genesisLogId: 0f9a1c7e-…-…
chainId: 84532  txHash: 0x9f…21
wrote deployment record to deployment.json

sign-statement

forestrie sign-statement \
  --key alice.es256.pem \
  --payload statement.json --content-type application/json \
  --out statement.cose
signed statement: plain COSE Sign1 (ES256)
  kid:       241115ab754013fcbf2e88544a369009d5d7de7f54497ad640ef28ad6237392c
  iss:       241115ab754013fcbf2e88544a369009d5d7de7f54497ad640ef28ad6237392c
  sub:       sha-256:9c3f8a5d0e6b17c4a2f1d8e05b6c93a7714f2e8d90ab35c6e1d47f80b92c5a13
  payload:   45 bytes (application/json)
  statement: 253 bytes -> statement.cose

Statements carry SCITT CWT claims (protected COSE header label 15, covered by the signature). Defaults keep signing zero-config and deterministic; every claim is settable:

  • --iss <string-or-uri> — issuer (CWT claim 1). Default: the lowercase hex kid. The keyword ckt derives the RFC 9679 COSE Key Thumbprint URI (urn:ietf:params:oauth:ckt:sha-256:…) from the signing key — which also means a literal issuer named ckt is not expressible.
  • --sub <string-or-uri> — subject the statement speaks about (CWT claim 2). Default: sha-256:<hex> of the payload bytes.
  • --iat now|<unix-seconds> — issued-at (CWT claim 6). Omitted by default so repeated signing of the same payload is byte-identical.

Empty --iss/--sub values are rejected rather than silently replaced with the defaults.

register

forestrie register \
  --base-url "$FORESTRIE_BASE_URL" \
  --log-id "$LOG_ID" \
  --statement statement.cose \
  --grant-b64 "$GRANT_B64" \
  --out receipt.cbor
entryId: 0202020202020202…0001
statusUrl: https://.../register/.../entries/…
receiptUrl: …/receipt
wrote receipt (612 bytes) to receipt.cbor

register-grant

forestrie register-grant \
  --base-url "$FORESTRIE_BASE_URL" \
  --owner-log "$OWNER_LOG_ID" --data-log "$DATA_LOG_ID" \
  --sign-with bootstrap.es256.pem \
  --out-b64 grant.b64
ownerLog: 0f9a1c7e-…-… (grant leaf)
dataLog: 8c2e4b…-… (authorized)
signer: 04a91f…
entryId: …
wrote completed grant base64 to grant.b64

complete-grant

forestrie complete-grant \
  --grant grant.b64 \
  --checkpoint checkpoint.sth --massif massif.log \
  --out-b64 grant.completed.b64
complete-grant: leaf       — mmrIndex 0 (recovered from massif)
complete-grant: entry id   — 01010101010101010000000000000000 (idtimestamp from massif)
complete-grant: proof      — 1 node(s) to peak 1/2
complete-grant: checkpoint — sealed size 4, delegation cert copied: no
complete-grant: receipt    — 118 bytes attached (header 396)

Self-creates the Forestrie-Grant header from a checkpoint, without an operator round-trip: it locates the grant's leaf in the local --massif blob by its commitment hash (recovering the mmrIndex and the sequenced idtimestamp), rebuilds the inclusion receipt against the checkpoint's pre-signed peak, and attaches the receipt + idtimestamp — the same completed bearer register-grant produces online. --massif is required (it carries the leaf); --idtimestamp <hex|path> is a fallback for the rare massif with no index region.

resolve-receipt

One receipt producer (SCRAPI §2.4 "Resolve Receipt"; create-receipt is a kept alias). The source is chosen by the flags present, not a --mode; passing more than one source, or an under-specified one, errors with guidance.

# from tiles: rebuild the leaf→peak path from a massif and a checkpoint
forestrie resolve-receipt \
  --massif massif.log --checkpoint checkpoint.sth \
  --mmr-index 0 \
  --out receipt.cbor
resolve-receipt: massif     — index 0 (height 3, mmr indexes 0..3)
resolve-receipt: leaf       — mmrIndex 0 (from --mmr-index)
resolve-receipt: checkpoint — sealed size 4, 2 peak(s)
resolve-receipt: proof      — 1 node(s) to peak 1/2 (mmrIndex 2)
resolve-receipt: receipt    — 144 bytes -> receipt.cbor

Add --univocity <address> --log-id <id> --rpc-url $RPC_URL for a chain-anchored (report-only) verification instead of a checkpoint-based receipt.

Freshen a stale receipt (tile-free)

When log growth buries the peak a receipt commits to, freshen re-anchors it to the current sealed state without tiles. resolve-receipt --receipt <stale> plus a tile-free source extends the receipt's old inclusion path to the latest peak and re-emits it. The leaf value is recomputed exactly as verify does, from the same leaf inputs: --payload <statement> for statement receipts, or --committed-grant/--committed-grant-file for grant receipts (both with --entry-id).

# a STATEMENT receipt from a retained .sth chain (emitted under the chain's tail)
forestrie resolve-receipt \
  --receipt stale.cbor --checkpoint-chain ./checkpoints/ \
  --payload statement.cose --entry-id <hex> \
  --in-place

# a GRANT receipt, same chain source
forestrie resolve-receipt \
  --receipt stale.cbor --checkpoint-chain ./checkpoints/ \
  --committed-grant-file grant.cbor --entry-id <hex> \
  --in-place

# from on-chain publishCheckpoint calldata; calldata carries no peak receipts,
# so the latest .sth is supplied for emission, and --known-accumulator binds
# the folded state to a trusted snapshot
forestrie resolve-receipt \
  --receipt stale.cbor \
  --rpc-url $RPC_URL --univocity <address> --log-id <id> \
  --checkpoint latest.sth --known-accumulator accumulator.cbor \
  --committed-grant-file grant.cbor --entry-id <hex> \
  --out fresh.cbor

--in-place rewrites the --receipt file (mutually exclusive with --out; crash-safe — written to a sibling temp then atomically renamed). The freshened receipt is a native receipt that verifies with plain verify against the current state, and freshen fails closed (it never emits a receipt whose recomputed peak does not match the folded latest accumulator).

Bind the freshened state (--known-accumulator)

Both freshen sources fold a consistency-proof chain to the current accumulator; they differ only in the emission checkpoint. --checkpoint-chain emits under the chain's own tail (self-contained — reach for it by default). The calldata source borrows a separately-supplied latest --checkpoint, so pass --known-accumulator <snapshot> (a fetch-accumulator capture) to bind the folded state to a trusted chain read: freshen asserts it equals your snapshot at the same size and fails closed otherwise (this also catches a lying/stale RPC). --known-accumulator works with either source.

--known-accumulator anchors freshness — that the leaf roots into the genuine current log — which is a different question from who sealed the state or whether the log's authority chains back to genesis. Freshen never re-opens those; it re-anchors an already-issued receipt to the current accumulator. Which anchor proves what is the trust model: see the trust model.

decode-receipt

forestrie decode-receipt receipt.cbor
COSE_Sign1 — tagged 18 (COSE_Sign1) — 304 bytes
├─ protected: 1 (alg): -7 — ES256   4 (kid): 6c6c6c6c…
├─ unprotected: inclusion proof — mmr index 5, path length 3
├─ payload: detached — recomputed from the inclusion path at verify time
└─ signature: 64 bytes

verify

forestrie verify \
  --genesis genesis.cbor --receipt receipt.cbor --committed-grant "$GRANT_B64"
verify: parse     ok      — receipt COSE decodes; genesis trust root loads (ES256)
verify: signature ok      — checkpoint signature verifies under the genesis trust key
verify: inclusion ok      — proof path recomputes the checkpoint peak
verify: binding   ok      — leaf binds the grant commitment at the receipt idtimestamp
PASS: receipt verified offline against the cached checkpoint

Add --univocity <address> --log-id <id> --rpc-url $RPC_URL to check the receipt's peak against the on-chain accumulator instead of the checkpoint signature — no operator trust required.

Trust anchors (FOR-297)

verify takes four named trust anchors — pass whichever you hold:

  • --known-log-key — a caller-known log owner key (base64 x||y, env KNOWN_LOG_KEY); fully offline, checks the signature under a key you trust out of band (the SCITT / SSH-known-hosts posture).
  • --genesis — a cached genesis.cbor (the univocity-instance root registration). Roots a receipt whose signer chains to that root owner; for a child log deeper in the hierarchy, reaching genesis is the grant-chain walk (still open) or the on-chain path below.
  • --known-accumulator — a cached chain read (forestrie fetch-accumulator): verifies the peak against the on-chain accumulator, no operator trust and no signature needed. Never source it unauthenticated from the operator's tile store. Older receipts extend to a newer snapshot via --massif.
  • --rpc-url — a live chain read; same as --known-accumulator, current.

What each actually proves — and how forestrie separates split-view (is this a single, un-forked history) from sealing (who signed this state) from authority (does the log chain to genesis, via grants — not genesis.cbor) from attribution (who was authorised to sign this leaf) — is the trust model: see the trust model. The receipt never expires, and the anchor never needs to be current — only trusted.

Note that freshness is not one of the four. It is a weaker axis that bears only on coverage — whether a given accumulator snapshot reaches your leaf — and collapsing it into split-view is the specific confusion the trust model warns against. Split-view is the load-bearing property; currency is at most the --rpc-url "as of now" delta.

fetch-accumulator

forestrie fetch-accumulator \
  --univocity "$UNIVOCITY" --log-id "$LOG_ID" --rpc-url "$RPC_URL" \
  --out accumulator.cbor

Reads logState(logId) pinned to a block and writes the --known-accumulator snapshot: canonical CBOR binding (chainId, univocity, logId, size, blockNumber, blockHash) — anyone with RPC can re-run the read at that block and confirm or disprove it.

Use as a library: decode-receipt

The receipt decoder behind forestrie decode-receipt is exported as a subpath so other tools can render receipts without shelling out to the CLI or vendoring a copy:

import {
  decodeReceipt,
  renderReceipt,
  HEADER_LABELS,
} from "@forestrie/forestrie-cli/decode-receipt";

const decoded = decodeReceipt(receiptBytes);  // Uint8Array in
console.log(renderReceipt(decoded));          // the annotated tree
console.log(JSON.stringify(decoded, null, 2)); // the same model as --json

The entry point is pure: no node:*, no Bun.*, no I/O and no network, so it runs unchanged in Node, Bun, Deno, a worker or a browser. Its only dependencies are @forestrie/receipt-verify (the load-bearing receipt parse) and @forestrie/encoding. Ships with .d.ts.

Also exported: DecodeReceiptError (carries the failing stage), the display-model types (DecodedReceipt, DecodedHeaderEntry, DecodedClaim, Json), the label tables (HEADER_LABELS, ALG_NAMES, VDS_NAMES, CWT_CLAIM_NAMES, COSE_KEY_PARAM_NAMES, PROOF_KIND_NAMES, headerLabelInfo) and the minimal CBOR reader (decodeCborValue, decodeCborMap, isCborTagged). The bare @forestrie/forestrie-cli specifier re-exports the same surface; prefer the subpath.

Decoding is display only — it does not check signatures or inclusion. That is forestrie verify (and @forestrie/receipt-verify in-process).

Built on (published packages)

The binary is buildable from published packages only (plan-2607-12) — all public on npmjs under the @forestrie scope, MIT licensed per-package, installable without auth:

@forestrie/cli-kit · @forestrie/deploy-core · @forestrie/encoding · @forestrie/grant-builder · @forestrie/scrapi-client · @forestrie/receipt-verify · @forestrie/merklelog · @forestrie/delegation-cose · viem (deploy tx signing/submission)

License

MIT — see LICENSE.

Keywords

forestrie

FAQs

Package last updated on 21 Sep 2026

Related posts