All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
This release adds hex-encoding decorators for hashers and signer/verifiers.
New constructors wrap an existing Hasher or SignerVerifier so the stored
payload is lower-case hex while Name() is passed through unchanged,
preserving the on-disk key layout.
- hasher, crypto: hex-encoding decorators that wrap an existing
HasherorSignerVerifierso the stored payload is lower-case hex whileName()is passed through unchanged, preserving the on-disk key layout. New constructors:hasher.NewHexHasher,hasher.NewHexSHA256Hasher,hasher.NewHexSHA1Hasher,crypto.NewHexSignerVerifier,crypto.NewHexVerifier,crypto.NewHexRSAPSSSignerVerifier, andcrypto.NewHexRSAPSSVerifier. The raw constructors are unchanged (#114).
This release introduces a new locker package providing distributed locking
across all drivers: Driver and Storage gain a NewLocker method (with
etcd, TCS, and in-memory dummy implementations), plus locker.Factory,
Prefixed, Do, and Done helpers. It also adds key-prefix support to
namer.LayeredNamer and integrity.CodecBuilder (WithKeyPrefix) so
multiple codecs can share one storage in disjoint sub-trees for atomic
commits, along with new ValueKey/FullKeys accessors on integrity codecs
and stores. Includes fixes for name validation, empty-value hashing, and
etcd connections for non-admin users.
- namer.LayeredNamer:
WithKeyPrefix(prefix)option prepends a fixed path prefix to every emitted/parsed key. Lets two codecs sit in disjoint sub-trees of a singlestorage.Storageso they can be committed atomically in oneintegrity.Tx(which cannot span multiple storage handles, the waystorage.Prefixedwrappers can). Validation matchesstorage.Prefixed: must start with/, must not end with/; an empty prefix is a no-op. Returnsnamer.ErrKeyPrefixNoLeadingSlash/namer.ErrKeyPrefixTrailingSlashon malformed input;ParseKeyreturnsnamer.ErrKeyPrefixMissingfor keys that do not start with the configured prefix (#101). - integrity.CodecBuilder:
WithKeyPrefix(prefix)threads the namer option through to the underlyingnamer.NewLayeredNamer, so codecs can be built directly with a prefix without writing a customCodecNamerConstructor(#101). locker.Do(ctx, f, name, fn, opts...): helper that creates a Locker via the suppliedFactory, acquires it, runsfnwhile the lock is held, and releases the lock on return.fn's error leads any joined Unlock error soerrors.Isagainst domain errors works without peeling off lock-machinery wrappers; Unlock always runs afterfnregardless offn's result (#104).locker.Prefixed(prefix, inner): Factory-level name-scoping helper that concatenatesprefixto every caller-supplied lock name before delegating toinner, mirroringstorage.Prefixed's rewrite convention. Lets a component receive a name-scopedlocker.Factorywithout seeing the fullStorage; an empty prefix is a transparent passthrough, and a non-empty prefix must start with/(ErrPrefixNoLeadingSlash) and not end with/(ErrPrefixTrailingSlash) (#104).- integrity.Codec:
ValueKey(name)returns the namer-relative value-layer key;FullKeys(name)returns every key the codec's namer would emit (value + hashes + signatures), in namer-emitted order. Both reject empty, leading-slash, and trailing-slash names withErrInvalidName, matching the rule applied byPut/Delete/Get/Watch(#102). - integrity.Store:
ValueKey(name)andFullKeys(name)mirror the codec methods but return on-disk keys — when the storage is wrapped withstorage.Prefixed, the wrapper's prefix is prepended (#102). - integrity.SingletonStore: no-arg
ValueKey()andFullKeys()delegate to the innerStorewith the bound name (#102). - storage.Prefixer: optional interface implemented by
Prefixedstorages to expose their key prefix.storage.StoragePrefix(s)returns the prefix ornilfor storages that are not wrapped (#102). - test_helpers/etcd: new public helper package built on
go.etcd.io/etcd/server/v3/embed, replacing the oldinternal/testing/etcdhelper that pulled in conflictinggenprotoandgrpc-middlewareversions and broke module-mode builds. ExposesNew,Cluster,ClusterConfig,EndpointsGRPC,EndpointsHTTP,Terminate, and aLazyClusterfor sharing one embedded cluster across a test suite (#105).
driver.Driverandstorage.Storageinterfaces gained aNewLocker(ctx, name, opts...) (locker.Locker, error)method backed by a newlockerpackage. Minor breaking change for out-of-tree implementers of either interface. The dummy driver ships an in-memory implementation; the etcd driver wiresconcurrency.Mutexand supportsNewLockeronly when theClientpassed toetcd.Newis a concrete*etcd.Client(theconcurrencypackage needs the concrete type which theClientinterface does not expose); otherwiseNewLockerreturnslocker.ErrUnsupported. The TCS driver layers a "smallest mod_revision wins" protocol overconfig.storage.put/keepalive/get/deleteand requires a TCS schema with bothfeatures.ttlandfeatures.keepalive. TheLockerinterface also gained aDone() <-chan struct{}method that closes when the lock is no longer held — either viaUnlockor because the backend session was lost (TTL elapsed without renewal, connection dropped, etc.); callingDonebefore any successful acquire returns an already-closed channel. Minor breaking change for out-of-treeLockerimplementers (same flavor as the originalNewLockeraddition) (#103, #98, #99, #100, #104).locker.FactoryandStorage.LockerFactory(): a lightweight "create a lock" surface for components that do not need the fullStorageinterface.Prefixedimplements it so factory-issued lockers keep the prefix-rewrite path that scopes lock names under the wrapper's namespace. Breaking change:locker.Factoryis now an interface with aNewLocker(ctx, name, opts...) (Locker, error)method instead of a function type.Storageand thePrefixedwrapper already satisfy it via theirNewLockermethod; adapt a bare function with the newlocker.FactoryFunc(mirrorshttp.HandlerFunc).locker.Prefixedcomposition is now flattened at construction and is outer-first —locker.Prefixed("/a", locker.Prefixed("/b", inner))is equivalent tolocker.Prefixed("/a/b", inner), matchingstorage.Prefixed(#104).
- integrity.Codec.BindPredicate now rejects empty, leading-slash, and
trailing-slash names with
ErrInvalidName, matchingPut/Delete/Get/Watch. Without it, a leading slash was silently stripped by the namer and aliased"/foo"to"foo", letting a predicate match a row no sibling call could see (#102). - hasher: nil and empty input now hash to the empty-string digest instead of
failing with "data is nil". Storage backends round-trip empty stored values
as nil, so reading a legitimately-empty value previously exploded in the
SHA-256 hasher and the RSA-PSS verifier, forcing
IgnoreVerificationError()as the only workaround. The internal-onlyErrDataIsNilis removed (#107). - connect.NewEtcdStorage failed if user has no admin permission (#106).
This release renames the namer's hash key path marker from hash to
hashes, adds tx.Factory for handing transaction-begin capability
to components without exposing the full Storage interface,
introduces a LegacyHashSigLayout namer option for compatibility with
the legacy product layout, tightens storage.Prefixed to require a
leading /, and makes the connect package accept scheme-prefixed
endpoint URLs and probe every configured endpoint before failing.
-
tx.Factory and
Storage.TxFactory(): a lightweight "begin a transaction" surface for components that do not need the fullStorageinterface.Prefixedimplements it so factory-issued transactions keep the prefix-rewrite path. -
storage.Prefixednow rejects a non-empty prefix that does not start with/, returning the newstorage.ErrPrefixNoLeadingSlash. Interior/separators remain allowed; a trailing/is still rejected withstorage.ErrPrefixTrailingSlash. -
namer.LayeredNamer:
LegacyHashSigLayout()option emits hash and signature keys without the per-codecobjectLocationsegment (value keys keep it) to match the layout produced by the legacy product:/<objectLocation>/<name> /hashes/<hashLocation>/<name> /sig/<sigLocation>/<name>Composes with
CompactSingleHash/CompactSingleSigand is a no-op in unnamed mode.
- BREAKING: namer: hash key path marker changed from
hashtohashes. The default layout is now/<objectLocation>/hashes/<hashLocation>/<name>(default namer) and/hashes/<hashLocation>/<objectLocation>/<name>(layered namer); the unnamed-layered and compact-hash variants follow the same rename. Existing keys written under/hash/...will not be parsed by the new namer — operators upgrading must migrate stored keys to the new prefix. The reserved-marker check onobjectLocationand the unnamed-mode reserved-first-segment check now rejecthashesinstead ofhash.
- connect: TCS connection failure when an endpoint URL contained
http://orhttps://scheme. Endpoints are now normalized by stripping the scheme prefix; etcd endpoints usehttp://orhttps://based onSSL.Enable. - connect: a configured endpoint list is now probed in order — the
first reachable endpoint wins and the client/pool is returned.
Previously only
Endpoints[0]was probed, so the connection failed if the first endpoint was down even when later ones were healthy. When every endpoint fails, a single joined error is returned.
This release adds an unnamed codec layout and a SingletonStore[T] for
single-key configuration objects, switches watch to a signal-only
Event.Prefix contract, makes storage.Prefixed return an error, and
fixes several cases where Range and Watch dropped every result under
integrity.
- integrity.SingletonStore[T] and
Codec[T].BindSingleton: bind a codec to one fixed key at construction time, then callGet/Put/Delete/Watch(and theTx*variants) without passing a name on every call. Suited for configuration objects that live at a single known key (e.g./settings/auth) rather than under a directory of<objectLocation>/<id>items; the on-disk layout matchesStore[T]for the same name. - integrity, namer: unnamed codec layout. A
CodecBuilderwithoutWithObjectLocation, orNewLayeredNamerwith the newObjectLocationMissingsentinel, emits keys as/<name>,/hash/<hashLocation>/<name>,/sig/<sigLocation>/<name>— the per-codec location segment is dropped. Object names whose first slash-separated segment ishashorsigare rejected to avoid colliding with the category markers. - namer.LayeredNamer:
objectLocationmay now be a multi-segment path (e.g."settings/ldap"); the reservedhash/sigmarker check applies only to the first segment. - namer.Namer: new
Prefixes(val, isPrefix) []stringmethod returning one range prefix per key category (value, hash, sig).DefaultNamerandLayeredNamerimplement it; third-partyNamerimplementations must add the method.
- integrity.CodecBuilder: omitting
WithObjectLocationno longer falls back to"objects"; it now produces an unnamed codec (keys at/<name>instead of/objects/<name>). Callers who relied on the silent default must add.WithObjectLocation("objects")explicitly. This is a breaking change. - storage.Prefixed: signature is now
Prefixed(prefix, inner) (Storage, error). A non-empty prefix ending with/is rejected withErrPrefixTrailingSlash; an empty prefix still yields a transparent wrapper. This is a breaking change — callers must handle the returned error. - watch: all drivers (etcd, dummy, tcs) now follow a signal-only
Event.Prefixcontract. Every driver emits the watched key with any trailing/stripped — a signal that something at or under the watched key changed, not the per-event changed key. Consumers that need the changed key must follow up withRange. The tcs per-watcher buffer is bumped to 16 with a blocking, ctx-aware send so server bursts no longer drop on a full channel.
- integrity:
Store[T].Range(ctx, "")andTyped[T].Range(ctx, "")returned an empty slice as soon as a hasher or signer/verifier was configured, because the empty-name branch fetched only the value-layer prefix and the validator then dropped every result as missing its hash/signature. Both methods now fan out across every category prefix. - integrity: prefix-shaped
Watchcalls (e.g.Watch(ctx, "acl/")) and whole-codec watches (Watch(ctx, "")) silently dropped every event. Trailing slashes are now normalised on both the driver and integrity sides, and whole-codec watches forward driver events as-is. - connect:
NewEtcdStoragecould spin in an infinite loop with bad endpoints;NewTCSStoragenow validates credentials.
This release introduces a new transaction-aware integrity API
(Codec[T], Store[T], and Tx), a Prefixed storage wrapper for
namespace scoping, a LayeredNamer with per-category key layout, and
additional typed marshallers. It also bumps dependencies to address
vulnerabilities reported by govulncheck.
- storage.Prefixed: wrapper that scopes every storage operation, predicate, Range, and Watch call under a given namespace prefix. Nested wrappers are flattened automatically (#67).
- namer.LayeredNamer: namespace-agnostic namer that places each key
category under its own top-level location segment
(
/<objectLocation>/<name>,/hash/<hashLocation>/...,/sig/<sigLocation>/...). Segments are validated at construction andParseKeyparses keys back to(name, KeyType, property)unambiguously (#68). - integrity: new schema-driven API for integrity-protected storage.
Codec[T]describes the value layout independently of any storage handle and is built via the fluentCodecBuilder[T], which validates location-override keys eagerly so typos likeWithHashLocation("sah256", …)are no longer silently ignored.Store[T]binds a codec to a storage handle and exposesGet/Put/Delete/Range/Watch. Conditional multi-key updates are available throughTxwithBranchand typed futures; multi-codec transactions are lowered to a single storage call (#69, #70, #71). - marshaller:
TypedJSONMarshaller[T]forencoding/json-based marshalling andTypedBytesMarshallerpassthrough for values already stored as opaque bytes (#73).
- Bumped
google.golang.org/grpctov1.79.3(GO-2026-4762: authorization bypass via missing leading slash in:path) andgo.opentelemetry.io/otel/sdktov1.40.0(GO-2026-4394: arbitrary code execution via PATH hijacking) (#75).
This release introduces a new connect package for building Storage
instances from configuration, fixes a goroutine leak in storage.Watch,
and improves TCS predicate compatibility with etcd's absence/presence
idioms.
- connect: Added a convenience package to create
Storageinstances from configuration for etcd and Tarantool Config Storage backends. TLS support for TCS is available via thego_storage_sslbuild tag (requires CGO) (#55).
- driver.etcd: etcd.New now accepts an interface that is compatible with
*etcdclientv3.Clientinstead of the concrete type, which should not cause any issues when upgrading the version.
- storage.Watch: prevent goroutine leak when the consumer stops reading from the event channel before the watch is cancelled.
- driver.tcs: predicates of the form
VersionEqual(key, 0)andVersionNotEqual(key, 0)are now transparently rewritten to TCS'scount == 0/count != 0predicates on the wire. Previously these errored against TCS becausemod_revisionis undefined for absent keys. This brings parity with etcd's canonical absence/presence idioms (#61).
This release fixes an issue where Range returned empty results for names with a trailing slash.
- integrity.Typed: Range returns empty results when name has trailing slash.
This release updates the TypedBuilder to use a generic marshaller interface for more flexibility.
- integrity.Typed: TypedBuilder uses generic TypedMarshaller interface instead of TypedYamlMarshaller (#51).
This release adds prefix deletion, predicates support for integrity.Typed, and a dummy driver for testing purposes.
- integrity.Validator: Ability to get ModRevision from a validated result (#23).
- integrity: Integration tests have been implemented (#30).
- integrity.Typed: The possibility of prefix deletion (#24).
- integrity.Typed: Ability to use predicates (#25).
- driver: Added dummy driver implementation (#47).
- namer.Namer: Fixed a bug where a double slash was placed at the end of a prefix (#41).
- integrity.Typed: Fixed an ability to use Range with verification using non-empty names (#26).
The release introduces the initial version of the library.
- storage.Storage: Middle-level interface with Watch, Tx, and Range operations.
- tx.Tx: Conditional transaction execution with predicates and operations.
- operation.Operation: Get, Put, Delete operations with typed interfaces.
- predicate.Predicate: Value and version comparisons for conditional logic.
- watch.Event: Real-time change monitoring with prefix support.
- driver.tcs: Tarantool Config Storage backend implementation with transaction support.
- driver.etcd: Basic etcd backend implementation with conditional transactions.
- hasher: SHA1 and SHA256 hash implementations.
- integrity: Signer and verifier for data integrity checking.
- namer.Namer: Key naming and metadata management.
- integrity.Typed: High-level interface for integrity-protected storage operations with Get, Put, Delete, Range, and Watch methods.