Relyra.Metadata.DriftDetector (relyra v1.1.0)

Copy Markdown View Source

Drift detection for Phase 21 scheduled metadata refresh per D-18.

A "drift" is either:

  • the freshly-fetched entityID does not match the persisted idp_entity_id on the connection (:entity_id_drift), or
  • the freshly-fetched signing-cert fingerprint set contains an element NOT in the persisted last_known_metadata_signing_certs MapSet (:new_signing_cert).

On drift, the wrapper auto-suspends the source and refuses to apply this revision. Per D-32, the new cert still stages as :next via the existing certificate-inventory path — drift detection ONLY pauses the scheduled apply pending operator review (Phase 10/12 D-08 unchanged).

Why fingerprints, not PEMs: comparing PEM strings is whitespace-sensitive (RESEARCH Pitfall 7). A metadata reformat that re-emits the same cert with different line wrapping would re-fire :new_signing_cert. Compare MapSets of SHA-256 fingerprints only.

Pure: no I/O, no Ecto. Caller passes already-fingerprint-extracted lists.

Summary

Functions

Compares a freshly-parsed metadata candidate against the stored connection + source state.

Types

drift_result()

@type drift_result() ::
  {:ok, :no_drift}
  | {:drift,
     %{
       entity_id_changed?: boolean(),
       new_signing_certs: [String.t()],
       reason: :entity_id_drift | :new_signing_cert
     }}

Functions

diff(candidate, source_state)

@spec diff(map(), map()) :: drift_result()

Compares a freshly-parsed metadata candidate against the stored connection + source state.

candidate:

  • :idp_entity_id (string)
  • :certificate_fingerprints (list of SHA-256 hex strings)

source_state:

  • :idp_entity_id (string — from the Connection, not the source row)
  • :last_known_metadata_signing_certs (list of SHA-256 hex strings — from MetadataSource)

Returns {:ok, :no_drift} or {:drift, %{reason: ...}}.