tr-json-chain-tools
v0.5.0
Published
Command-line tools for tr-json-chain: a CLI to record into and inspect a hash-chained JSON event log, and a standalone CSV-export integrity verifier.
Maintainers
Readme
tr-json-chain-tools
Command-line tools for tr-json-chain
— an immutable, append-only, SHA-256 hash-chained JSON event log on PostgreSQL.
Four executables:
| command | what it does |
|---|---|
| tr-json-chain-cli | record events into a chain and inspect it (interactive or piped) |
| tr-json-chain-check | verify the integrity of a chain exported to CSV — standalone, no database |
| tr-json-chain-seal-keygen | generate a seal key pair (JWKs) to files |
| tr-json-chain-scheduler | a server that appends periodic timestamp and seal events to a chain |
Install
npm install -g tr-json-chain-toolsRequires Node.js ≥ 24 (the seal tooling uses tr-jwt / tr-jwk).
or run without installing:
npx -p tr-json-chain-tools tr-json-chain-cli postgres://… mychain
npx -p tr-json-chain-tools tr-json-chain-check export.csvA PostgreSQL to play with
A throwaway server is handy for trying things out. A compose file ships under
dc/:
docker compose -f dc/docker-compose.yml up -d
# → postgres on localhost:5433, user/password postgres/postgrestr-json-chain-cli
tr-json-chain-cli --pg-url <uri> [--namespace <ns>] [--init] [--verify]
[--seal-key <jwk> | --seal-key-file <path>]| option | env | description |
|---|---|---|
| --pg-url <uri> | OPT_PG_URL | PostgreSQL connection URI (required) |
| --namespace <ns> | OPT_NAMESPACE | chain namespace, so one database can hold several independent chains (<ns>_event_chain, …); omit for bare names |
| --init | OPT_INIT | create/initialize the chain if it does not exist (the CLI never creates a chain silently) |
| --verify | OPT_VERIFY | on startup, verify the entire chain server-side ({ verifyChain: true }) instead of only the root-event canary |
| --seal-key <jwk> | OPT_SEAL_KEY | a public seal JWK as inline JSON (see below) |
| --seal-key-file <path> | OPT_SEAL_KEY_FILE | a public seal JWK read from a file |
| -h, --help | | show help and exit |
Every option may be supplied via its environment variable instead of the flag
(booleans accept yes/no), which makes the tool convenient to drive from a
container or service manager. --help lists everything.
Initialization. The CLI no longer creates a chain implicitly. On an
uninitialized chain it exits with an error unless --init is given; with --init
it creates the schema and root event. An already-initialized chain is used as-is.
Seal key. --seal-key / --seal-key-file (mutually exclusive) supply a
public seal JWK — typically the seal-public.jwk from tr-json-chain-seal-keygen.
On --init it is embedded in the root event (sealKey) per the canonical-events
spec; on an already-initialized chain it is verified against the root (the
public key must match; kid/alg are checked when present). Any mismatch — or a
failed initialization — is printed and the CLI does not start.
Each input line is either:
a JSON object → recorded as an event; its
event_idis printed (hex); ora slash command:
| command | action | |---|---| |
/timestamp| record a{ "type": "ts", "ts": … }event | |/head| fetch the chain head (appends an empty checkpoint if needed) | |/root| print the chain's root event (id + stored data) | |/lc [<start> [<end>]]| list the chain (getEvents()slice indices; default = all) | |/cc| check chain integrity fully client-side (re-hash every event) | |/export <filename>| export the chain (minus genesis) to a semicolon CSV | |/help| list commands | |/exit| quit |
When stdin is a terminal it runs as an interactive line editor with command history (up/down) and TAB completion; with piped input it runs as a plain batch processor. A non-object line, malformed JSON, or unknown command prints an error and sets a non-zero exit code.
# interactive
tr-json-chain-cli --pg-url postgres://postgres:postgres@localhost:5433/postgres --namespace demo
# batch
printf '%s\n' '{"type":"user.login","user":42}' '/timestamp' '/cc' \
| tr-json-chain-cli --pg-url postgres://postgres:postgres@localhost:5433/postgres --namespace demo
# same, configured entirely from the environment
export OPT_PG_URL=postgres://postgres:postgres@localhost:5433/postgres OPT_NAMESPACE=demo
tr-json-chain-cliBy convention every event carries a top-level "type" discriminator (the
chain's own root and timestamp events do); it is not required.
CSV export format
/export <file> writes a semicolon-separated CSV, genesis row excluded, in
sequence order starting at #1:
#;event_id;parent_id;data_hash;data#— the event's sequence number (event_chain.id).event_id/parent_id/data_hash— lowercase hex.data— the canonical payload text (exactly the bytes that were hashed), RFC-4180 quoted; empty for events with no stored payload.
This is precisely what tr-json-chain-check consumes.
tr-json-chain-seal-keygen
Generates a seal key pair and writes the two JWKs to files: a public key
to embed in a chain's root (tr-json-chain-cli --seal-key) and a secret key
to sign seals with (tr-json-chain-scheduler --seal-secret-key).
tr-json-chain-seal-keygen [--alg <alg>] [--kid <kid>] [--secret <file>] [--public <file>] [--force]| option | env | description |
|---|---|---|
| --alg <alg> | OPT_ALG | ES256 (default), ES384, ES512, RS256, RS384, RS512 |
| --kid <kid> | OPT_KID | key id stored in both JWKs (default: a random UUID) |
| --secret <file> | OPT_SECRET | output path for the private JWK (default seal-secret.jwk, mode 0600) |
| --public <file> | OPT_PUBLIC | output path for the public JWK (default seal-public.jwk) |
| --force | OPT_FORCE | overwrite existing output files |
tr-json-chain-seal-keygen --alg ES256 --secret seal-secret.jwk --public seal-public.jwkThe secret file is written with mode 0600; existing files are not overwritten
without --force. The supported algorithms are a deliberate subset of the
canonical seal set — PS* and ML-DSA-* are not offered here.
tr-json-chain-scheduler
A long-running server that appends periodic ts and seal events to a chain.
Seals are signed (tr-jwt) with a private seal JWK whose public half must be
embedded in the chain root.
tr-json-chain-scheduler --pg-url <uri> [--namespace <ns>]
[--seal-secret-key <jwk> | --seal-secret-key-file <path>]
[--seal-interval <seconds>] [--timestamp-interval <seconds>]
[--init-chain] [--verbose]| option | env | description |
|---|---|---|
| --pg-url <uri> | OPT_PG_URL | PostgreSQL connection URI (required) |
| --namespace <ns> | OPT_NAMESPACE | chain namespace; omit for bare names |
| --seal-secret-key <jwk> | OPT_SEAL_SECRET_KEY | a private seal JWK as inline JSON |
| --seal-secret-key-file <path> | OPT_SEAL_SECRET_KEY_FILE | a private seal JWK read from a file |
| --seal-interval <s> | OPT_SEAL_INTERVAL | seconds between seals (requires a seal secret key) |
| --timestamp-interval <s> | OPT_TIMESTAMP_INTERVAL | seconds between timestamp events |
| --init-chain | OPT_INIT_CHAIN | create the chain if absent (else exit with an error) |
| --verbose | OPT_VERBOSE | print the data of each event recorded |
At least one of --seal-interval / --timestamp-interval must be given. On
startup the server applies the same root-key check as the CLI when a seal key is
supplied (public key must match; kid/alg if present); a mismatch — or an
uninitialized chain without --init-chain — is fatal. It records events until it
receives SIGINT/SIGTERM, then drains and exits cleanly.
# heartbeat every minute, seal every hour
tr-json-chain-scheduler --pg-url postgres://postgres:postgres@localhost:5433/postgres \
--namespace demo --seal-secret-key-file seal-secret.jwk \
--timestamp-interval 60 --seal-interval 3600 --verboseThe seal event embeds a signed JWT whose chain-op: "seal" marker is a payload
claim (per tr-json-chain ≥ 1.0.2); the JWT names the sealed event_id and the
chain identity, attesting that the chain reached that point under the seal key.
tr-json-chain-check
tr-json-chain-check <file.csv>A standalone, self-contained verifier (only Node builtins — no database, no
tr-json-chain dependency). It re-derives every hash from the spec, so it also
serves as a compact reference implementation of the chain's integrity rules:
data_hash == SHA256(utf8(data))for every row whose payload is present;event_id == SHA256(parent_id ‖ data_hash)for every row;- each
parent_idequals the previous row'sevent_id, and#is contiguous.
It accepts a partial chain (a contiguous slice not starting at the root):
the first row then has # ≠ 1 and a non-zero parent_id, and the summary reads
Partial chain OK instead of Chain OK.
The data column is optional: if absent, no event carries a payload; an empty
cell means that event's payload is unavailable.
$ tr-json-chain-check export.csv
Chain OK: 1502 events verified, payload 1501/1501 (100.0%) present, in 12.3 msThe n/m figure is payloads-present over events that have a real (non-zero)
data_hash — head/checkpoint placeholder events are excluded from the
denominator. Exit code is 0 on success, 1 on an integrity failure (printed
as INVALID: <reason>), 2 on a usage/IO error.
See also
tr-json-chain— the library these tools operate on, including the full hash specification and the language-agnostic verification algorithm.
Author
Timo J. Rinne [email protected]
License
MIT
