custody
Encrypted, tenant-scoped custody of secret values, their revocation and deletion, and atomic prepared mutation batches.
secrets.custody is one of secrets's bounded contexts. All domains.
Types
DeleteMutation
secrets.custody.DeleteMutation is a record of one field:
reference—secrets.custody.Reference
Disclosure
secrets.custody.Disclosure is one of workload_only and user_revealable.
Mutation
secrets.custody.Mutation is one of two shapes, told apart by a op field — tagged, so a decoder never has to guess which branch it is reading:
delete—secrets.custody.DeleteMutationput—secrets.custody.PutMutation
PutMutation
secrets.custody.PutMutation is a record of one field:
secret—secrets.custody.SecretWrite
Reference
secrets.custody.Reference is a record of three fields:
tenant—Stringnamespace—Stringkey—String
SecretWrite
secrets.custody.SecretWrite is a record of five fields:
reference—secrets.custody.Referenceowner_subject—Stringvalue—Bytesdisclosure—Optional<secrets.custody.Disclosure>, which may be absentlabels—Optional<Map<String, String>>, which may be absent
Entities
An entity is what this context is about: something with an identity that outlives any one request, a shape, and a lifecycle. The lifecycle is exhaustive — a move that is not drawn below is a move this specification does not permit, and that is the only way it says so. Every move is labelled with the command that takes it, because a move nothing can trigger is refused rather than drawn.
PreparedTransaction
secrets.custody.PreparedTransaction.
An instance is identified by transaction, a Uuid. The name is part of the model and not a convention: a view projects the identity under that name, so a projection inventing its own would disagree with the view.
It holds:
tenant—Stringactor—Stringmutations—List<secrets.custody.Mutation>created_at—Stringexpires_at—String
It declares no relation to another entity, and no other entity names it.
No invariant is declared, so nothing here constrains an instance at rest.
Its state is a secrets.custody.PreparedTransaction.State, one of Prepared. That enum is synthesised from the lifecycle rather than declared beside it, so the states a view's filter compares and the states drawn below cannot disagree.
An instance is created in Prepared. Prepared is terminal, so an instance may rest there forever. That is declared rather than inferred from having no way out: an entity that cannot leave a state is either finished or stuck, and only its author knows which.
It declares no moves, so nothing changes its state once it exists.
It has one state, so there is no move to permit or to forbid.
No view projects it, so nothing outside this context is promised a way to observe one.
Secret
secrets.custody.Secret.
An instance is identified by reference, a secrets.custody.Reference. The name is part of the model and not a convention: a view projects the identity under that name, so a projection inventing its own would disagree with the view.
It holds:
id—Uuidowner_subject—Stringdisclosure—secrets.custody.Disclosureversion—Integerlabels—Map<String, String>value—Bytescreated_at—Stringupdated_at—String
It declares no relation to another entity, and no other entity names it.
Every instance satisfies version > 0 — a predicate over this entity's own fields, checked against them rather than stored as a sentence, so an invariant reading something the entity does not have is refused instead of documented.
Its state is a secrets.custody.Secret.State, one of Active and Revoked. That enum is synthesised from the lifecycle rather than declared beside it, so the states a view's filter compares and the states drawn below cannot disagree.
An instance is created in Active. No state is terminal: nothing in this lifecycle says an instance may stop moving.
Each move is taken by a declared command outcome, and a move nothing takes is refused as missing_causation rather than left as a state change nobody can trigger:
revoke— taken bysecrets.custody.RevokeOwnedSecreton itsrevokedoutcomereplace— taken bysecrets.custody.PutSecreton itsreplacedoutcome
An instance is brought into existence by secrets.custody.PutSecret on its created outcome.
Every ordered pair of these states is connected by some move, so this lifecycle forbids nothing.
Five views project it: ActiveSecrets, NamespaceSecrets, OwnedSecretDetail, OwnedSecrets and SecretValue.
Views
A view is what the outside world is promised it can observe. Each one says which instances it contains and how soon it reflects a command that has already returned, because "you can read this" without "how soon" is the promise every flaky suite is built on.
ActiveSecrets
secrets.custody.ActiveSecrets.
It reads Secret.
It contains the instances where state == Active holds, and only those — so an instance a caller cannot find in here has been filtered out rather than lost.
It exposes:
reference—secrets.custody.Reference
It declares no order, so the rows come back in whatever order the implementation has, and two reads may disagree.
Read-your-writes: it is current the moment the command that changed it returns. A caller that has just created an invoice and cannot see it in here has been told a lie about what it did.
A generated scenario asserts it once, immediately after the command: a view promising this and not keeping the promise has to fail the suite rather than be retried until it passes.
NamespaceSecrets
secrets.custody.NamespaceSecrets.
It reads Secret.
It contains every instance of that entity: no filter narrows it, which is a decision somebody made and not a line somebody omitted.
It exposes:
id—Uuidreference—secrets.custody.Referenceowner_subject—Stringdisclosure—secrets.custody.Disclosurestate—secrets.custody.Secret.Stateversion—Integerlabels—Map<String, String>created_at—Stringupdated_at—String
It declares no order, so the rows come back in whatever order the implementation has, and two reads may disagree.
Read-your-writes: it is current the moment the command that changed it returns. A caller that has just created an invoice and cannot see it in here has been told a lie about what it did.
A generated scenario asserts it once, immediately after the command: a view promising this and not keeping the promise has to fail the suite rather than be retried until it passes.
OwnedSecretDetail
secrets.custody.OwnedSecretDetail.
It reads Secret.
It contains every instance of that entity: no filter narrows it, which is a decision somebody made and not a line somebody omitted.
It exposes:
id—Uuidreference—secrets.custody.Referenceowner_subject—Stringdisclosure—secrets.custody.Disclosurestate—secrets.custody.Secret.Stateversion—Integerlabels—Map<String, String>created_at—Stringupdated_at—String
It declares no order, so the rows come back in whatever order the implementation has, and two reads may disagree.
Read-your-writes: it is current the moment the command that changed it returns. A caller that has just created an invoice and cannot see it in here has been told a lie about what it did.
A generated scenario asserts it once, immediately after the command: a view promising this and not keeping the promise has to fail the suite rather than be retried until it passes.
OwnedSecrets
secrets.custody.OwnedSecrets.
It reads Secret.
It contains every instance of that entity: no filter narrows it, which is a decision somebody made and not a line somebody omitted.
It exposes:
id—Uuidreference—secrets.custody.Referenceowner_subject—Stringdisclosure—secrets.custody.Disclosurestate—secrets.custody.Secret.Stateversion—Integerlabels—Map<String, String>created_at—Stringupdated_at—String
It declares no order, so the rows come back in whatever order the implementation has, and two reads may disagree.
Read-your-writes: it is current the moment the command that changed it returns. A caller that has just created an invoice and cannot see it in here has been told a lie about what it did.
A generated scenario asserts it once, immediately after the command: a view promising this and not keeping the promise has to fail the suite rather than be retried until it passes.
SecretValue
secrets.custody.SecretValue.
It reads Secret.
It contains the instances where state == Active holds, and only those — so an instance a caller cannot find in here has been filtered out rather than lost.
It exposes:
reference—secrets.custody.Referenceversion—Integervalue—Bytes
It declares no order, so the rows come back in whatever order the implementation has, and two reads may disagree.
Read-your-writes: it is current the moment the command that changed it returns. A caller that has just created an invoice and cannot see it in here has been told a lie about what it did.
A generated scenario asserts it once, immediately after the command: a view promising this and not keeping the promise has to fail the suite rather than be retried until it passes.
Commands
AbortTransaction
secrets.custody.AbortTransaction.
It takes:
tenant—Stringtransaction—Uuid
It has seven outcomes.
aborted — The batch is discarded without applying any mutation. The default branch, taken when no other outcome's condition matched. It removes the secrets.custody.PreparedTransaction its input names; no view shows it afterwards. The instance is the one named by the input field transaction. It emits secrets.custody.TransactionAborted. A test reaches it by constructing an input that satisfies no other outcome's condition.
not-found — Decided outside the input: no transaction with this id is prepared in the tenant, its expires_at column has passed, or its key is not in the keyring. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.NotFound. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Kubernetes authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the grant's tenant is not the path tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the grant lacks secret:abort, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Decided outside the input: storage or TokenReview is unavailable. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-path — Decided outside the input: the transaction path segment is not a UUID. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
CheckSecretExists
secrets.custody.CheckSecretExists.
It takes:
reference—secrets.custody.Reference
It has seven outcomes.
checked — The caller learns whether an Active secret is stored at the reference. The default branch, taken when no other outcome's condition matched. No entity in this specification changes. It emits secrets.custody.SecretExistenceChecked. A test reaches it by constructing an input that satisfies no other outcome's condition.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Kubernetes authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the grant's tenant is not reference.tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the grant lacks secret:read_metadata, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Decided outside the input: storage or TokenReview is unavailable. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
too-large — Decided outside the input: the request body is longer than 1048576 bytes. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.PayloadTooLarge. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-body — Decided outside the input: the request body is not JSON of the command's request shape. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
CommitTransaction
secrets.custody.CommitTransaction.
It takes:
tenant—Stringtransaction—Uuid
It has 10 outcomes.
committed — Every mutation in the batch is applied atomically and the batch is gone. The default branch, taken when no other outcome's condition matched. It removes the secrets.custody.PreparedTransaction its input names; no view shows it afterwards. The instance is the one named by the input field transaction. It emits secrets.custody.TransactionCommitted. A test reaches it by constructing an input that satisfies no other outcome's condition.
not-found — Decided outside the input: no unexpired transaction with this id is prepared in the tenant; a batch expires 600 seconds after it is prepared, by its expires_at or by the expiry sealed in its envelope. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.NotFound. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
delete-target-missing — Decided outside the input: a Delete mutation names a reference with no secret. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.NotFound. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-reference — Decided outside the input: a Put mutation's reference part is empty or contains NUL. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
invalid-reference — Decided outside the input: a Put mutation's reference part is longer than 255 bytes, and none is empty or contains NUL. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Kubernetes authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the grant's tenant is not the path tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the grant lacks secret:commit, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Decided outside the input: storage, encryption or TokenReview is unavailable, or the batch envelope does not open under its tenant, id and actor. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-path — Decided outside the input: the transaction path segment is not a UUID. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
DeleteOwnedSecret
secrets.custody.DeleteOwnedSecret.
It takes:
reference—secrets.custody.Reference
It has eight outcomes.
deleted — The secret and all its encrypted versions are gone. The default branch, taken when no other outcome's condition matched. It removes the secrets.custody.Secret its input names; no view shows it afterwards. The instance is the one named by the input field reference. It emits secrets.custody.SecretDeleted. A test reaches it by constructing an input that satisfies no other outcome's condition.
not-owned — Decided outside the input: no secret at the reference is owned by the calling subject. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.NotFound. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Identity authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the caller's tenant is not reference.tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the scope lacks secret:delete, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Decided outside the input: storage or the Identity authority is unavailable. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
too-large — Decided outside the input: the request body is longer than 1048576 bytes. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.PayloadTooLarge. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-body — Decided outside the input: the request body is not JSON of the command's request shape. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
DeleteSecret
secrets.custody.DeleteSecret.
It takes:
reference—secrets.custody.Referenceactor—Optional<String>, which may be absent
It has eight outcomes.
deleted — The secret and all its encrypted versions are gone. The default branch, taken when no other outcome's condition matched. It removes the secrets.custody.Secret its input names; no view shows it afterwards. The instance is the one named by the input field reference. It emits secrets.custody.SecretDeleted. A test reaches it by constructing an input that satisfies no other outcome's condition.
not-found — Decided outside the input: no secret is stored at the reference. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.NotFound. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Kubernetes authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the grant's tenant is not reference.tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the grant lacks secret:delete, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Decided outside the input: storage or TokenReview is unavailable. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
too-large — Decided outside the input: the request body is longer than 1048576 bytes. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.PayloadTooLarge. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-body — Decided outside the input: the request body is not JSON of the command's request shape. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
ListNamespaceSecrets
secrets.custody.ListNamespaceSecrets.
It takes:
tenant—Stringnamespace—String
It has seven outcomes.
listed — The caller receives the metadata of every secret in the namespace, in both states. The default branch, taken when no other outcome's condition matched. No entity in this specification changes. It emits secrets.custody.NamespaceSecretsListed. A test reaches it by constructing an input that satisfies no other outcome's condition.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Kubernetes authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the grant's tenant is not the requested tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the grant lacks secret:list, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Decided outside the input: storage or TokenReview is unavailable. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
too-large — Decided outside the input: the request body is longer than 1048576 bytes. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.PayloadTooLarge. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-body — Decided outside the input: the request body is not JSON of the command's request shape. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
PrepareTransaction
secrets.custody.PrepareTransaction.
It takes:
transaction—Uuidactor—Stringmutations—List<secrets.custody.Mutation>
It has 11 outcomes.
prepared — The batch is held in the caller's tenant for later commit or abort, only as an AES-256-GCM envelope from the service keyring bound to the tenant, the transaction id and the actor, and for at most 600 seconds; expired batches of the tenant are removed. The default branch, taken when no other outcome's condition matched. It creates a secrets.custody.PreparedTransaction, which starts in Prepared. The new instance's identity is published as transaction on secrets.custody.TransactionPrepared. It emits secrets.custody.TransactionPrepared. It sets tenant from implementation-generated, actor from input.actor and mutations from input.mutations. A test reaches it by constructing an input that satisfies no other outcome's condition.
empty-batch — Taken when mutations.count == 0 holds of the input. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by constructing an input that satisfies that condition.
cross-tenant-batch — Eligible when mutations.count > 0; externally decided: a mutation names a tenant other than the caller's. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
duplicate — Eligible when mutations.count > 0; externally decided: a transaction with this id is already prepared in the tenant. No entity in this specification changes. It reports secrets.custody.Conflict. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Kubernetes authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the path names a tenant other than the grant's. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the grant lacks secret:prepare, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Eligible when mutations.count > 0; externally decided: storage or TokenReview is unavailable. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
too-large — Decided outside the input: the request body is longer than 1048576 bytes. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.PayloadTooLarge. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-body — Decided outside the input: the request body is not JSON of the command's request shape. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-path — Decided outside the input: the transaction path segment is not a UUID. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
PutSecret
secrets.custody.PutSecret.
It takes:
reference—secrets.custody.Referenceowner_subject—Stringvalue—Bytesdisclosure—Optional<secrets.custody.Disclosure>, which may be absentlabels—Optional<Map<String, String>>, which may be absent
It has 10 outcomes.
created — A secret exists at the reference, Active, version 1. Decided outside the input: no secret is stored at the reference. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. It creates a secrets.custody.Secret, which starts in Active. The new instance's identity is published as reference on secrets.custody.SecretPut. It emits secrets.custody.SecretPut. It sets id from implementation-generated, owner_subject from input.owner_subject, disclosure from input.disclosure, converted because An absent disclosure is workload_only (#[serde(default = "workload_only")], crates/secrets-core/src/lib.rs:52-53)., version from "1", labels from input.labels, converted because Absent labels are the empty map (#[serde(default)], crates/secrets-core/src/lib.rs:54-55)., value from input.value, created_at from implementation-generated and updated_at from implementation-generated. A test reaches it by injecting the declared fault, because no input can.
replaced — The secret holds a new current version and is Active again, including after a revoke. The default branch, taken when no other outcome's condition matched. It moves a secrets.custody.Secret from Active and Revoked to Active, along the declared move replace. The instance is the one named by the input field reference. It emits secrets.custody.SecretPut. It sets owner_subject from input.owner_subject, disclosure from input.disclosure, converted because An absent disclosure is workload_only (#[serde(default = "workload_only")], crates/secrets-core/src/lib.rs:52-53)., version from its previous value plus 1, labels from input.labels, converted because Absent labels are the empty map (#[serde(default)], crates/secrets-core/src/lib.rs:54-55)., value from input.value and updated_at from implementation-generated. A test reaches it by constructing an input that satisfies no other outcome's condition.
malformed-reference — Taken when (reference.tenant.count == 0 or reference.namespace.count == 0 or reference.key.count == 0 or reference.tenant contains "\0" or reference.namespace contains "\0" or reference.key contains "\0") holds of the input. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by constructing an input that satisfies that condition.
invalid-reference — Eligible when (reference.key.count >= 256 or reference.tenant.count > 63 or reference.namespace.count > 63 or reference.key.count > 63); externally decided: a reference part is longer than 255 bytes. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Kubernetes authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the grant's tenant is not reference.tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the grant lacks secret:write, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Decided outside the input: storage, encryption or TokenReview is unavailable. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
too-large — Decided outside the input: the request body is longer than 1048576 bytes. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.PayloadTooLarge. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-body — Decided outside the input: the request body is not JSON of the command's request shape. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
ReadSecretValue
secrets.custody.ReadSecretValue.
It takes:
reference—secrets.custody.Reference
It has eight outcomes.
read — The caller receives the current version's value and metadata. The default branch, taken when no other outcome's condition matched. No entity in this specification changes. It emits secrets.custody.SecretValueRead. A test reaches it by constructing an input that satisfies no other outcome's condition.
not-found — Decided outside the input: no Active secret is stored at the reference. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.NotFound. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Kubernetes authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the grant's tenant is not reference.tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the grant lacks secret:read_value, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Decided outside the input: storage, decryption or TokenReview is unavailable. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
too-large — Decided outside the input: the request body is longer than 1048576 bytes. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.PayloadTooLarge. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-body — Decided outside the input: the request body is not JSON of the command's request shape. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
RevokeOwnedSecret
secrets.custody.RevokeOwnedSecret.
It takes:
reference—secrets.custody.Reference
It has eight outcomes.
revoked — The secret is Revoked; workload get and exists no longer see it, list and detail still do. The default branch, taken when no other outcome's condition matched. It moves a secrets.custody.Secret from Active and Revoked to Revoked, along the declared move revoke. The instance is the one named by the input field reference. It emits secrets.custody.SecretRevoked. It sets updated_at from implementation-generated. A test reaches it by constructing an input that satisfies no other outcome's condition.
not-owned — Decided outside the input: no secret at the reference is owned by the calling subject. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.NotFound. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unauthorized — Decided outside the input: the bearer token is missing or refused by the Identity authority. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unauthorized. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
forbidden — Decided outside the input: the caller's tenant is not reference.tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
missing-action — Decided outside the input: the scope lacks secret:revoke, for a principal of the addressed tenant. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Forbidden. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
unavailable — Decided outside the input: storage or the Identity authority is unavailable. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
too-large — Decided outside the input: the request body is longer than 1048576 bytes. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.PayloadTooLarge. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
malformed-body — Decided outside the input: the request body is not JSON of the command's request shape. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.InvalidInput. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
RewrapSecrets
secrets.custody.RewrapSecrets.
It takes:
actor—String
It has three outcomes.
rewrapped — Every stored version and every held prepared batch is wrapped by the active key. Decided outside the input: at least one stored version is wrapped by a key other than the operator's active key. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It emits secrets.custody.SecretsRewrapped. A test reaches it by injecting the declared fault, because no input can.
nothing-to-rewrap — Every stored version was already wrapped by the active key; nothing is re-encrypted and nothing is recorded. The default branch, taken when no other outcome's condition matched. No entity in this specification changes. It emits secrets.custody.NothingRewrapped. A test reaches it by constructing an input that satisfies no other outcome's condition.
failed — Decided outside the input: storage is unavailable, or a stored version or a held batch does not decrypt with the keyring. No predicate over the input reaches this branch, and saying when: false instead would have claimed it is unreachable, which is a different and false statement. No entity in this specification changes. It reports secrets.custody.Unavailable. It emits nothing. A test reaches it by injecting the declared fault, because no input can.
Events
NamespaceSecretsListed
secrets.custody.NamespaceSecretsListed.
It carries:
tenant—Stringnamespace—String
Emitted by secrets.custody.ListNamespaceSecrets on its listed outcome.
Nothing in this system reacts to it.
NothingRewrapped
secrets.custody.NothingRewrapped.
It carries:
actor—String
Emitted by secrets.custody.RewrapSecrets on its nothing-to-rewrap outcome.
Nothing in this system reacts to it.
SecretDeleted
secrets.custody.SecretDeleted.
It carries:
reference—secrets.custody.Reference
Emitted by secrets.custody.DeleteOwnedSecret on its deleted outcome.
Emitted by secrets.custody.DeleteSecret on its deleted outcome.
Nothing in this system reacts to it.
SecretExistenceChecked
secrets.custody.SecretExistenceChecked.
It carries:
reference—secrets.custody.Reference
Emitted by secrets.custody.CheckSecretExists on its checked outcome.
Nothing in this system reacts to it.
SecretPut
secrets.custody.SecretPut.
It carries:
reference—secrets.custody.Referenceowner_subject—String
Emitted by secrets.custody.PutSecret on its created and replaced outcomes.
Nothing in this system reacts to it.
SecretRevoked
secrets.custody.SecretRevoked.
It carries:
reference—secrets.custody.Reference
Emitted by secrets.custody.RevokeOwnedSecret on its revoked outcome.
Nothing in this system reacts to it.
SecretValueRead
secrets.custody.SecretValueRead.
It carries:
reference—secrets.custody.Reference
Emitted by secrets.custody.ReadSecretValue on its read outcome.
Nothing in this system reacts to it.
SecretsRewrapped
secrets.custody.SecretsRewrapped.
It carries:
actor—String
Emitted by secrets.custody.RewrapSecrets on its rewrapped outcome.
Nothing in this system reacts to it.
TransactionAborted
secrets.custody.TransactionAborted.
It carries:
tenant—Stringtransaction—Uuid
Emitted by secrets.custody.AbortTransaction on its aborted outcome.
Nothing in this system reacts to it.
TransactionCommitted
secrets.custody.TransactionCommitted.
It carries:
tenant—Stringtransaction—Uuid
Emitted by secrets.custody.CommitTransaction on its committed outcome.
Nothing in this system reacts to it.
TransactionPrepared
secrets.custody.TransactionPrepared.
It carries:
tenant—Stringtransaction—Uuid
Emitted by secrets.custody.PrepareTransaction on its prepared outcome.
Nothing in this system reacts to it.
Errors
Conflict
- A prepared transaction already exists for this tenant and id; code
duplicate(crates/secrets-postgres/src/lib.rs:305-311; crates/secrets-http/src/lib.rs:431).
It carries nothing beyond its name, so a caller can tell what went wrong and not which value caused it.
Reported by secrets.custody.PrepareTransaction on its duplicate outcome.
Forbidden
- The principal lacks the action (code
missing-action), or its tenant differs from the addressed tenant (codeforbidden) (crates/secrets-http/src/lib.rs:346-358).
It carries nothing beyond its name, so a caller can tell what went wrong and not which value caused it.
Reported by secrets.custody.AbortTransaction on its forbidden and missing-action outcomes.
Reported by secrets.custody.CheckSecretExists on its forbidden and missing-action outcomes.
Reported by secrets.custody.CommitTransaction on its forbidden and missing-action outcomes.
Reported by secrets.custody.DeleteOwnedSecret on its forbidden and missing-action outcomes.
Reported by secrets.custody.DeleteSecret on its forbidden and missing-action outcomes.
Reported by secrets.custody.ListNamespaceSecrets on its forbidden and missing-action outcomes.
Reported by secrets.custody.PrepareTransaction on its forbidden and missing-action outcomes.
Reported by secrets.custody.PutSecret on its forbidden and missing-action outcomes.
Reported by secrets.custody.ReadSecretValue on its forbidden and missing-action outcomes.
Reported by secrets.custody.RevokeOwnedSecret on its forbidden and missing-action outcomes.
InvalidInput
- A store input rule (codes
malformed-reference,invalid-reference,empty-batch,cross-tenant-batch), a body that does not parse (malformed-body) or a path that does not parse (malformed-path) (crates/secrets-http/src/lib.rs:432-435, 485, 501).
It carries nothing beyond its name, so a caller can tell what went wrong and not which value caused it.
Reported by secrets.custody.AbortTransaction on its malformed-path outcome.
Reported by secrets.custody.CheckSecretExists on its malformed-body outcome.
Reported by secrets.custody.CommitTransaction on its malformed-reference, invalid-reference and malformed-path outcomes.
Reported by secrets.custody.DeleteOwnedSecret on its malformed-body outcome.
Reported by secrets.custody.DeleteSecret on its malformed-body outcome.
Reported by secrets.custody.ListNamespaceSecrets on its malformed-body outcome.
Reported by secrets.custody.PrepareTransaction on its empty-batch, cross-tenant-batch, malformed-body and malformed-path outcomes.
Reported by secrets.custody.PutSecret on its malformed-reference, invalid-reference and malformed-body outcomes.
Reported by secrets.custody.ReadSecretValue on its malformed-body outcome.
Reported by secrets.custody.RevokeOwnedSecret on its malformed-body outcome.
NotFound
- No record at the address (code
not-found), on user routes no secret the caller owns (codenot-owned), or a commit whose Delete names no secret (codedelete-target-missing) (crates/secrets-http/src/lib.rs:308-324, 425-430; crates/secrets-postgres/src/lib.rs:352-360).
It carries nothing beyond its name, so a caller can tell what went wrong and not which value caused it.
Reported by secrets.custody.AbortTransaction on its not-found outcome.
Reported by secrets.custody.CommitTransaction on its not-found and delete-target-missing outcomes.
Reported by secrets.custody.DeleteOwnedSecret on its not-owned outcome.
Reported by secrets.custody.DeleteSecret on its not-found outcome.
Reported by secrets.custody.ReadSecretValue on its not-found outcome.
Reported by secrets.custody.RevokeOwnedSecret on its not-owned outcome.
PayloadTooLarge
- The request body exceeds 1 MiB (RequestBodyLimitLayer, crates/secrets-http/src/lib.rs:57); refused before the handler authenticates, by the layer from the declared length or by
JsonBodyfrom the bytes read, and answered in the refusal shape with codetoo-large(crates/secrets-http/src/lib.rs:59-60, 461-469, 482-484).
It carries nothing beyond its name, so a caller can tell what went wrong and not which value caused it.
Reported by secrets.custody.CheckSecretExists on its too-large outcome.
Reported by secrets.custody.DeleteOwnedSecret on its too-large outcome.
Reported by secrets.custody.DeleteSecret on its too-large outcome.
Reported by secrets.custody.ListNamespaceSecrets on its too-large outcome.
Reported by secrets.custody.PrepareTransaction on its too-large outcome.
Reported by secrets.custody.PutSecret on its too-large outcome.
Reported by secrets.custody.ReadSecretValue on its too-large outcome.
Reported by secrets.custody.RevokeOwnedSecret on its too-large outcome.
Unauthorized
- No bearer token, or the authority refused it; code
unauthorized(crates/secrets-http/src/lib.rs:340-345, 439-444).
It carries nothing beyond its name, so a caller can tell what went wrong and not which value caused it.
Reported by secrets.custody.AbortTransaction on its unauthorized outcome.
Reported by secrets.custody.CheckSecretExists on its unauthorized outcome.
Reported by secrets.custody.CommitTransaction on its unauthorized outcome.
Reported by secrets.custody.DeleteOwnedSecret on its unauthorized outcome.
Reported by secrets.custody.DeleteSecret on its unauthorized outcome.
Reported by secrets.custody.ListNamespaceSecrets on its unauthorized outcome.
Reported by secrets.custody.PrepareTransaction on its unauthorized outcome.
Reported by secrets.custody.PutSecret on its unauthorized outcome.
Reported by secrets.custody.ReadSecretValue on its unauthorized outcome.
Reported by secrets.custody.RevokeOwnedSecret on its unauthorized outcome.
Unavailable
- Storage, crypto or authority unavailable; code
unavailable(crates/secrets-http/src/lib.rs:436, 442).
It carries nothing beyond its name, so a caller can tell what went wrong and not which value caused it.
Reported by secrets.custody.AbortTransaction on its unavailable outcome.
Reported by secrets.custody.CheckSecretExists on its unavailable outcome.
Reported by secrets.custody.CommitTransaction on its unavailable outcome.
Reported by secrets.custody.DeleteOwnedSecret on its unavailable outcome.
Reported by secrets.custody.DeleteSecret on its unavailable outcome.
Reported by secrets.custody.ListNamespaceSecrets on its unavailable outcome.
Reported by secrets.custody.PrepareTransaction on its unavailable outcome.
Reported by secrets.custody.PutSecret on its unavailable outcome.
Reported by secrets.custody.ReadSecretValue on its unavailable outcome.
Reported by secrets.custody.RevokeOwnedSecret on its unavailable outcome.
Reported by secrets.custody.RewrapSecrets on its failed outcome.
Actors
An actor is who may ask this context for something. Every grant below points at a command this specification declares — a grant is a resolved reference, so "may invoke" something nobody wrote is not a permission this model can express, and an authorisation that authorises nothing cannot ship quietly.
Operator
secrets.custody.Operator.
It may invoke RewrapSecrets.
User
secrets.custody.User.
It may invoke DeleteOwnedSecret and RevokeOwnedSecret.
Workload
secrets.custody.Workload.
It may invoke AbortTransaction, CheckSecretExists, CommitTransaction, DeleteSecret, ListNamespaceSecrets, PrepareTransaction, PutSecret and ReadSecretValue.
Type crossings
Types in this context that the specification permits to be used as another type, or the other way round. Nothing else crosses: two newtypes over the same primitive stay distinct until a line in the specification says otherwise.
Optional<secrets.custody.Disclosure> may be used as secrets.custody.Disclosure, because:
An absent disclosure is
workload_only(#[serde(default = "workload_only")], crates/secrets-core/src/lib.rs:52-53).
Every crossing in the system is on one page: Type crossings.
Generated from secrets v1 · model digest a400890747eb193dcba0b845cca169bfb1ded00af40933596fb0f3cfe7dd593b · contract digest slice-sha256/2:eb2b2fa175f064351382d36e78be9b9ecc892f199a15384ba55ef727bd14fec7. Do not edit this file; change the specification and regenerate it with task docs-generate.