cadrumo.adapters.persistence.storage.attachment module¶
Encrypted SQL-backed content-addressed attachment store implementation.
Concrete adapter-layer implementation of the
AttachmentStoreProtocol. The
domain declares the protocol; this module provides the implementation that
reads/writes encrypted attachment blobs and manifests through the
SecureObjectRepository persistence
substrate. Blob rows are framed byte payloads governed by
adapters.persistence.storage.secure_object_namespaces.ATTACHMENT_BLOB_NAMESPACE; manifest
rows wrap Attachment payloads in
Envelope records governed by
adapters.persistence.storage.secure_object_namespaces.ATTACHMENT_MANIFEST_NAMESPACE.
Sensitivity rationale: attachment blobs and manifests are content-addressed
byte objects (invoice PDFs, bank statements, supporting documents) that are
FINANCIAL regardless of the modelo that triggered the upload. Attachments are
not modelo-scoped - a single blob may be referenced from multiple modelos and
filing revisions. The ModeloDefinition.output_sensitivity field governs
model output artefacts; attachment storage is an independent content-
addressed substrate and its sensitivity class is irreducibly FINANCIAL.
- unwrap_blob_payload(stored)[source]¶
Strip the envelope prefix from a stored blob; refuse an un-enveloped payload.
Every blob is wrapped by
_wrap_blob_payload()at write time, so a missing prefix can only mean corruption, never valid data. Refuse it rather than returning unframed bytes.
- class AttachmentStore(**data)[source]¶
Bases:
BaseModelEncrypted SQL-backed content-addressed attachment store.
Implements
AttachmentStoreProtocolby storing raw document bytes under their SHA-256 digest inadapters.persistence.storage.secure_object_namespaces.ATTACHMENT_BLOB_NAMESPACEand encryptedAttachmentmanifests inadapters.persistence.storage.secure_object_namespaces.ATTACHMENT_MANIFEST_NAMESPACE. Both namespaces are profile-local FINANCIAL custody surfaces; theSecureObjectRepositoryencrypts the stored rows and HMAC-digests the object keys.- Parameters:
objects (SecureObjectRepository | None)
bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)] | None)
- objects: SecureObjectRepository | None¶
- bucket_id: BucketId | None¶
- manifest_path(attachment_id)[source]¶
Return a logical object marker for
attachment_id.- Return type:
Path- Parameters:
attachment_id (str)
- put_file(source)[source]¶
Read
sourceand store it viaput_bytes(), deduplicating by digest.
- write_manifest(attachment)[source]¶
Persist
attachmentas an encrypted manifest envelope.- Return type:
- Parameters:
attachment (Attachment)
- load_manifest(attachment_id)[source]¶
Load and validate the
Attachmentmanifest envelope.- Return type:
- Parameters:
attachment_id (str)
- iter_manifests()[source]¶
Iterate over every
Attachmentmanifest in sorted attachment-id order.Each manifest is bound to the row it was stored under and checked to reference bytes this store actually holds. The presence check is a key lookup rather than the full length/digest reproduction
load_manifest()performs: iteration is the listing path, and re-reading every blob would decrypt the whole evidence corpus to render a list. The declared size is bound to the payload atwrite_manifest(), so a listed size cannot have been admitted unverified.The key binding is what iteration previously lacked.
load_manifestpasses the row key into the decoder, so a manifest whose embeddedsha256drifted from the key it is filed under is refused there. Iteration had no key to pass and derived the identity from that same embedded field instead, which is self-consistent by construction – so the one surface that could not detect the drift was the one that enumerates the whole corpus, and a tampered manifestload_manifestrejected was listed as though it were sound.- Return type:
- resolve_attachment_store(store)[source]¶
Return the injected byte-custody port, or construct the default concrete store.
Every service that accepts an optional
AttachmentStoreProtocolso a test can inject a real store into an isolated profile needs the same fallback, and that fallback names a concrete adapter. Resolving it here – in the module that ownsAttachmentStore– keeps the construction to one site. A copy per consuming package looks harmless while the constructor takes no arguments and drifts the moment it takes one; two such copies had already appeared, in the ledger action services and in the live notification custody service, and neither package owns the class.- Parameters:
store (
AttachmentStoreProtocol|None) – The caller’s injected port, orNoneto take the default.- Return type:
- Returns:
storeunchanged when one was injected, otherwise a newAttachmentStorebound to the active bucket’s runtime.