Changelog
All notable changes to Tayra are recorded here, newest first. Tayra follows Semantic Versioning: major versions may break the public API, minor versions add backward-compatible features, and patch versions are bug fixes only.
The framework major lives in the package name (Tayra.Marten8 / Tayra.Marten9, Tayra.Wolverine5 / Tayra.Wolverine6); the version below is Tayra's own suite version, shared across every package. See Installation.
2.6.0 - 2026-08-10
Upgrading from 2.5.0
See the upgrade guide. Licensing only - nothing to migrate, and every change removes a way to be locked out of your own data.
Fixed
Tayra.Marten9threwMissingMethodExceptionon every document read against Marten 9.20 and newer. Marten moved most ofISerializerontoWeasel.Storage.IStorageSerializer(whichMarten.ISerializernow inherits) during the 9.x line. Source compiles against either version, so nothing warned at build or restore time, butTayraSerializerwas compiled against the older assembly and its calls named members that no longer existed where it looked for them. Writes appeared to succeed - Marten's write path happens to use one of the four members that did not move - so the failure showed up on the first query.TayraSerializernow resolves every member it delegates to at runtime, against whichever interface the running Marten declares it on, so one package serves the whole 9.x line. It also implementsWriteToParameter(DbParameter, object), a member the newer contract added and Tayra did not have, which was a hole a write could have passed through unencrypted. If a future Marten changes the contract again, Tayra now fails at startup naming the member instead of at first read.Reported as issue #15 against 2.5.0 on Marten 9.22.5.
Tayra.Marten8is unaffected: Marten 8's serializer contract is identical from 8.35.0 through 8.37.4.
Changed
Decryption is no longer gated by the license under any circumstances. The runtime licensing gate now sits on the encryption path only.
DecryptAsyncperforms no license check of any kind, and there is no longer a code path anywhere in Tayra that can refuse a decryption for licensing reasons - not a lapsed term, not an expired trial, not a clock. Previously an expired trial key stopped decryption as well as encryption; production keys were already unconditional.An expired trial key now starts the host and keeps decrypting. Startup validation now fails only when the key is absent, tampered with, unverifiable, or malformed - the cases where the key is not genuine. A genuine key always starts, however far past its term, because a process that refuses to boot cannot decrypt either. An expired trial is refused per-call on
EncryptAsyncinstead, with an error that says decryption is unaffected. Anything you encrypted while evaluating stays readable.If you relied on an expired trial halting an application at startup, that no longer happens; check
LicenseChecker.IsLicensedexplicitly if you want that behaviour.
Added
LicenseChecker.IsAuthenticandLicenseValidationResult.IsAuthentic- whether the key itself is genuine (signature verified, payload well-formed), as distinct fromIsLicensed, which additionally means "usable for encrypting new data".IsAuthenticis the startup gate;IsLicensedgoverns encryption and the edition-gated Compliance features.LicenseValidationResult.Expired(license, error)- the result for a genuine key whose term has run out, carrying the parsedLicenseInfoso callers can report which key and which date.
Removed
LicenseChecker.EnsureCanDecrypt(), added during the 2.x licensing work and never part of a release, is gone rather than deprecated: a method that answers "may I decrypt?" is exactly the thing that must not exist.EnsureCanEncrypt()is unchanged and remains the only runtime gate.
Documentation
Wire Format Specification - the normative, byte-exact description of everything Tayra writes, published so that encrypted data can be recovered with nothing but your key store and any AES-256-GCM implementation: no Tayra assemblies, no license key, no involvement from Radarleaf. Covers the
0x03and0x04payload layouts, the associated-data construction (the part a hand-written decryptor cannot guess), theTAYRA_M:masking prefix, key id derivation and rotation suffixes, the key store table layout, and the0xE1envelope format for wrapped DEKs. Decryption only; it is not a specification for producing ciphertext.Reference decryptors in .NET and Python, neither of which references any Tayra package. The .NET one uses only the base class library, the Python one only the
cryptographypackage, and both are maintained as single runnable files that decrypt the published test vectors and report. The spec page also publishes those vectors inline: known-good ciphertext with its keys and expected plaintexts, so a reimplementation in any language can be validated against the same cases. Both implementations are wired into the docs as snippets rather than copied, and the snippet plugin now scans.pysources as well as.cs.WireFormatSpecTestsguards the specification itself: the documented byte offsets, the associated-data string, the0xE1envelope layout, the published vectors, and a run of the .NET recovery script as a customer would invoke it. CI runs the Python script the same way, so both published recovery paths are enforced. A change that would invalidate the published document now fails the build naming the part that moved.The wire format diagram on Encryption showed the
0x01and0x02layout while the text described0x03as what field encryption writes, and the stated 31-byte overhead was given as 29. Both corrected.LICENSE §1(a), §1(d), §1(e) and the contract templates now state that no license check runs on decryption at all, and that only an inauthentic key can fail startup.
2.5.0 - 2026-08-08
Upgrading from 2.4.0
See the upgrade guide. One thing changes, and it changes your build rather than your data.
Security
The analyzers were packaged so that they never ran. No TAYRA rule fired in any consumer build up to and including 2.4.0.
Tayra.Analyzersshipped its assembly in the package'slib/folder, where NuGet treats it as an ordinary library reference; analyzers are only loaded fromanalyzers/dotnet/cs. No package depended on it either, so the documented experience - installTayra.Core, get the rules - delivered nothing, and neither did installingTayra.Analyzersexplicitly.Tayra.Corenow carries the analyzers itself and they load correctly.Every compile-time guard described in the 2.4.0 notes and throughout the docs was inert. In particular, TAYRA013 could not have warned you about a non-string
[PersonalData]member whose companion was missing, which is the case where the value was persisted in cleartext. On your first build after upgrading, expect diagnostics you have never seen before; each one describes a condition that already existed in your code. Read them before suppressing any.
Removed
- The standalone
Tayra.Analyzerspackage is no longer published. The analyzers now ship insideTayra.Core, so there is nothing left for a separate package to deliver, and installing both would load two copies of the same assembly and report every diagnostic twice. Every rule is about Tayra attributes and is useless withoutTayra.Core, so nothing needs it on its own. If you referenceTayra.Analyzersexplicitly, remove thePackageReference; the rules arrive throughTayra.Corewith no configuration.
2.4.0 - 2026-08-08
Superseded by 2.5.0
Install 2.5.0. Everything below shipped in 2.4.0 and is accurate, but the analyzers it describes were packaged so that they never loaded, so none of the compile-time guards worked until 2.5.0.
Upgrading from 2.3.x
See the upgrade guide for the ordered steps: what breaks the build, what changes behavior with no compile signal, and the one-way wire format change. Coming from 2.3.x you will land on 2.5.0, so read the 2.4.0 to 2.5.0 section as well.
Security
A missing companion member no longer persists PII as plaintext. A non-string
[PersonalData]member (int,DateOnly, an enum, a record, ...) is serialized and its ciphertext written to a companionbyte[]?member, because a value type cannot hold ciphertext in place. If that companion was absent or misnamed (a typo likeBsnEncrypedwas enough), Tayra logged a warning, skipped the field, and left the value in cleartext for the caller to persist. Encryption and decryption now throw anInvalidOperationExceptionnaming the member, and analyzer TAYRA013 reports it as a compile-time error.If you hit TAYRA013 or this exception when upgrading, that field was never being encrypted. Treat the data already written to your database as compromised plaintext.
Added
Event-store encryption migration for Marten. A new
ITayraMartenEventMigrationService(registered byAddTayraMartenMigrations(), inTayra.Marten8/Tayra.Marten9) bulk-encrypts historical events that were written as cleartext before their type was marked[PersonalData]- the counterpart of the existing document migration.EncryptExistingEventsAsync<TEvent>rewrites the immutablemt_eventslog in place using raw SQL (never through Marten's read pipeline, which would crypto-shred a never-encrypted event on read), encrypting personal data out-of-band, computing blind-index companions, and writing back only thedatacolumn -version,seq_id,stream_id,idandtimestampare never touched. It runs against a raw store (noUseTayra(); a Tayra-enabled store is rejected), is idempotent/resumable (already-encrypted events are skipped), is tenant-correct (each row is encrypted under its owntenant_id), and needs no projection rebuild afterward.VerifyEventEncryptionAsync<TEvent>reports any event still holding cleartext PII. v1 limitations: upcast-target event types and binary (bdata) events are counted as skipped and logged, not migrated. See Marten: event migration.Bulk key prefetch for large projection rebuilds. A new
IKeyStore.GetManyAsync(keyIds)retrieves many keys in one batched round trip (returning a map of found key id to bytes; absent or shredded ids are simply omitted), andICryptoEngine.PrefetchKeysAsync(keyIds)warms the in-memory key cache from it so a subsequent per-document decrypt hits the cache instead of the store. This collapses the millions of single-row key lookups a Marten rebuild would otherwise issue - one per subject - into a handful of bulk reads. Fast backends override with a single query (= ANY(...)on PostgreSQL/Aurora, chunkedIN (...)on SQLite, a dictionary scan in-memory); the envelope and tenant-aware decorators pass the batch through; stores with no native multi-get (Vault, cloud secret stores) fall back to a per-key loop. Both new members ship with default implementations, so existing custom stores and engines keep working unchanged. Prefetch is best-effort and never throws. See Marten: bulk key prefetch for large rebuilds.Rolling-window prefetch pacer for very large Marten rebuilds. An opt-in overload
RebuildProjectionWithPrefetchAsync<TProjection>(PrefetchPacing pacing, ...)(inTayra.Marten8/Tayra.Marten9) warms a bounded window of subject keys just ahead of the async daemon and evicts windows it has already passed, so the warmed working set stays bounded for rebuilds spanning tens of millions of subjects where warming everything up front is too much memory. It follows the daemon via its progress tracker and paces windowedSELECT DISTINCTsubject scans against the observedseq_id. Best-effort: the rebuild is authoritative and any pacer failure is caught and logged without breaking it. v1 targets the single-shard case (conjoined multi-tenancy or single-tenant); for per-tenant-partitioned or database-per-tenant projections it logs a warning and falls back to warm-all-then-rebuild. Two new cache-only Core primitives back it:ICryptoEngine.EvictCachedKeys(keyIds)andIFieldEncrypter.EvictCachedKeysForSubjects<TEntity>(subjectIds)(also onITayra), the inverse of the prefetch members, which remove keys from the in-memory cache without deleting them from the store (not crypto-shredding). Both Core members ship with default/no-op behavior so existing custom engines keep working. The simple warm-all overload is unchanged. See Marten: rolling-window pacer.Turnkey projection prefetch for Marten rebuilds. New
IServiceProviderextensionsRebuildProjectionWithPrefetchAsync<TProjection>()andPrefetchKeysForProjectionAsync<TProjection>()(inTayra.Marten8/Tayra.Marten9) discover a projection's handled event types, scanmt_eventsfor the distinct[DataSubjectId]values those events carry, warm the keys in chunks, and (for the first) rebuild the projection - all in one call, no hand-built key id strings. A new subject-id-based Core surface,IFieldEncrypter.PrefetchForSubjectsAsync<TEntity>(subjectIds)(also onITayra), takes domain ids and derives the base key ids from the entity's[DataSubjectId]metadata (Prefix and Group). This is turnkey (one call), not transparent per-page batching; it is best for full rebuilds where the subject set is knowable, targets the DataEmbedded / single-tenant model, and warms base (version 0) keys only. See Marten: turnkey projection prefetch.One-call multi-tenancy setup:
.WithMultiTenancy(...)on the Tayra builder. A single fluent entry point (chained after.UseXxxKeyStore()and.WithXxxMasterKey(...)) picks a tenancy model and wires everything coherently: the tenant provider, theTenantAwareKeyStoredecorator, envelope master-key resolution, strict mode, and integration auto-flow, with the correct decorator ordering. Two strategies:TenantStrategy.Ambient(default, request/handler-scoped, settableAsyncLocalTenantProvideror a customITenantProvidervia the generic overload) andTenantStrategy.DataEmbedded(background-safe, master key derived from the subject-id prefix, requires envelope mode). Fail-closed by default:RequireTenantis seeded totrue, and for Ambient + envelope it is propagated to the envelope resolver in the same call, fixing the footgun of setting it in two places. The lower-levelAddTayraMultiTenancy()/DeriveMasterKeyIdFromSubjectPrefix()building blocks remain for advanced use. See Multi-tenancy: configure once.Multi-tenancy now works in background contexts (Marten async daemon, projection rebuilds, Wolverine handlers). A new data-derived master-key resolver,
opts.DeriveMasterKeyIdFromSubjectPrefix(), takes the tenant/org from the DEK key id itself (e.g.orgAfromorgA:patient1), so per-tenant envelope isolation holds with no ambient tenant. The shippedAsyncLocalTenantProvider(registered via the new parameterlessAddTayraMultiTenancy()) lets background code set the tenant per unit of work withBeginScope(tenantId), and opt-in strict mode (RequireTenant, on bothTayraMultiTenancyOptionsandEnvelopeOptions) throwsTayraTenantRequiredExceptioninstead of silently sharing one master key. Breaking (EA minor):IMasterKeyIdResolver.Resolve()is nowResolve(MasterKeyContext context)- update any custom resolver. See Multi-tenancy.Wolverine handlers now flow the tenant automatically. When the settable
AsyncLocalTenantProvideris registered (parameterlessAddTayraMultiTenancy()) alongsideUseTayra(), the tenant flows fromEnvelope.TenantIdinto message-body encryption/decryption and into the handler's ambient scope for its whole duration, so Marten session work inside a handler (outbox and tenant sessions included) keys off the message tenant with noBeginScopecall. No behavior change when the settable provider is not registered. See Wolverine multi-tenancy.Marten startup advisory for multi-tenant async projections. When a
TenantAwareKeyStoreis combined with a store that has async-lifecycle projections, Tayra now logs a startup warning: the async projection daemon serializes on a background consumer with no ambient tenant, so ambient tenancy cannot reach it. The advisory points to data-embedded tenancy (DeriveMasterKeyIdFromSubjectPrefix()) andRequireTenant. See Marten multi-tenancy.UseTayra()now covers Marten ancillary stores automatically. Previously it decorated only the defaultAddMarten()store, so a store registered viaAddMartenStore<TStore>kept its own serializer and[PersonalData]on its types was silently ignored. Tayra's serializer wrapper is now registered as a global Marten configuration, so a singleUseTayra()protects the default store and every ancillary store, with no per-store opt-in. See Marten integration.AWS Secrets Manager keystore supports a custom endpoint and DI-supplied client. A new
ServiceUrloption targets a custom endpoint (e.g. LocalStack) for dev/CI parity, and the envelope master keystore now prefers anIAmazonSecretsManagerregistered in DI (for example viaAddAWSService<IAmazonSecretsManager>()) - matching the data keystore - so the whole key topology can run against LocalStack without a real AWS account. See AWS Secrets Manager.[PersonalData]is now the single field-level PII attribute and dispatches on member type. Astring(or string collection) is encrypted in place as before; any other type (int,DateOnly,decimal,Guid, enums, records, ...) is JSON-serialized and its ciphertext is stored in a companionbyte[]?member automatically (named{Member}Encrypted, or set via the attribute'sEncryptedFieldName/ the fluent.StoredIn(...)). You no longer choose the mechanism - Tayra infers it from the type - and you do not need to migrate strongly-typed members to strings.[DeepPersonalData]is unchanged.Any type
System.Text.Jsoncan serialize now works on a non-string member. Previously a hand-rolled binary serializer supported exactly eleven types and threwNotSupportedExceptionat first encrypt for anything else, with no compile-time warning - so a plausible member likeshort AgeorTimeSpancompiled clean and failed in production.short,byte,uint,ulong,char,TimeOnly,TimeSpan,Uri, records, classes, and collections now all work, alongside the types that already did.DateTimekeeps itsKindandDateTimeOffsetkeeps its offset. An exotic type System.Text.Json cannot handle now fails with a clear message naming the member and pointing at a customJsonConverter.Per-field crypto-shred replacement values.
[PersonalData(ReplacementValue = "-1")]writes a specific value back after shredding instead of the type default; the string is parsed into the member's type (invariant culture; enum name or number) once at metadata-build time, so an unparseable value fails fast. The fluent form is strongly typed to the member -e.PersonalData(x => x.Bsn).WithReplacement(-1)takes a value of the member's own type, with no string parsing and no invariant-culture ambiguity.A single generic fluent overload.
PersonalData<TProperty>(Expression<Func<T, TProperty>>)binds by inference for any member type (string, string collection, or a value type with itsbyte[]?companion configured via.StoredIn(x => x.FooEncrypted)).Blind indexes work on any canonicalizable type.
[BlindIndex]now supportsint,long,DateOnly,DateTime,Guid, enums, and their nullable forms, alongsidestring. Strongly-typed PII is now queryable without migrating it to strings. Existing string indexes are byte-identical - strings are still hashed as themselves, so no reindex is needed for them.IBlindIndexer.BuildPredicateAsync()builds the query predicate for you, so a query site never has to name the companion member or the index:csharpvar match = await blindIndexer.BuildPredicateAsync((Patient x) => x.Bsn, 123456789); var results = await session.Query<Patient>().Where(match).Where(x => x.Active).ToListAsync();It returns a plain
Expression<Func<T,bool>>, so it composes with other clauses and works on Marten, EF Core, and MongoDB alike. Note that a blind index is an HMAC: equality only, never ranges.Analyzer TAYRA015 catches cleartext comparisons in query predicates. Comparing a
[PersonalData]or[BlindIndex]member against a value inside a query predicate (==,!=,.Equals(...)in a lambda that converts toExpression<Func<T, bool>>, or the equivalent LINQ query syntax) is now a compile-time warning. At rest the member holds ciphertext, or a zeroed default for a non-string member, so the predicate translates to SQL, runs against the ciphertext, and silently matches zero rows - which reads as "no such record" rather than as a bug. The diagnostic names the fix that applies: compare the blind index viaIBlindIndexer.BuildPredicateAsync, or add[BlindIndex]first if the member has none. A comparison in a lambda that converts to a plainFunc<,>runs in memory over objects Tayra has already decrypted and is not reported, and neither is a comparison againstnull. See TAYRA015.Analyzer TAYRA014 reports a compile-time error when a
Maskingstrategy is set on a non-string[PersonalData]member. Masking is string-only (a deliberate partial-cleartext leak); useReplacementValuefor non-string members. In the fluent API theWithMask*methods only exist on a string member, so masking a non-string member does not compile at all.
Changed
Licensing is now a single-term model (breaking). One signed
expiresAtreplacesMaintenanceUntil; production decryption is perpetual, encryption is term + 60-day grace, and pre-term keys must be re-issued. See Licensing.Non-string
[PersonalData]payloads are now UTF-8 JSON in a v4 wire format (previously a binary encoding in v3). Both formats remain readable - the decoder is chosen by the version byte on the data - so existing ciphertext keeps decrypting after you upgrade, and a field migrates to v4 lazily the next time it is encrypted. No migration job is required.Upgrade is one-way
A v4 payload cannot be read by Tayra 2.3.x or earlier. Once a node running 2.4.0 encrypts a field, a node still on 2.3.x cannot decrypt it, so rolling the library version back after writing is not supported. Upgrade the whole fleet together, and take a backup first if you need a rollback path.
This affects serialized (non-string) members only.
[PersonalData]strings and string collections are unchanged, and so are envelope-wrapped DEKs.[SerializedPersonalData]and theSerializedPersonalData(...)fluent method are now[Obsolete]aliases of[PersonalData]. The distinction between the two attributes was purely mechanical (non-string PII needs a companionbyte[]?), and Tayra now infers it from the member type, so the separate attribute is redundant. The alias behaves identically and existing code keeps compiling; move to[PersonalData]at your convenience. Tayra is in early access, so this lands as a minor rather than going through the deprecation window..WithMaskValue(...)is renamed to.WithReplacement(...)(the old name is an[Obsolete]alias), and the attribute'sMaskValueproperty is renamed toReplacementValue(the old name is an[Obsolete]alias). A replacement value is universal (every member type) and leaks nothing; it is kept distinct fromMasking, which is string-only and a deliberate partial-cleartext leak.Analyzer TAYRA006 retargeted. It used to report
[BlindIndex]on any non-string member as unsupported, which is no longer true. It now warns when[BlindIndex]is applied to a bool or a small enum, where a deterministic index is close to useless: with only two or three distinct hashes in the column, frequency analysis recovers the plaintext immediately.[BlindIndex]on anintBSN is now correct and silent;[BlindIndex]onbool HivPositivenow warns.
Removed
The standalone
SerializedPersonalDataBuilder<T>is removed, folded into the genericPersonalDataBuilder<T, TProp>. ItsWithGroup,StoredIn, andWithReplacementare now methods on the unified builder. TheSerializedPersonalData(...)entry point remains as an[Obsolete]shim returning the unified builder.BinaryFieldSerializer.Serializeis removed. Nothing writes the v3 binary payload any more, so the write half of the frozen encoder is gone. Tayra is in early access, so this lands as a minor rather than going through the deprecation window.BinaryFieldSerializer.Deserializeis deliberately kept, and is not deprecated. It is the only thing that can decode a v3 payload, and every serialized (non-string) field encrypted by 2.3.x is v3. Removing it would make that PII permanently undecryptable, so it stays indefinitely. Writing v3 ended; reading v3 has not, and no re-encrypt migration is forced on you.
Fixed
A key-store outage during encryption now fails closed with a clear
TayraKeyStoreUnavailableException. Previously, when the key store was unreachable while resolving a key (for example a missing schema under Marten, in envelope mode), the raw store error (aPostgresException) propagated with nothing in the exception chain naming Tayra or the key store, and could surface downstream as a misleading error (e.g. Marten's42883: operator does not exist: text = integer). Key resolution runs before any field is touched, so this exception is raised with the object unmodified - no half-encrypted or corrupt data reaches the database. The underlying store failure is preserved as the inner exception.[BlindIndex]on a non-string member silently indexed nothing. The indexer read the member as a string, which isnullfor anintor aDateOnly, and skipped it. The companion stayednullon every row, so every query on that index matched zero rows and nothing errored. TAYRA006 was only a warning, so it compiled and shipped. Non-string members are now indexed properly (see below), and a value that cannot be canonicalized throws instead of quietly producing an unusable index.If you had
[BlindIndex]on a non-string member, its companion column is empty. Recompute the indexes for those documents after upgrading; see Recomputing Indexes.PostgreSQL key store
AutoMigratenow creates the configured schema. WhenSchemawas set to a non-default value (e.g."tayra_keys") on a fresh database,AutoMigrateissuedCREATE TABLEwithout first creating the schema, so every key operation failed with3F000: schema "..." does not exist. Tayra now issuesCREATE SCHEMA IF NOT EXISTSbefore the table. This also covers the master-key store configured viaWithPostgreSqlMasterKey. The runtime role needsCREATEon the database for first-run schema creation; setAutoMigrate = falseand provision the schema through your own pipeline if it cannot.
2.3.0 - 2026-06-10
Core hardening: encryption now fails closed, tampering is surfaced, and crypto-shredding stays complete after key rotation. Annotations now work on public instance fields as well as properties.
Changed (breaking)
- Encryption fails closed.
EncryptAsyncnow throws anInvalidOperationExceptionwhen a[DataSubjectId]value isnull, or when no encryption key can be resolved for a field's group. Previously the affected fields were silently persisted as plaintext with only a warning log. Decryption tolerance is unchanged: a missing (shredded) key still follows the replacement-value path. - The metadata API no longer exposes a
PropertyInfo. The reflection model now describes annotated members through aPersonalDataMemberaccessor (withName,MemberType,CanWrite,GetValue,SetValue, andUnderlying) that backs both properties and fields.PersonalDataFieldInfo.PropertybecomesPersonalDataFieldInfo.Member,DataSubjectInfoexposesMember, and the blind-index metadata exposesSourceMember/IndexMember. Code that read.Propertyoff the public metadata types must move to the newMemberaccessor. - Non-writable annotated members now fail with a clear error at first use instead of being silently skipped. A member Tayra writes back to (
TextandSerializedkinds, plus their companions) must be writable: a settable property (init-only is fine) or a non-readonly, non-constfield. A get-only property or areadonly/constfield throws anInvalidOperationExceptionat first encrypt. Similarly, read-only string collection instances (e.g.ImmutableList<string>) now throw instead of being silently skipped. - The AAD decrypt path rejects legacy v1 ciphertexts unconditionally. Version
0x01payloads predate associated-data binding, so they cannot be context-verified - a database-level attacker could splice a v1 ciphertext into a different field of the same subject.AesGcmEncryptor.DecryptWithAssociatedData/DecryptStringWithAssociatedDatanow throw aCryptographicExceptionon v1 payloads; at the field-encrypter level a v1 value is left untouched and surfaces as anIntegrityCheckFailedaudit event. The v1 format remains valid only for the no-AADAesGcmEncryptor.Encrypt/Decryptprimitive pairing.
Added
- All seven attributes now annotate properties or public instance fields.
[PersonalData],[DeepPersonalData],[SerializedPersonalData],[DataSubjectId],[BlindIndex],[CompoundBlindIndex], and[ArrayBlindIndex]are recognized on public instance fields in addition to properties; field usage compiles and works. Only public instance members are scanned (private and static members are ignored), mirroring the existing property scan rule. The fluent API expression selectors now accept field expressions (x => x.SomeField) as well as property expressions. - Tamper detection is surfaced. A
CryptographicExceptionduring decryption (tampered ciphertext or context mismatch) is now logged atWarningand emits a newTayraAuditEventType.IntegrityCheckFailedaudit event; the field value is left untouched. Legacy-plaintext tolerance (FormatException) stays quiet atDebug. HashSet<string>and other mutableICollection<string>properties are now encrypted (previously onlyIList<string>/string[]worked; other collection types were silently skipped). A new fluent overloadEntityTypeBuilder<T>.PersonalData(Expression<Func<T, IEnumerable<string>>>)configures string-collection properties directly.
Fixed
ShredAsync(subjectId)now deletes the subject's base key and every{subjectId}:-prefixed key - rotated versions (:vN) and group keys - so crypto-shredding remains complete after key rotation. Previously only the exact base key was deleted, leaving data encrypted under rotated keys readable. Neighboring subjects that merely share an ID prefix are unaffected.[DeepPersonalData]object graphs with cycles no longer cause aStackOverflowException, and shared (diamond) references are processed exactly once instead of being double-encrypted.DeepPersonalDataconfigured on a collection property via the fluent API now encrypts the elements (previously silently skipped).- Eliminated a key-creation race that could encrypt data with a never-persisted key:
IKeyStore.StoreAsyncis now documented as first-writer-wins, and the crypto engine and blind-index key provider read the key back after storing and serialize per-key-ID creation in-process. - Blind-index HMAC key rotation now actually replaces the stored key (delete, store, then read-back verify); previously the first-writer-wins store made rotation a silent no-op. The HMAC key cache also gained a TTL (default 5 minutes) so rotations and deletions performed elsewhere are observed.
2.2.0 - 2026-06-10
Changed
- Updated the integration targets to Marten 9.7.1 and Wolverine 6.6.0. The dependency ranges still cap the next framework major, so a consumer that pulls Marten 10 or Wolverine 7 fails fast at restore instead of at runtime.
- Wolverine 6 dead-letter exception redaction now uses a supported hook. When
RedactExceptionMessagesis enabled,Tayra.Wolverine6scrubs the exception text on Wolverine'sIDeadLetterInterceptor(added in WolverineFx 6.6.0) instead of reaching into private exception internals. The redacted dead letter now preserves the original exception type, so you can still filter and triage dead letters by type while the message is replaced withException details redacted by Tayra.The behavior of the option is otherwise unchanged.Tayra.Wolverine5keeps its existing redaction mechanism.
No public API changes.
2.1.0 - 2026-06-09
Added
- Per-major integration packages. Pick the package that matches your framework version:
Tayra.Marten8/Tayra.Marten9andTayra.Wolverine5/Tayra.Wolverine6. Both lines ship from a single trunk at the same suite version, so you are never forced onto a new framework major to stay current with Tayra. See Installation.
Fixed
- HashiCorp Vault key store: keys are now read back correctly from Vault KV v2.
- Wolverine 5: the integration's source generator is kept enabled so its handlers (such as the built-in GDPR erasure handler) are discovered at runtime.
2.0.0 - 2026-06-07
Tayra 2.0 moves the default integrations onto the current framework majors.
Changed (breaking)
- The Marten integration now targets Marten 9 and the Wolverine integration targets Wolverine 6. Marten 8 / Wolverine 5 support continues through the per-major packages introduced in 2.1.0.
- The Wolverine integration now encrypts at the message serializer rather than via handler middleware. PII is therefore ciphertext at rest in the durable outbox/inbox and in dead-letter storage (the durable outbox could previously hold cleartext).
UseTayraMiddleware()is now an[Obsolete]alias forUseTayra(), and theRedactDeadLetterQueuesoption was removed because dead-letter bodies are encrypted automatically.
Added
- Searchable encrypted collections via
[ArrayBlindIndex], with fluent configuration. - New Roslyn analyzers: TAYRA007 (PII on Marten binary events), TAYRA008 (flat-table PII guard), TAYRA009 (duplicated-field PII guard), and TAYRA010 (PII on a Wolverine saga).
- Marten: fail fast at startup if a custom serializer drops the Tayra wrapper.
Performance
- Core encrypt/decrypt dispatch is cached as compiled delegates.
1.4.0 - 2026-05-05
Added
- Envelope encryption: two-tier encryption with pluggable master and data key stores and pluggable master-key id resolution.
- AWS Aurora (PostgreSQL with IAM authentication) key store.
- Persistent, hash-chained audit trail (Marten-backed), plus signed (ECDSA P-256) and scheduled compliance reports with a pluggable archive.
- Date-only perpetual-fallback licensing model (the major-version gate was dropped).
Changed
[DeepPersonalData]now recurses transitively through nested annotations.
Removed
- The
Tayra.MediatR,Tayra.MassTransit, andTayra.NServiceBusintegrations were dropped because their upstreams moved to commercial or copyleft licensing. Tayra ships no copyleft or commercial-by-default dependencies.
1.3.0 - 2026-05-01
Changed
- Compliance features were extracted into a dedicated
Tayra.Compliancepackage.
1.2.0 and earlier - 2026-03 to 2026-04
Initial public releases established the core library and ecosystem:
- AES-256-GCM field-level encryption with a versioned wire format and embedded key version for rotation, plus GDPR crypto-shredding.
- The attribute model (
[PersonalData],[DataSubjectId],[DeepPersonalData],[SerializedPersonalData]) and Roslyn analyzers TAYRA001 to TAYRA006. - Key stores: In-Memory, SQLite, PostgreSQL, HashiCorp Vault, Azure Key Vault, AWS Parameter Store, and AWS Secrets Manager.
- Integrations for Entity Framework Core, Marten, MongoDB, Serilog, System.Text.Json, and ASP.NET Core, plus the
dotnet tayraCLI.
