# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/under-what-conditions-must-an-instance-reject-a-signed-agent-envelope-whose-nonce-is-valid-but-whose-session-binding-is-stale.md
# Remnant Warrant:    https://corpusfetch.com/c/under-what-conditions-must-an-instance-reject-a-signed-agent-envelope-whose-nonce-is-valid-but-whose-session-binding-is-stale.warrant.json
# Markdown SHA-256:   e77ccb2ba23cf4a9fcd0e24df24ae59b2498e3005e4a69fe8ffc9ae6dd2ca2f0
# Code AST SHA-256:   a998c8ebb308444257481337664704a456d71057739636dd9aed19ca1b6e4be1
# Verified Classes:   StaleSessionEnvelopeError, EnvelopeRejected
# Defensive Guards:   6 fail-closed invariants (zero external deps)

class StaleSessionEnvelopeError(RuntimeError):
    """Raised when an envelope's signature and nonce are valid but its
    session binding is no longer live. Distinct from replay/nonce errors
    so callers remediate by re-establishing a session, not re-noncing."""


class EnvelopeRejected(RuntimeError):
    """Ordinary replay or malformed-envelope rejection."""


def verify_envelope(envelope, session_store, dedup_store, keyring, now):
    # 1. Signature must verify under a key valid for the claimed sender.
    key = keyring.verification_key(envelope.sender_id, envelope.key_id)
    if key is None or not key.verify(envelope.signing_bytes(), envelope.signature):
        raise EnvelopeRejected("signature invalid or key unknown")

    # 2. Session liveness FIRST, against authoritative state. A key in the
    #    rotation overlap window proves authenticity, NOT liveness.
    binding = session_store.authoritative_binding(
        envelope.sender_id, envelope.receiver_id, envelope.session_id
    )
    if binding is None or not binding.is_live(now):
        # Signature is genuine, nonce may be unseen, but the session is retired.
        raise StaleSessionEnvelopeError(
            f"session {envelope.session_id} between "
            f"{envelope.sender_id}->{envelope.receiver_id} is not live"
        )

    # 3. Timestamp skew check against the live binding's clock policy.
    if abs(now - envelope.timestamp) > binding.max_skew:
        raise EnvelopeRejected("timestamp outside skew tolerance")

    # 4. Nonce dedup. A MISSING table is a hard failure, never "fresh".
    table = dedup_store.table_for(envelope.session_id)
    if table is None:
        # Dedup state was dropped; freshness is unverifiable. Fail closed.
        raise StaleSessionEnvelopeError(
            f"nonce dedup state missing for session {envelope.session_id}; "
            "cannot establish freshness"
        )
    if not table.consume_if_unseen(envelope.nonce):
        raise EnvelopeRejected(
            f"nonce {envelope.nonce} already consumed for session "
            f"{envelope.session_id} (replay)"
        )

    # 5. Invariant guard: dedup retention must cover key overlap + skew.
    if dedup_store.retention(envelope.session_id) < (
        keyring.overlap(envelope.key_id) + binding.max_skew
    ):
        # Configuration drift: the replay window has reopened. Refuse.
        raise StaleSessionEnvelopeError(
            "dedup_ttl < key_overlap + max_skew; replay window open"
        )

    return envelope.payload
