cadrumo.application.user_profile.capsule_archive module

Back a profile up to a sealed archive, and bring it back on another machine.

The archive is the operator’s answer to “this machine is gone”. It carries a published capsule’s custody material and its encrypted database, and it restores through the SAME publication path a directory restore uses: both readers produce a ProfileCapsuleSource, and exactly one thing knows how to turn that into a published profile.

Nothing here encrypts anything, and that is the design rather than an omission. Every member is already ciphertext under the operator’s password: the database is authenticated-encrypted under the profile’s data key, and the password envelope and sentinel protect that data key under supervised Argon2id. Recovery is deliberately external. Wrapping the bundle a second time under the same password would double the derivation cost at both ends, guard nothing an attacker was not already facing, and mint a second key schedule over one secret. The archive is exactly as strong as the password that already protects the profile, and no stronger claim is made for it.

Three properties are enforced rather than documented.

The member set is INVARIANT. The schema-v1 recovery slot remains constant-size but is required to carry the absent marker. A recovery wrapper is never backup cargo; a restored profile enrols recovery again explicitly.

The label never enters the archive. It lives in the published capsule as a plaintext projection beside the ciphertext, so an archive built by copying the capsule directory would publish the operator’s chosen label to anyone who can read a tar. Building from the capsule source excludes it structurally, because that reader does not read it; the label is supplied again at import.

Identity is preserved verbatim. Bucket identity IS profile identity, so an import that minted a new one would have cloned the profile rather than restored it.

RECOVERY_SLOT_BYTES: Final[int]

Constant width of the recovery slot, enrolled or not.

Sized well above a real recovery wrapper (684 bytes measured) so the padding never has to be revisited for an ordinary record, and refused rather than truncated if one ever exceeds it. The width is what makes enrolment un-inferable: a slot that shrank when absent would signal the absence just as loudly as omitting it.

PROFILE_CAPSULE_ARCHIVE_MAX_PAYLOAD_BYTES: Final[int]

The largest capsule-archive payload this product will write.

Public because it is a CROSS-LAYER contract, not an internal bound: the sealed-archive reader in the storage adapter sets its own member ceiling at or above this value, so no archive this product produced can carry a member the reader refuses. The adapter deliberately keeps its own constant rather than importing this one – an adapter importing the application layer would invert the dependency – so the two are held equal by a test, and that test needs a public name to compare against.

exception ProfileCapsuleArchiveError(message=None, *, context=None, translated_message=None)[source]

Bases: CadrumoError

Raised when an archive cannot be written or cannot be read back.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
class ProfileCapsuleArchiveReceipt(**data)[source]

Bases: BaseModel

Non-secret record of one archive export.

Parameters:

data (Any)

bucket_id: BucketId
target: str
archive_schema_version: int
recovery_enrolled: bool

Whether the source profile was enrolled when the archive was made.

Recovery itself is never archive cargo; this receipt reports only the source state already visible to the authenticated local operator.

class ProfileCapsuleArchiveInspection(**data)[source]

Bases: BaseModel

What an archive discloses without any key at all.

Exactly the plaintext header. Every field here is readable by anyone holding the file, so nothing may be added to it that is a fact about the operator rather than about the archive.

Parameters:

data (Any)

product: str
bucket_id: BucketId
archive_schema_version: int
created_at: datetime
manifest_digest: str
export_profile_capsule_archive(*, profile_id, target, root=None, now=None)[source]

Write the published capsule for profile_id to a sealed archive.

Parameters:
  • profile_id (UUID) – The published profile to back up.

  • target (Path) – Destination archive path. Refused if it already exists.

  • root (Path | None) – Storage root override; the effective root when omitted.

  • now (datetime | None) – Creation instant for the header; the clock when omitted.

Return type:

ProfileCapsuleArchiveReceipt

Returns:

A ProfileCapsuleArchiveReceipt naming what was written.

Raises:

ProfileCapsuleArchiveError – When the capsule is not published, or a member does not fit the archive’s invariant layout.

inspect_profile_capsule_archive(source)[source]

Report what source discloses without decrypting anything.

Reads the plaintext header only. This is what an operator can learn about an archive they are unsure of, and equally what anyone else can learn from a copy of it.

Return type:

ProfileCapsuleArchiveInspection

Parameters:

source (Path)

read_profile_capsule_archive(source)[source]

Return the restorable capsule material carried by source.

The digest check below is the ONLY integrity check the payload gets. The sealed-archive transport is deliberately opaque: it stores and returns the bytes it was handed verbatim, names no payload type, and carries the header’s manifest_digest without verifying it. So this is not belt-and-braces over a self-validating container – match the header’s digest against the payload it labels here, before any member is parsed, or a truncated or edited archive is partially believed.

The result is the same type a capsule DIRECTORY produces, which is what lets an import and a directory restore converge on one publication path instead of growing a second one.

Raises:

ProfileCapsuleArchiveError – When the archive’s digest does not match its payload, or a member is missing or will not parse.

Return type:

ProfileCapsuleSource

Parameters:

source (Path)