# Changelog
Notable changes to the published crates. Generated from conventional commits by
[git-cliff](https://git-cliff.org) when a release is cut — do not edit by hand.
## [0.2.2](https://github.com/OpenVTC/verifiable-trust-infrastructure/compare/vta-keyspaces-v0.2.1...vta-keyspaces-v0.2.2) — 2026-08-28
## [0.2.1](https://github.com/OpenVTC/verifiable-trust-infrastructure/compare/vta-keyspaces-v0.2.0...vta-keyspaces-v0.2.1) — 2026-08-26
### Added
- **app-state**: A third store for versioned, namespaced application state ([#1051](https://github.com/OpenVTC/verifiable-trust-infrastructure/pull/1051))
Applications built on a VTA have had nowhere to keep versioned metadata.
Adds `vta/app-state/{get,put,list,delete,get-many,put-many}/1.0` — a store
beside the secrets vault and the credential vault, for JSON an application
owns and the VTA does not interpret.
Records are addressed `(contextId, namespace, key)`. The namespace scopes one
application so several tools can share a context without colliding, and is the
seam a per-namespace grant would later use — which is why it is part of the
address rather than a prefix convention on the key. In 1.0 a namespace is
collision avoidance and NOT a trust boundary: an application with write access
to a context reaches every namespace in it, and the `put` and `delete` specs
say so normatively. Isolation means separate contexts.
Deliberately not built on `vta/memory/*`. `MemoryItem` is `{key, value}` with
nothing to hang a precondition on, and its `list` returns the whole context —
but the argument that settles it is that "forget everything" has to stay a safe
thing to ask an agent, which it cannot be if account state lives there.
Three properties are why this is a store rather than a field on an existing one.
**One counter per `(contextId, namespace)`, not per record.** A record's
`version` is the counter value its most recent write took, so one number is
simultaneously the optimistic-concurrency token `expectedVersion` compares
against and the watermark `sinceVersion` compares against. A per-record counter
serves the first but cannot serve the second — two records' counters are not
comparable, so no single number means "everything after this point" — and would
have forced a second sequence kept consistent by hand. The cost is that a
record's version jumps by whatever its neighbours consumed, which the wire
contract states: versions are opaque and monotonic, never an edit count.
**A failed precondition returns the current version AND value.** A bare
rejection obliges a re-read, and the re-read races the next write; the pattern
has no fixed point under contention. Returning the winner's view removes the
race rather than narrowing it, and the spec makes it normative.
**Delete leaves a versioned tombstone, and the tombstones are reaped.** Without
one, a consumer pulling from a watermark learns of every create and update and
never of a deletion, so deleted records resurrect on its next rebuild.
Retention is `app_state.tombstone_retention_days` (default 30, matching the
vault's `grace_days`) — a destructive window is an operator's choice, not a
constant — and `list` advertises the configured value, since a consumer
schedules against that number. The sweeper runs from the storage thread beside
the ACL/consent/vault sweepers.
The sweeper reaps a *prefix*, not a set: each namespace walks its tombstones in
version order and stops at the first still inside the window. Reaping a later
tombstone while leaving an earlier one would make the reap watermark
unstateable — no single number would describe what survives, which is precisely
what `watermarkTooOld` has to be able to say. `0` days disables reaping, and
that is enforced at the call site rather than as a zero cutoff, which would mean
the opposite.
Version reservation is fsynced and re-seals the TEE integrity manifest, for the
reason `vti_common::store::counter` gives for BIP-32 counters: a counter
surviving only in the journal buffer can be re-derived after a crash and reissue
a used value. Here a reused version means two records collide on one `appv:`
index key, so one disappears from the change feed and every incremental consumer
misses that change permanently, silently. A batch reserves a block and pays one
fsync rather than N; writes that then fail leave gaps, which are safe and
tested.
Retry safety: reads are `ReadOnly`, `delete` is `RetrySafe` (a second delete
finds a tombstone and deliberately takes no new version, so a watcher sees
nothing), and `put`/`put-many` are `Keyed` — a `put` without `expectedVersion`
does not converge, and the class is per URI, not per payload.
Blobs are deliberately out of scope in 1.0; adding a `blobRef` is additive.
Concurrency is a process-local lock per namespace, not a store-layer
compare-and-swap. fjall takes an exclusive database lock so two processes cannot
share a store, and the vsock protocol has no atomic opcode — its
`insert_if_absent`/`swap` are already non-atomic fallbacks. A CAS today would be
atomic exactly where the lock suffices and a warn-and-fallback exactly where it
would need to be real. Recorded in the design note with what would change that.
Schemas published upstream as trustoverip/dtgwg-trust-tasks-tf#252 and #253;
this depends on the released trust-tasks-rs 0.11.2, pinned to a minimum patch so
an older resolve fails as a stale dependency rather than as unspecced URIs.
Conformance witnesses cover all six URIs, so nothing enters
`UNSPECCED_DISPATCHED_URIS`.
## [0.2.0](https://github.com/OpenVTC/verifiable-trust-infrastructure/compare/vta-keyspaces-v0.1.5...vta-keyspaces-v0.2.0) — 2026-08-20
### Added
- **vta**: Dedup keyed Trust Tasks on an idempotency key ([#1011](https://github.com/OpenVTC/verifiable-trust-infrastructure/pull/1011))
A client that retries a timed-out request is doing the right thing. The
dangerous case is the one where the VTA processed it and only the reply
was lost, because the retry then produces a second durable effect —
`webvh/dids/create` being the sharp example, where auto-assigned paths
mean the retry mints a *different* DID and the first stays published
with nobody holding a reference to it.
The existing `trust_tasks::replay` layer cannot catch that. It keys on
`(actor, envelope-id)` and every SDK path mints a fresh `urn:uuid:` per
attempt, so a genuine retry sails past it. Its own module docs name this
work as the deliberate follow-up.
## Built on the store that was already here
## [0.1.5](https://github.com/OpenVTC/verifiable-trust-infrastructure/compare/vta-keyspaces-v0.1.4...vta-keyspaces-v0.1.5) — 2026-08-18
## [0.1.4](https://github.com/OpenVTC/verifiable-trust-infrastructure/compare/vta-keyspaces-v0.1.3...vta-keyspaces-v0.1.4) — 2026-08-17
### Added
- **vta-keys**: Add non-extractable internal signing keys ([#995](https://github.com/OpenVTC/verifiable-trust-infrastructure/pull/995))
An ordinary VTA key is BIP-32 derived, so anyone holding the 24-word mnemonic
can reconstruct it offline. That is what makes the VTA recoverable, and equally
what makes "the operator cannot obtain this key" false — the second limb of what
eIDAS calls sole control.
An internal key is generated from the system CSPRNG, has no derivation path, and
is never returned by any surface. The VTA acts only as a signing oracle for it.
Deliberately not a flag on the imported-key path. That path wraps its secrets
under a KEK derived from the master seed (derive_kek(seed, salt)), so a
non-extractable flag on it would be decorative: the boundary it claims to
enforce has already been walked around. Internal keys get their own keyspace,
INTERNAL_KEYS, with no seed involvement at any point, and that keyspace is in
EXCLUDED_FROM_BACKUP by design — a backup carrying it would be an export of keys
the VTA promises never to export, and restoring it elsewhere would clone a
signer.
Refused for did:webvh log entries, enforced in code rather than left to
guidance. WebVH is append-only and each entry is authorised by the update key
the previous entry named; an unrecoverable update key means that if storage is
lost the DID can never be updated again by anyone, permanently, and every
integration pinned to it is stranded. Credentials can be re-issued, an
append-only identity log cannot. Internal keys remain fine as a signing
verificationMethod inside a published document, where loss costs the ability to
produce new signatures rather than control of the identity.
The export refusal is not a permission check — admin is not a bypass, because
the value of the origin is that no caller holds this power. There are two
refusals (an early return and an in-match arm); removing either leaves the other,
and removing both does not compile, since the match over KeyOrigin becomes
non-exhaustive. An export path cannot silently reopen.
Operator surfaces carry the cost prominently: `pnm keys create --internal`
prints what is lost and requires the operator to type a confirmation phrase
rather than mash y, the response repeats the warning, and docs/02-vta/
internal-keys.md covers when to use one, what actually protects it (enclave
measurement + KMS, not a mnemonic), and the two things that genuinely destroy
it.
## [0.1.3](https://github.com/OpenVTC/verifiable-trust-infrastructure/compare/vta-keyspaces-v0.1.2...vta-keyspaces-v0.1.3) — 2026-08-16
## [0.1.2](https://github.com/OpenVTC/verifiable-trust-infrastructure/compare/vta-keyspaces-v0.1.1...vta-keyspaces-v0.1.2) — 2026-08-13
### Added
- **release**: Publish vta-service and its closure again ([#962](https://github.com/OpenVTC/verifiable-trust-infrastructure/pull/962))
* feat(release): publish vta-service and its closure again
#938 unpublished `vta-service` and the twelve subsystem crates behind it,
on the finding that nothing external depended on them. The audit read
normal dependencies. `openvtc-core` depends on `vta-service` as a
**dev-dependency**, for `test_support::MockVta` — an in-process VTA its
end-to-end tests run against. That harness boots the real service, so no
client crate can stand in for it.
Unpublishing did not merely freeze the crate. It broke it.
`vti-common` re-exports `vta_sdk::acl::{ActScope, ApproveScope,
ContextDirection}` as its own public API, so **a re-export makes the
re-exported crate's version part of your public API**: any graph
combining `vti-common` with another `vta-sdk` consumer must resolve one
`vta-sdk`. The frozen `vta-service` 0.14.37 asks for `vta-sdk ^0.21`
while `vti-common` has moved to `^0.23`. A downstream `cargo update`
resolves both and `vta-service` fails to compile with
expected `vti_common::acl::ApproveScope`,
found `vta_sdk::acl::ApproveScope`
at ten call sites — which is how this surfaced, in openvtc #213. Nothing
downstream can fix that; only a release that moves the requirements
together can.
So the thirteen manifests go back to the workspace default. The cost is
the closure — twelve subsystem crates return to crates.io, which is
exactly what #938 set out to stop. Taken deliberately over the
alternatives: yanking the published copies breaks OpenVTC's tests with no
replacement, and leaving them up ships a crate on the registry that
cannot be built.
**On release ordering.** `cargo publish --dry-run -p vta-service` fails
today, and will until the closure is on the registry: packaging strips
path deps, so `vta-keys = "0.2"` resolves the *published* 0.2.1, which
still asks for `vta-sdk ^0.21` — two nodes, same error. That resolves
itself in the release, which publishes in dependency order: every
subsystem crate in this workspace already requires `vta-sdk = "0.23"`, so
once they upload, `vta-service` verifies against them. Crates whose
dependencies are all published already dry-run clean (verified on
`vta-keyspaces` and `vta-config`).
Docs updated to match: CLAUDE.md, RELEASING.md and the release-plz.toml
header all said 7-of-21. They now say 20-of-26, name the six that stay
internal, and record the rule the audit missed — check dev-dependencies,
in sibling repos, before unpublishing anything.
### Build & CI
- **release**: Adopt release-plz, publish 7 crates instead of 21 ([#938](https://github.com/OpenVTC/verifiable-trust-infrastructure/pull/938))
Merging and releasing were the same act. publish.yml fired on every push to
main and shipped whatever versions were newly present, and a CI guard required
the version bump to live in the feature PR — so every PR was a release
decision, taken by whoever opened it, days before it merged. Two open PRs
touching one crate wrote the same number into the same line of the same
Cargo.toml, and the second to merge had to rebase, renumber, and fix a
changelog entry that had gone stale. #932/#936/#937 hit it three times in one
afternoon.