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.

Return type:

bytes

Parameters:

stored (bytes)

class AttachmentStore(**data)[source]

Bases: BaseModel

Encrypted SQL-backed content-addressed attachment store.

Implements AttachmentStoreProtocol by storing raw document bytes under their SHA-256 digest in adapters.persistence.storage.secure_object_namespaces.ATTACHMENT_BLOB_NAMESPACE and encrypted Attachment manifests in adapters.persistence.storage.secure_object_namespaces.ATTACHMENT_MANIFEST_NAMESPACE. Both namespaces are profile-local FINANCIAL custody surfaces; the SecureObjectRepository encrypts 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
property blobs_dir: Path

Return the logical marker for the attachment blob namespace.

property manifests_dir: Path

Return the logical marker for the attachment manifest namespace.

manifest_path(attachment_id)[source]

Return a logical object marker for attachment_id.

Return type:

Path

Parameters:

attachment_id (str)

put_bytes(data)[source]

Write data under its SHA-256 digest in the blob namespace.

Return type:

str

Parameters:

data (bytes)

put_file(source)[source]

Read source and store it via put_bytes(), deduplicating by digest.

Return type:

tuple[str, int]

Parameters:

source (Path)

read_bytes(sha256)[source]

Return the raw bytes for sha256.

Return type:

bytes

Parameters:

sha256 (str)

verify_blob(attachment_id)[source]

Re-hash the stored blob and verify it matches attachment_id.

Return type:

None

Parameters:

attachment_id (str)

write_manifest(attachment)[source]

Persist attachment as an encrypted manifest envelope.

Return type:

None

Parameters:

attachment (Attachment)

load_manifest(attachment_id)[source]

Load and validate the Attachment manifest envelope.

Return type:

Attachment

Parameters:

attachment_id (str)

iter_manifests()[source]

Iterate over every Attachment manifest 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 at write_manifest(), so a listed size cannot have been admitted unverified.

The key binding is what iteration previously lacked. load_manifest passes the row key into the decoder, so a manifest whose embedded sha256 drifted 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 manifest load_manifest rejected was listed as though it were sound.

Return type:

Iterator[Attachment]

resolve_attachment_store(store)[source]

Return the injected byte-custody port, or construct the default concrete store.

Every service that accepts an optional AttachmentStoreProtocol so 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 owns AttachmentStore – 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, or None to take the default.

Return type:

AttachmentStoreProtocol

Returns:

store unchanged when one was injected, otherwise a new AttachmentStore bound to the active bucket’s runtime.