Chronicle. Redaction
(chronicle v0.1.2)
Copy Markdown
Central protection policy for event data and Ecto record snapshots.
Protection is applied by field name, recursively, at every nesting depth. A protected field is never written to the ledger in clear text.
Built-in protection
Chronicle protects credential-shaped field names by default. Detection is deliberately generous because an audit ledger is append-only: a value written in clear text cannot be removed later without breaking the hash chain.
A field is protected when its name
- contains
password,passwd,secret,token,credential,apikey,privkey, orpassphrase; or - has an underscore-separated segment equal to
pwd,ssn,cvv,cvc,iban,pin,salt, orjwt; or - exactly matches a known credential or financial identifier such as
api_key,card_number, orrouting_number.
Built-in protection covers credentials, not general personal data. Classify application-specific personal data explicitly.
Configuration
Configured fields extend the built-in list:
config :chronicle,
redaction: [
fields: [:internal_note],
hash_fields: [:email],
omit_fields: [:raw_response]
]Adding one field never silently disables built-in protection. To take complete ownership of the policy, opt out explicitly:
config :chronicle,
redaction: [builtin: false, fields: [:password, :token]]Effect on Ecto record versions
A withheld field cannot be reconstructed, so a snapshot containing one is
reported as incomplete and Chronicle.at/2 and Chronicle.revert/2 fail
closed rather than inventing a value. mix chronicle.doctor lists the
withheld fields of every audited schema so this is a visible decision.
Ecto schemas may instead declare erasable: [field: :privacy_key_id].
Chronicle stores authenticated ciphertext and reconstructs it while the
named external key exists. Destroying that key makes later reconstruction
fail without invalidating the signed chain.
What name matching structurally cannot see
Every rule here matches on a field name, which means a sensitive value that
never sits under a name is invisible to all of them. A secret embedded in a
free-text description, a token inside an error message being logged as
context, an element of a list whose own key did not match — none of these are
protected, and no amount of extending the name lists will reach them.
This is not a gap to be closed later; it is the boundary of what the approach
can do. Values that carry their sensitivity in content rather than in naming
have to be marked at the point they enter, with Chronicle.secret/1,
Chronicle.hash/1, or Chronicle.omit/0. Those markers are the escape hatch
precisely because the policy cannot infer what it cannot name.
Summary
Functions
Returns whether a field name is protected by the built-in rules.
Resolves the effective policy once.
Builds the field redactor used for Ecto changes and snapshots.
Applies the policy to one field, returning the value or a Sensitive marker.
Returns the protected fields of an Ecto schema, for diagnostics.
Functions
Returns whether a field name is protected by the built-in rules.
@spec compile(keyword()) :: Chronicle.Redaction.Policy.t()
Resolves the effective policy once.
Options:
:policy- an already-resolvedChronicle.Redaction.Policy:schema- an Ecto schema whoseChronicle.Schemapolicy contributes:redact- extra fields to redact for this call
Builds the field redactor used for Ecto changes and snapshots.
@spec protect_field(Chronicle.Redaction.Policy.t(), term(), term()) :: term()
Applies the policy to one field, returning the value or a Sensitive marker.
Returns the protected fields of an Ecto schema, for diagnostics.