Source code for aeat.adapters.outbound.aeat.auth._authenticator_persistence
"""Certificate-auth persisted-session metadata and redacted diagnostics.:class:`adapters.outbound.aeat.auth.AeatAuthenticator` writes:class:`PersistedSessionMetadata` into the encrypted:class:`adapters.outbound.aeat.auth._session_store.PersistedBrowserSession`metadata mapping after capturing Playwright storage state. Resume paths usethe metadata to validate the storage-state fingerprint, idle deadline,certificate thumbprint, and certificate subject before rebuilding the session.The reason-code helpers reduce detailed invalidation causes to stable,non-sensitive strings carried through :class:`AeatLoginAssertionError`."""from__future__importannotationsfromcollections.abcimportMappingfromdatetimeimportdatetimefromtypingimportFinalfrompydanticimportBaseModel,Fieldfrom.....coreimportSTRICT_FROZEN_CONFIGfrom._errorsimportAeatLoginAssertionErrorfrom.certificateimportHandshakeResultAEAT_STORAGE_STATE_SCHEMA_VERSION:Final[int]=1"""Schema version for certificate-auth :class:`PersistedSessionMetadata` records."""
[docs]classPersistedSessionMetadata(BaseModel):"""Certificate-auth metadata stored inside the encrypted session envelope. The fields bind a captured Playwright storage state to the certificate identity that produced it. :class:`HandshakeResult` preserves the verified AEAT handshake details, while ``storage_state_sha256`` lets resume checks reject metadata that no longer matches the encrypted storage-state payload. """model_config=STRICT_FROZEN_CONFIGschema_version:int=Field(default=AEAT_STORAGE_STATE_SCHEMA_VERSION,ge=1)certificate_thumbprint:str=Field(min_length=1)certificate_subject:str=Field(min_length=1)certificate_nif:str=Field(min_length=1)authenticated_at:datetimeidle_deadline:datetimestorage_state_sha256:str=Field(min_length=64,max_length=64)handshake:HandshakeResult
[docs]defpersisted_session_reason_code(reason:str)->str:"""Map a detailed persisted-session refusal reason to a non-sensitive code. The mapping mirrors the certificate-auth resume gates and storage-state parsing checks so callers can log or translate the outcome without exposing certificate subjects, logical storage paths, or browser-session contents. """reason_lower=reason.lower()if"hash does not match"inreason_lower:return"storage_hash_mismatch"if"past its idle deadline"inreason_lower:return"idle_deadline_expired"if"different certificate thumbprint"inreason_lower:return"certificate_thumbprint_mismatch"if"different certificate subject"inreason_lower:return"certificate_subject_mismatch"if"failed live verification"inreason_lower:return"live_verification_failed"if"could not be resumed"inreason_lower:return"resume_failed"if"storage_state missing"inreason_lower:return"storage_state_missing"if"storage_state is malformed"inreason_lower:return"storage_state_malformed"if"storage_state root"inreason_lower:return"storage_state_root_invalid"if"cookies array"inreason_lower:return"storage_state_cookies_missing"if"origins array"inreason_lower:return"storage_state_origins_missing"if"metadata is malformed"inreason_lower:return"metadata_malformed"if"schema version"inreason_lower:return"schema_version_unsupported"return"invalid_persisted_session"
[docs]defpersisted_session_reason_from_error(error:AeatLoginAssertionError)->str:"""Extract the redacted persisted-session reason code from an auth error. Returns the explicit ``context["reason"]`` value when :class:`AeatLoginAssertionError` carries one, otherwise falls back to the generic persisted-session invalidation code. """context=getattr(error,"context",None)ifisinstance(context,Mapping):reason=context.get("reason")ifisinstance(reason,str)andreason:returnreasonreturn"invalid_persisted_session"