tr-jwt-chain
v1.0.0
Published
Immutable JSON audit chain with cryptographically chained JWT
Maintainers
Readme
tr-jwt-chain
Immutable audit-chain helper built on cryptographically linked JWT records.
Each record is a signed compact JWT whose header contains the signature of the previous record as parent. The package can append records, resume from a previous head, and replay or validate an existing chain.
Reference
Installation
npm install tr-jwt-chainNode.js >=24.0.0 is required.
Export
const JwtChain = require('tr-jwt-chain');Constructor
const chain = new JwtChain({
alg: 'HS256',
key: 'secret',
kid: 'chain-key-1',
head: null,
omitSeqNo: false
});Configuration fields:
alg: required JWT signing algorithmkey: signing or verification keykid: required for non-empty HMAC keys and all asymmetric modeshead: optional previously issued token used to resume the chainomitSeqNo: optional boolean (defaultfalse). Whentrue, the chain does not emit aseqnofield in the token header. When resuming from aheadtoken, the sequencing mode is taken from that token's header — ifomitSeqNois also given explicitly and disagrees with what the head carries, the constructor throws.
Supported algorithms:
HS256,HS384,HS512ES256,ES384,ES512RS256,RS384,RS512ML-DSA-44,ML-DSA-65,ML-DSA-87
Key handling:
- HMAC mode accepts a raw string,
Buffer, typed array, oroctJWK - asymmetric mode accepts PEM,
KeyObject, or JWK - asymmetric mode becomes replay-only when only a public key is provided
initialize(extraProperties)
Creates the first chain record.
The payload starts with:
- random
id alg- optional
kid - optional public
keyJWK for asymmetric chains
Any extraProperties are merged into that initial payload.
record(data)
Appends a new record and returns the compact JWT string.
datamust be a JSON object- the token header includes
typ,alg,kid, optionalparent, and (unlessomitSeqNois set)seqno parentis set to the previous record signatureseqnostarts at0for the initialise token and increments by one for every subsequent record
replay(token)
Validates a token against the current chain state and advances the internal head.
Replay enforces both parent linkage and — when the chain is in
sequence-numbering mode — that the token's seqno is exactly
previous_seqno + 1. A chain with omitSeqNo: true rejects any token
that carries a seqno; a chain with omitSeqNo: false (default)
rejects tokens that omit it.
check(token)
Validates a token without advancing the internal head.
Use this for detached verification of a single record. check validates
the signature, typ, alg, and kid, but does not enforce parent
or seqno linkage — those checks belong to replay.
head()
Returns the current head signature.
Example
const JwtChain = require('tr-jwt-chain');
const chain = new JwtChain({
alg: 'HS256',
key: 'kukkuureset',
kid: 'hmac1'
});
const first = chain.initialize({ system: 'orders' });
const second = chain.record({ action: 'create', id: 1 });
const replay = new JwtChain({ alg: 'HS256', key: 'kukkuureset', kid: 'hmac1' });
console.log(replay.replay(first));
console.log(replay.replay(second));CLI tool — jwt-chain-tool
A small command-line driver is bundled for inspecting chain files
produced by this library. Install globally (npm i -g tr-jwt-chain)
to get the jwt-chain-tool binary on $PATH, or invoke it through
npx tr-jwt-chain jwt-chain-tool ... from within a project that has
tr-jwt-chain as a dependency.
jwt-chain-tool <command> [options] <chain-file> [target]Commands:
verify <chain-file>— replay the whole chain and exit 0 if every token verifies, every parent linkage holds, and (for seqno-enabled chains) everyseqnois exactly previous + 1. ReportsChain integrity verified. Chain length is N events.on success; exits non-zero with the offending line number on the first failure.extract <chain-file>— replay the chain and print every event payload, one JSON object per line.extract <chain-file> <seqno>— replay until the token whose header carries thatseqno, print the payload, and stop. The chain is not walked past the matched token. Exits non-zero if no such event exists, or if the chain is inomitSeqNomode.extract <chain-file> <event-id>— same, but matching by the third (signature) segment of the chain JWT. Use when sequence numbers are absent or unavailable.
A target made up entirely of digits is interpreted as a seqno;
otherwise it is taken as an event id. Pass --help for the full
syntax.
Options:
--event-chain-key=<jwk-file>— verification key for the chain. Accepts the public half of an asymmetric chain key or, for HMAC chains, the symmetricoctJWK. For asymmetric chains, the key's public part is checked against the public JWK embedded in the chain's root event; a mismatch aborts the run. For HMAC chains the option is required, since the symmetric key is not embedded. When omitted, the embedded public key from the root event is used.-v,--verbose— extra output. Forverify: print alg, key id, key type, sequencing mode, and the root event id. Forextract: prefix each emitted payload with# seqno=<n> id=<chain-id>.-p,--pretty— pretty-print emitted JSON (JSON.stringify(payload, null, 2)).
Exit codes: 0 on success, 1 on chain integrity failure / target
not found / bad key, 2 on argument errors.
Notes
- Tokens are compact JWT strings, but the payload semantics are chain-specific.
- Replay will fail if token metadata, signature, or parent linkage does not match.
- A chain resumed with
headcontinues from the referenced token signature.
Author
Timo J. Rinne [email protected] — https://github.com/rinne/
Copyright
Copyright © 2023–2026 Timo J. Rinne [email protected].
See COPYING for the full MIT license text.
License
MIT License
