        attempts = tuple(
            RecoveryAttempt(
                _required_string(item, "session_id"),
                RecoverySlot(_required_string(item, "slot")),
                _telegram_id(item["chat_id"], "attempt chat"),
                _required_int(item, "generation"),
                _required_datetime(item, "created_at"),
            )
            for item in attempt_values
        )
        consent_card_message_id = document["consent_card_message_id"]
        failure_code = document["failure_code"]
        session = RoomBootstrapSession(
            session_id=_required_string(document, "session_id"),
            sid_hash=_required_string(document, "sid_hash"),
            customer_draft=draft,
            customer_draft_digest=_required_string(
                document, "customer_draft_digest"
            ),
            bot_username=_required_string(document, "bot_username"),
            state=BootstrapState(_required_string(document, "state")),
            generation=_required_int(document, "generation"),
            created_at=_required_datetime(document, "created_at"),
            updated_at=_required_datetime(document, "updated_at"),
            expires_at=_required_datetime(document, "expires_at"),
            owner_id=_telegram_id(document["owner_id"], "owner"),
            role_claims=claims,
            consent_publication_attempt=_required_int(
                document, "consent_publication_attempt"
            ),
            consent_card_message_id=(
                None
                if consent_card_message_id is None
                else _telegram_id(consent_card_message_id, "consent receipt")
            ),
            recovery_attempt_generation=_required_int(
                document, "recovery_attempt_generation"
            ),
            recovery_attempts=attempts,
            failure_code=(
                None
                if failure_code is None
                else _strict_string(failure_code, "failure_code")
            ),
        )
    except (KeyError, TypeError, ValueError) as exc:
        raise BootstrapError("customer bootstrap session value is invalid") from exc
    if (
        _SESSION_RE.fullmatch(session.session_id) is None
        or _DIGEST_RE.fullmatch(session.sid_hash) is None
        or session.customer_draft_digest != session.customer_draft.digest
        or session.generation < 1
        or session.consent_publication_attempt < 0
        or session.recovery_attempt_generation < 0
        or any(item.session_id != session.session_id for item in session.recovery_attempts)
        or any(item.created_at.tzinfo is None for item in session.recovery_attempts)
        or any(
            timestamp.tzinfo is None
            for timestamp in (session.created_at, session.updated_at, session.expires_at)
        )
    ):
        raise BootstrapError("customer bootstrap session authority is invalid")
    return session

def _placeholder_session() -> RoomBootstrapSession:
    now = datetime(2000, 1, 1, tzinfo=timezone.utc)
    draft = CustomerDraft("x", "x", "2000-01-01", "08:00", 0, 1, 1, 1, ("x",))
    return RoomBootstrapSession(
        "cb_0000000000000000000000",
        "0" * 64,
        draft,
        draft.digest,
        "ownerbot",
        BootstrapState.PREPARED,
        1,
        now,
        now,
        now,
        "1",
    )

def _draft_from_mapping(value: object) -> CustomerDraft:
    if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
        raise BootstrapError("customer draft is invalid")
    document = cast(Mapping[str, object], value)
    required = _DRAFT_REQUIRED
    allowed = required | _DRAFT_OPTIONAL
    if set(document) - allowed or required - set(document):
        raise BootstrapError("customer draft fields are invalid")
    defaults = CustomerDraft(
        "placeholder",
        "placeholder",
        "2000-01-01",
        "08:00",
        0,
        1,
        1,
        1,
        ("placeholder",),
    ).to_dict()
    values = cast(Mapping[str, object], {**defaults, **dict(document)})
    try:
        draft = CustomerDraft(
            customer_key=_required_string(values, "customer_key"),
            display_name=_required_string(values, "display_name"),
            starts_on=_required_string(values, "starts_on"),
            daily_time=_required_string(values, "daily_time"),
            weekly_weekday=_required_int(values, "weekly_weekday"),
            monthly_day=_required_int(values, "monthly_day"),
            calories_kcal=_required_int(values, "calories_kcal"),
            protein_g=_required_int(values, "protein_g"),
            meals=_string_tuple(values, "meals"),
            primary_goal=_required_string(values, "primary_goal"),
            dietary_restrictions=_string_tuple(values, "dietary_restrictions"),
            allergies=_string_tuple(values, "allergies"),
            food_preferences=_string_tuple(values, "food_preferences"),
            supplements=_string_tuple(values, "supplements"),
            digestion_context=_optional_string(values, "digestion_context"),
            sleep_goal_hours=_optional_float(values, "sleep_goal_hours"),
            recovery_goal=_optional_string(values, "recovery_goal"),
            training_context=_optional_string(values, "training_context"),
            carbohydrate_g=_optional_int(values, "carbohydrate_g"),
            fat_g=_optional_int(values, "fat_g"),
            water_liters=_optional_float(values, "water_liters"),
            customer_user_id=_optional_string(values, "customer_user_id"),
        )
    except (TypeError, ValueError) as exc:
        raise BootstrapError("customer draft value is invalid") from exc
    if (
        not draft.customer_key
        or not draft.display_name
        or not draft.meals
        or type(draft.weekly_weekday) is not int
        or not 0 <= draft.weekly_weekday <= 6
        or type(draft.monthly_day) is not int
        or not 1 <= draft.monthly_day <= 28
        or type(draft.calories_kcal) is not int
        or draft.calories_kcal <= 0
        or type(draft.protein_g) is not int
        or draft.protein_g <= 0
    ):
        raise BootstrapError("customer draft value is invalid")
    return draft

def _require_private_regular_file(path: Path, label: str) -> None:
    if path.is_symlink():
        raise BootstrapError(f"{label} must not be a symlink")
    try:
        info = path.stat()
    except OSError as exc:
        raise BootstrapError(f"{label} is unavailable") from exc
    if (
        not stat.S_ISREG(info.st_mode)
        or info.st_nlink != 1
        or stat.S_IMODE(info.st_mode) & 0o077
        or info.st_size > 1_048_576
    ):
        raise BootstrapError(f"{label} must be one private regular file")

def _telegram_id(value: object, label: str) -> str:
    text = str(value)
    if _ID_RE.fullmatch(text) is None or str(int(text)) != text:
        raise BootstrapError(f"{label} Telegram identity is invalid")
    return text

def _canonical_json(value: object) -> str:
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    )

def _canonical_bytes(value: object) -> bytes:
    return _canonical_json(value).encode("utf-8")

def _sha256(value: bytes) -> str:
    return hashlib.sha256(value).hexdigest()

__all__ = [
    "BootstrapConflict",
    "BootstrapError",
    "BootstrapState",
    "CustomerDraft",
    "GenerationConflict",
    "PreparedRehearsalCustomerInvite",
    "RecoveryAttempt",
    "RecoverySlot",
    "Role",
    "RoleClaim",
    "RoomBootstrapSession",
    "RoomBootstrapStore",
    "RoomBootstrapTransport",
    "load_customer_draft",
    "room_bootstrap_state_dir",
]

=== hermes:gateway/platforms/telegram_staff_membership_gate.py ===
"""Strict, read-only proof that a customer is absent from every staff chat."""

from __future__ import annotations

import fcntl
import hashlib
import json
import os
import stat
import uuid
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from enum import Enum
from pathlib import Path
from typing import Protocol

EVIDENCE_SCHEMA = "telegram-staff-membership-evidence-v1"
JOURNAL_SCHEMA = "telegram-staff-membership-journal-v1"
ABSENT_STATUSES = frozenset({"left", "kicked"})
ADMIN_STATUSES = frozenset({"administrator", "creator"})
DEFAULT_MAX_AGE = timedelta(minutes=5)

class MembershipGateError(ValueError):
    """Membership evidence is absent, stale, contradictory, or unverifiable."""

class StaffChatKind(str, Enum):
    MEMBERSHIP_CHAT = "membership_chat"
    PRIVATE_STAFF_DM = "private_staff_dm"

class ReadOnlyMembershipBot(Protocol):
    async def get_me(self) -> object: ...

    async def get_chat_member(self, chat_id: int, user_id: int) -> object: ...

@dataclass(frozen=True, slots=True)
class StaffChat:
    chat_id: int
    kind: StaffChatKind
    staff_user_id: int | None
    roles: tuple[str, ...]
    sources: tuple[str, ...]

    def to_dict(self) -> dict[str, object]:
        result: dict[str, object] = {
            "chat_id": str(self.chat_id),
            "kind": self.kind.value,
            "roles": list(self.roles),
            "sources": list(self.sources),
        }
        if self.staff_user_id is not None:
            result["staff_user_id"] = str(self.staff_user_id)
        return result

@dataclass(frozen=True, slots=True)
class StaffChatInventory:
    rows: tuple[StaffChat, ...]
    sha256: str

    def to_list(self) -> list[dict[str, object]]:
        return [row.to_dict() for row in self.rows]

def canonical_json(value: object) -> bytes:
    return json.dumps(
        value, ensure_ascii=True, sort_keys=True, separators=(",", ":")
    ).encode("utf-8")

def sha256_json(value: object) -> str:
    return hashlib.sha256(canonical_json(value)).hexdigest()

def sha256_file(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()

def _mapping(value: object, label: str) -> Mapping[str, object]:
    if not isinstance(value, Mapping) or not all(
        isinstance(key, str) for key in value
    ):
        raise MembershipGateError(f"{label} is invalid")
    return value

def _integer(value: object, label: str) -> int:
    if isinstance(value, bool) or not isinstance(value, (int, str)):
        raise MembershipGateError(f"{label} is invalid")
    try:
        result = int(value)
    except (TypeError, ValueError) as exc:
        raise MembershipGateError(f"{label} is invalid") from exc
    if str(result) != str(value).strip() or result == 0:
        raise MembershipGateError(f"{label} is invalid")
    return result

def _telegram_extra(config: Mapping[str, object]) -> Mapping[str, object]:
    platforms = config.get("platforms")
    if isinstance(platforms, Mapping):
        telegram = platforms.get("telegram")
        if isinstance(telegram, Mapping):
            extra = telegram.get("extra", telegram)
            return _mapping(extra, "Telegram configuration")
    telegram = config.get("telegram")
    if isinstance(telegram, Mapping):
        return _mapping(telegram.get("extra", telegram), "Telegram configuration")
    return config

@dataclass(slots=True)
class _InventoryAccumulator:
    chat_id: int
    kind: StaffChatKind
    staff_user_id: int | None
    roles: set[str]
    sources: set[str]

def build_staff_chat_inventory(
    registry: Mapping[str, object], config: Mapping[str, object]
) -> StaffChatInventory:
    """Enumerate and canonicalize every staff-accessible Telegram route."""
    rows: dict[int, _InventoryAccumulator] = {}

    def add(
        chat_value: object,
        *,
        source: str,
        role: str,
        user_value: object | None = None,
        private_if_positive: bool = True,
    ) -> None:
        chat_id = _integer(chat_value, source)
        kind = (
            StaffChatKind.PRIVATE_STAFF_DM
            if chat_id > 0 and private_if_positive
            else StaffChatKind.MEMBERSHIP_CHAT
        )
        staff_user_id: int | None = None
        if kind is StaffChatKind.PRIVATE_STAFF_DM:
            staff_user_id = (
                chat_id if user_value is None else _integer(user_value, f"{source}.user_id")
            )
            if staff_user_id != chat_id:
                raise MembershipGateError(f"{source} private DM identity is contradictory")
        existing = rows.get(chat_id)
        if existing is not None:
            if existing.kind is not kind or existing.staff_user_id != staff_user_id:
                raise MembershipGateError(f"{source} staff route is contradictory")
            existing.roles.add(role)
            existing.sources.add(source)
            return
        rows[chat_id] = _InventoryAccumulator(
            chat_id, kind, staff_user_id, {role}, {source}
        )

    owner = _mapping(registry.get("owner"), "registry.owner")
    add(
        owner.get("chat_id"),
        source="registry.owner",
        role="owner",
        user_value=owner.get("user_id"),
    )
    extra = _telegram_extra(_mapping(config, "configuration"))
    nutrition = extra.get("nutrition_coaching")
    if nutrition is not None:
        nutrition_map = _mapping(nutrition, "nutrition_coaching")
        for key, role in (
            ("operator_review", "operator_review"),
            ("operator_card_route", "operator_card"),
        ):
            route = nutrition_map.get(key)
            if route is not None:
                route_map = _mapping(route, f"nutrition_coaching.{key}")
                add(
                    route_map.get("chat_id"),
                    source=f"platforms.telegram.extra.nutrition_coaching.{key}",
                    role=role,
                    user_value=route_map.get("user_id"),
                )
    adaptive = extra.get("adaptive_nutrition")
    if adaptive is not None:
        adaptive_map = _mapping(adaptive, "adaptive_nutrition")
        route = adaptive_map.get("review_operator")
        if route is not None:
            route_map = _mapping(route, "adaptive_nutrition.review_operator")
            add(
                route_map.get("chat_id"),
                source="platforms.telegram.extra.adaptive_nutrition.review_operator",
                role="adaptive_review",
                user_value=route_map.get("user_id"),
            )
        legacy_chat = adaptive_map.get("operator_chat_id")
        if legacy_chat is not None:
            add(
                legacy_chat,
                source="platforms.telegram.extra.adaptive_nutrition.operator_chat_id",
                role="adaptive_review",
                private_if_positive=False,
            )
    for key, role in (
        ("group_allowed_chats", "gateway_group"),
        ("allowed_chats", "gateway_allowed"),
    ):
        values = extra.get(key, ())
        if isinstance(values, str):
            values = tuple(item.strip() for item in values.split(",") if item.strip())
        if not isinstance(values, Sequence) or isinstance(values, (bytes, bytearray)):
            raise MembershipGateError(f"{key} is invalid")
        for value in values:
            if _integer(value, f"platforms.telegram.extra.{key}") > 0:
                raise MembershipGateError(
                    f"platforms.telegram.extra.{key} has an unknown positive route"
                )
            add(
                value,
                source=f"platforms.telegram.extra.{key}",
                role=role,
                private_if_positive=True,
            )
    topics = extra.get("group_topics", ())
    if not isinstance(topics, Sequence) or isinstance(topics, (str, bytes, bytearray)):
        raise MembershipGateError("group_topics is invalid")
    for topic in topics:
        topic_map = _mapping(topic, "group_topics entry")
        if _integer(topic_map.get("chat_id"), "group_topics.chat_id") > 0:
            raise MembershipGateError("group_topics has a positive group route")
        add(
            topic_map.get("chat_id"),
            source="platforms.telegram.extra.group_topics",
            role="group_topic",
            private_if_positive=False,
        )
    canonical_rows = tuple(
        StaffChat(
            item.chat_id,
            item.kind,
            item.staff_user_id,
            tuple(sorted(item.roles)),
            tuple(sorted(item.sources)),
        )
        for item in sorted(rows.values(), key=lambda item: item.chat_id)
    )
    if not canonical_rows:
        raise MembershipGateError("staff chat inventory is empty")
    encoded = [row.to_dict() for row in canonical_rows]
    return StaffChatInventory(canonical_rows, sha256_json(encoded))

async def verify_subscription_readiness(
    bot: ReadOnlyMembershipBot,
    inventory: StaffChatInventory,
) -> int:
    """Require the bot to observe all membership chats as an administrator."""
    try:
        identity = await bot.get_me()
        bot_user_id = getattr(identity, "id")
    except Exception as exc:
        raise MembershipGateError("bot identity query failed") from exc
    if type(bot_user_id) is not int or bot_user_id <= 0:
        raise MembershipGateError("bot identity query failed")
    for row in inventory.rows:
        if row.kind is not StaffChatKind.MEMBERSHIP_CHAT:
            continue
        try:
            status = str(getattr(await bot.get_chat_member(row.chat_id, bot_user_id), "status"))
        except Exception as exc:
            raise MembershipGateError("bot administrator readiness query failed") from exc
        if status not in ADMIN_STATUSES:
            raise MembershipGateError(
                "bot is not administrator in every staff membership chat"
            )
    return bot_user_id

async def observe_customer_absence(
    bot: ReadOnlyMembershipBot,
    inventory: StaffChatInventory,
    *,
    customer_user_id: int,
) -> dict[str, object]:
    """Query only Bot identity and chat membership, failing on every uncertainty."""
    if type(customer_user_id) is not int or customer_user_id <= 0:
        raise MembershipGateError("customer Telegram identity is invalid")
    bot_user_id = await verify_subscription_readiness(bot, inventory)
    membership_rows = tuple(
        row for row in inventory.rows if row.kind is StaffChatKind.MEMBERSHIP_CHAT
    )
    results: list[dict[str, str]] = []
    for row in membership_rows:
        try:
            status = str(
                getattr(
                    await bot.get_chat_member(row.chat_id, customer_user_id),
                    "status",
                )
            )
        except Exception as exc:
            raise MembershipGateError("customer membership query failed") from exc
        if status not in ABSENT_STATUSES:
            raise MembershipGateError("customer is not absent from every staff chat")
        results.append({"chat_id": str(row.chat_id), "status": status})
    private_results: list[dict[str, object]] = []
    for row in inventory.rows:
        if row.kind is not StaffChatKind.PRIVATE_STAFF_DM:
            continue
        separated = (
            row.chat_id > 0
            and row.staff_user_id == row.chat_id
            and customer_user_id != row.chat_id
        )
        if not separated:
            raise MembershipGateError("customer identity overlaps a private staff DM")
        private_results.append(
            {
                "chat_id": str(row.chat_id),
                "identity_separated": True,
                "staff_user_id": str(row.staff_user_id),
            }
        )
    return {
        "bot_user_id_sha256": hashlib.sha256(str(bot_user_id).encode()).hexdigest(),
        "membership_results": results,
        "private_dm_results": private_results,
    }

class MembershipJournal:
    """Append-only mode-0600 hash chain for sanitized subscription events."""

    def __init__(self, path: Path) -> None:
        self.path = Path(path)
        self._lock_path = self.path.with_suffix(self.path.suffix + ".lock")

    def append(self, payload: Mapping[str, object]) -> dict[str, object]:
        self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
        self.path.parent.chmod(0o700)
        flags = os.O_CREAT | os.O_RDWR
        nofollow = getattr(os, "O_NOFOLLOW", 0)
        lock_fd = os.open(self._lock_path, flags | nofollow, 0o600)
        try:
            os.fchmod(lock_fd, 0o600)
            fcntl.flock(lock_fd, fcntl.LOCK_EX)
            rows = self.verify()
            body = dict(payload)
            forbidden = {"token", "username", "name", "raw_response", "message"}
            if forbidden.intersection(body):
                raise MembershipGateError("membership journal contains private fields")
            body["schema"] = JOURNAL_SCHEMA
            body["previous_row_sha256"] = (
                rows[-1]["row_sha256"] if rows else "0" * 64
            )
            digest = sha256_json(body)
            body["row_sha256"] = digest
            encoded = canonical_json(body) + b"\n"
            descriptor = os.open(
                self.path,
                os.O_CREAT | os.O_APPEND | os.O_WRONLY | nofollow,
                0o600,
            )
            try:
                os.fchmod(descriptor, 0o600)
                os.write(descriptor, encoded)
                os.fsync(descriptor)
            finally:
                os.close(descriptor)
            return body
        finally:
            fcntl.flock(lock_fd, fcntl.LOCK_UN)
            os.close(lock_fd)

    def append_transition(
        self,
        *,
        update_id: int,
        chat_id: int,
        customer_user_id: int,
        old_status: str,
        new_status: str,
        event_date_utc: str,
        subscription_epoch_id: str,
    ) -> dict[str, object]:
        rows = self.verify()
        transitions = [row for row in rows if row.get("event") == "transition"]
        for row in transitions:
            if row.get("update_id") == update_id:
                expected = {
                    "chat_id": str(chat_id),
                    "customer_user_id": str(customer_user_id),
                    "old_status": old_status,
                    "new_status": new_status,
                    "subscription_epoch_id": subscription_epoch_id,
                }
                if all(row.get(key) == value for key, value in expected.items()):
                    return row
                raise MembershipGateError("duplicate membership update conflicts")
        if transitions:
            previous_update_id = transitions[-1].get("update_id")
            if type(previous_update_id) is not int:
                raise MembershipGateError("membership journal is invalid")
            if previous_update_id > update_id:
                raise MembershipGateError("membership update ID regressed")
        if new_status not in ABSENT_STATUSES:
            raise MembershipGateError("customer entered a staff membership chat")
        return self.append(
            {
                "event": "transition",
                "update_id": update_id,
                "chat_id": str(chat_id),
                "customer_user_id": str(customer_user_id),
                "old_status": old_status,
                "new_status": new_status,
                "event_date_utc": event_date_utc,
                "subscription_epoch_id": subscription_epoch_id,
            }
        )

    def verify(self) -> list[dict[str, object]]:
        if not self.path.exists():
            return []
        metadata = self.path.lstat()
        if (
            self.path.is_symlink()
            or not stat.S_ISREG(metadata.st_mode)
            or stat.S_IMODE(metadata.st_mode) != 0o600
            or metadata.st_nlink != 1
        ):
            raise MembershipGateError("membership journal is not private")
        try:
            rows = [json.loads(line) for line in self.path.read_text().splitlines()]
        except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
            raise MembershipGateError("membership journal is invalid") from exc
        previous = "0" * 64
        for row in rows:
            if not isinstance(row, dict) or row.get("schema") != JOURNAL_SCHEMA:
                raise MembershipGateError("membership journal is invalid")
            digest = row.get("row_sha256")
            body = {key: value for key, value in row.items() if key != "row_sha256"}
            if row.get("previous_row_sha256") != previous or digest != sha256_json(body):
                raise MembershipGateError("membership journal hash chain is invalid")
            previous = str(digest)
        return rows

def write_private_json(path: Path, payload: Mapping[str, object]) -> None:
    path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
    path.parent.chmod(0o700)
    temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
    flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY | getattr(os, "O_NOFOLLOW", 0)
    descriptor = os.open(temporary, flags, 0o600)
    try:
        encoded = canonical_json(payload) + b"\n"
        os.write(descriptor, encoded)
        os.fsync(descriptor)
    finally:
        os.close(descriptor)
    os.replace(temporary, path)
    path.chmod(0o600)

async def create_pre_activation_evidence(
    bot: ReadOnlyMembershipBot,
    inventory: StaffChatInventory,
    *,
    output_path: Path,
    deployment_receipt_path: Path,
    registry_path: Path,
    config_path: Path,
    customer_id: str,
    customer_user_id: int,
    bootstrap_session_id: str,
    bootstrap_generation: int,
    subscription_epoch_id: str,
    subscription_armed_at_utc: str,
    phase: str = "pre_activation",
    now: datetime | None = None,
) -> dict[str, object]:
    """Perform a fresh query and persist its complete immutable bindings."""
    current = now or datetime.now(UTC)
    started = current.isoformat()
    try:
        deployment = json.loads(deployment_receipt_path.read_text(encoding="utf-8"))
    except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise MembershipGateError("deployment receipt is unavailable") from exc
    required_deployment = (
        "candidate_digest", "candidate_core_digest", "candidate_inventory_digest",
        "hermes_wheel_sha256", "profile_wheel_sha256",
    )
    if not isinstance(deployment, dict) or any(
        not isinstance(deployment.get(field), str) for field in required_deployment
    ):
        raise MembershipGateError("deployment receipt is invalid")
    result = await observe_customer_absence(
        bot, inventory, customer_user_id=customer_user_id
    )
    observed = current.isoformat()
    if phase not in {"pre_activation", "post_lifecycle"}:
        raise MembershipGateError("membership evidence phase is invalid")
    payload: dict[str, object] = {
        "schema": EVIDENCE_SCHEMA,
        "phase": phase,
        **{field: deployment[field] for field in required_deployment},
        "deployment_receipt_path": str(deployment_receipt_path.resolve()),
        "deployment_receipt_sha256": sha256_file(deployment_receipt_path),
        "customer_id": customer_id,
        "customer_user_id": str(customer_user_id),
        "bootstrap_session_id": bootstrap_session_id,
        "bootstrap_generation": bootstrap_generation,
        "registry_sha256": sha256_file(registry_path),
        "config_sha256": sha256_file(config_path),
        "staff_chat_inventory": inventory.to_list(),
        "staff_chat_inventory_sha256": inventory.sha256,
        "subscription_epoch_id": subscription_epoch_id,
        "subscription_armed_at_utc": subscription_armed_at_utc,
        "request_started_at_utc": started,
        "observed_at_utc": observed,
        "activation_journal_sha256": "0" * 64,
        **result,
    }
    payload["evidence_sha256"] = sha256_json(payload)
    write_private_json(output_path, payload)
    return payload

def validate_membership_evidence(
    path: Path,
    *,
    expected: Mapping[str, object],
    now: datetime | None = None,
    max_age: timedelta = DEFAULT_MAX_AGE,
) -> dict[str, object]:
    """Validate a complete candidate- and customer-bound pre-activation proof."""
    try:
        metadata = path.lstat()
        payload = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise MembershipGateError("membership evidence is unavailable") from exc
    if (
        path.is_symlink()
        or not stat.S_ISREG(metadata.st_mode)
        or stat.S_IMODE(metadata.st_mode) != 0o600
        or metadata.st_nlink != 1
        or not isinstance(payload, dict)
        or payload.get("schema") != EVIDENCE_SCHEMA
        or payload.get("phase") != "pre_activation"
    ):
        raise MembershipGateError("membership evidence is invalid")
    evidence_digest = payload.get("evidence_sha256")
    body = {key: value for key, value in payload.items() if key != "evidence_sha256"}
    if evidence_digest != sha256_json(body):
        raise MembershipGateError("membership evidence digest is invalid")
    required_bindings = (
        "candidate_digest",
        "candidate_core_digest",
        "candidate_inventory_digest",
        "hermes_wheel_sha256",
        "profile_wheel_sha256",
        "deployment_receipt_sha256",
        "customer_id",
        "customer_user_id",
        "bootstrap_session_id",
        "bootstrap_generation",
        "registry_sha256",
        "config_sha256",
        "staff_chat_inventory_sha256",
        "subscription_epoch_id",
        "activation_journal_sha256",
    )
    for key in required_bindings:
        if key not in payload or key not in expected or payload[key] != expected[key]:
            raise MembershipGateError(f"membership evidence {key} is stale")
    inventory = payload.get("staff_chat_inventory")
    if not isinstance(inventory, list) or sha256_json(inventory) != payload.get(
        "staff_chat_inventory_sha256"
    ):
        raise MembershipGateError("membership evidence inventory is invalid")
    group_ids = {
        row.get("chat_id")
        for row in inventory
        if isinstance(row, dict) and row.get("kind") == StaffChatKind.MEMBERSHIP_CHAT.value
    }
    private_ids = {
        row.get("chat_id")
        for row in inventory
        if isinstance(row, dict) and row.get("kind") == StaffChatKind.PRIVATE_STAFF_DM.value
    }
    memberships = payload.get("membership_results")
    private = payload.get("private_dm_results")
    if (
        not isinstance(memberships, list)
        or {row.get("chat_id") for row in memberships if isinstance(row, dict)} != group_ids
        or any(
            not isinstance(row, dict) or row.get("status") not in ABSENT_STATUSES
            for row in memberships
        )
        or not isinstance(private, list)
        or {row.get("chat_id") for row in private if isinstance(row, dict)} != private_ids
        or any(
            not isinstance(row, dict) or row.get("identity_separated") is not True
            for row in private
        )
    ):
        raise MembershipGateError("membership evidence absence results are incomplete")
    try:
        started = datetime.fromisoformat(str(payload["request_started_at_utc"]))
        observed = datetime.fromisoformat(str(payload["observed_at_utc"]))
        armed = datetime.fromisoformat(str(payload["subscription_armed_at_utc"]))
    except (KeyError, ValueError) as exc:
        raise MembershipGateError("membership evidence timestamps are invalid") from exc
    current = now or datetime.now(UTC)
    if any(value.tzinfo is None for value in (started, observed, armed)):
        raise MembershipGateError("membership evidence timestamps are invalid")
    if not armed <= started <= observed <= current or current - observed > max_age:
        raise MembershipGateError("membership evidence is stale")
    return payload

__all__ = [
    "ABSENT_STATUSES",
    "EVIDENCE_SCHEMA",
    "MembershipGateError",
    "MembershipJournal",
    "ReadOnlyMembershipBot",
    "StaffChat",
    "StaffChatInventory",
    "StaffChatKind",
    "build_staff_chat_inventory",
    "canonical_json",
    "create_pre_activation_evidence",
    "observe_customer_absence",
    "sha256_file",
    "sha256_json",
    "validate_membership_evidence",
    "verify_subscription_readiness",
    "write_private_json",
]

=== hermes:gateway/platforms/telegram_nutrition_onboarding_runtime_callback.py ===
from __future__ import annotations

import asyncio
import base64
import hashlib
import logging
from typing import Any, Protocol
from gateway.platforms.telegram_nutrition_onboarding import (
    OPTIONAL_QUESTION_DEFAULTS,
    decode_callback,
    log_ingress_stage,
)
from gateway.platforms.telegram_nutrition_onboarding_runtime_notice import (
    TelegramNutritionOnboardingRuntimeNoticeMixin,
)
from gateway.platforms.telegram_nutrition_onboarding_runtime_errors import (
    NutritionOnboardingRuntimeError,
)
from gateway.platforms.telegram_customer_bootstrap import BootstrapState

logger = logging.getLogger(__name__)
_CALLBACK_ACK_TIMEOUT_SECONDS = 2.0

class _CallbackAnswerQuery(Protocol):
    async def answer(self, text: str | None = None) -> object: ...

class _CallbackActor(Protocol):
    @property
    def id(self) -> int | None: ...

class _OnboardingCallbackQuery(_CallbackAnswerQuery, Protocol):
    @property
    def id(self) -> str | None: ...

    @property
    def from_user(self) -> _CallbackActor | None: ...

class _OnboardingCallbackMessage(Protocol):
    @property
    def message_id(self) -> int | str | None: ...

class _BestEffortCallbackQuery:
    def __init__(
        self,
        query: _OnboardingCallbackQuery,
        *,
        update_id: int | None,
    ) -> None:
        self._query = query
        self._update_id = (
            str(update_id)
            if type(update_id) is int and update_id >= 0
            else "unknown"
        )

    @property
    def id(self) -> str | None:
        return self._query.id

    @property
    def from_user(self) -> _CallbackActor | None:
        return self._query.from_user

    async def answer(self, text: str | None = None) -> None:
        try:
            acknowledgement = (
                self._query.answer()
                if text is None
                else self._query.answer(text=text)
            )
            await asyncio.wait_for(
                acknowledgement,
                timeout=_CALLBACK_ACK_TIMEOUT_SECONDS,
            )
        except asyncio.CancelledError as exc:
            task = asyncio.current_task()
            if task is not None and task.cancelling():
                raise
            logger.warning(
                "nutrition_onboarding_callback_ack_failed "
                "gate=onboarding_callback update_id=%s error=%s",
                self._update_id,
                type(exc).__name__,
            )
        except Exception as exc:
            logger.warning(
                "nutrition_onboarding_callback_ack_failed "
                "gate=onboarding_callback update_id=%s error=%s",
                self._update_id,
                type(exc).__name__,
            )

def callback_update_id(query: _OnboardingCallbackQuery) -> int:
    callback_id = str(getattr(query, "id", "") or "")
    if not callback_id:
        raise NutritionOnboardingRuntimeError(
            "callback query id is required"
        )
    digest = hashlib.sha256(callback_id.encode()).digest()
    return int.from_bytes(digest[:8], "big") & ((1 << 63) - 1)

class TelegramNutritionOnboardingRuntimeCallbackMixin(
    TelegramNutritionOnboardingRuntimeNoticeMixin
):
    async def handle_callback(
        self: Any,
        query: _OnboardingCallbackQuery,
        data: str,
        message: _OnboardingCallbackMessage,
        *,
        update_id: int | None = None,
    ) -> None:
        raw_query = query
        query = _BestEffortCallbackQuery(query, update_id=update_id)
        log_ingress_stage("ingress", update_id, logger=logger)
        await query.answer()
        try:
            token = decode_callback(data)
            sid_hash = base64.urlsafe_b64decode(
                token.session_hash + "="
            ).hex()
            session = self.bootstrap.store.get_by_sid_hash(sid_hash)
        except (ValueError, TypeError):
            log_ingress_stage(
                "validation",
                update_id,
                reason_code="callback_invalid",
                logger=logger,
            )
            await query.answer(
                text="만료되거나 잘못된 온보딩 버튼입니다."
            )
            return
        if (
            session is None
            or session.state is not BootstrapState.AWAITING_ACTIVATION
        ):
            log_ingress_stage(
                "validation",
                update_id,
                reason_code="onboarding_state_unavailable",
                logger=logger,
            )
            await query.answer(text="현재 사용할 수 없는 온보딩 버튼입니다.")
            return
        service = self._service(session.customer_key)
        try:
            publication = service.store.load_session(session.session_id)
        except ValueError:
            log_ingress_stage(
                "validation",
                update_id,
                reason_code="onboarding_session_unavailable",
                logger=logger,
            )
            await query.answer(text="현재 온보딩 질문을 확인하지 못했습니다.")
            return
        actor_id = getattr(getattr(query, "from_user", None), "id", None)
        route_roles = {
            "prev": "customer",
            "later": "customer",
            "skip": "customer",
            "resume": "customer",
            "revise": "customer",
            "attest": "customer",
            "owner_ok": "owner",
            "op_rev": "owner",
            "op_rej": "owner",
            "op_hold": "owner",
        }
        role = route_roles.get(token.action)
        if (
            token.generation != publication.generation
            or str(getattr(message, "message_id", ""))
            != str(publication.message_id)
            or role is None
            or not self._publication_callback_is_current(
                session=session,
                publication=publication,
                role=role,
                message=message,
            )
        ):
            log_ingress_stage(
                "validation",
                update_id,
                reason_code="stale_publication",
                logger=logger,
            )
            await query.answer(
                text="가장 최근 온보딩 카드를 사용해 주세요."
            )
            return
        try:
            member_chat_id, _ = self._route(session, role)
        except (OSError, TypeError, ValueError):
            log_ingress_stage(
                "authority",
                update_id,
                reason_code="route_unavailable",
                logger=logger,
            )
            await query.answer(
                text="현재 참여자 권한을 확인하지 못했습니다."
            )
            return
        logger.info(
            "nutrition_onboarding_callback_stage stage=member_check_start"
        )
        if not await self._member_present(member_chat_id, actor_id):
            log_ingress_stage(
                "authority",
                update_id,
                reason_code="member_unavailable",
                logger=logger,
            )
            await query.answer(
                text="현재 참여자 권한을 확인하지 못했습니다."
            )
            return
        logger.info(
            "nutrition_onboarding_callback_stage stage=member_check_pass"
        )
        try:
            evidence_update_id = (
                update_id
                if type(update_id) is int and update_id >= 0
                else callback_update_id(query)
            )
        except ValueError:
            log_ingress_stage(
                "validation",
                update_id,
                reason_code="callback_identifier_unavailable",
                logger=logger,
            )
            await query.answer(
                text="만료되었거나 사용할 수 없는 버튼입니다."
            )
            return
        evidence = self._evidence(
            actor_id=actor_id,
            message=message,
            update_id=evidence_update_id,
        )
        try:
            authority = self._current_authority(session)
        except (OSError, ValueError):
            log_ingress_stage(
                "authority",
                update_id,
                reason_code="authority_unavailable",
                logger=logger,
            )
            await query.answer(
                text="온보딩 권한 또는 동의가 변경됐습니다."
            )
            return
        log_ingress_stage("authority", update_id, logger=logger)
        logger.info(
            "nutrition_onboarding_callback_stage stage=authority_pass"
        )
        status = service.status()
        try:
            if role is not None:
                try:
                    self.domain.validate_route(
                        authority,
                        evidence,
                        role=role,
                    )
                except ValueError:
                    log_ingress_stage(
                        "route",
                        update_id,
                        reason_code="route_rejected",
                        logger=logger,
                    )
                    await query.answer(
                        text="온보딩 권한 또는 상태가 변경됐습니다."
                    )
                    return
                logger.info(
                    "nutrition_onboarding_callback_stage stage=route_pass"
                )
            log_ingress_stage("route", update_id, logger=logger)
            replayed_status = self.replayed_status_if_consumed(
                service,
                evidence_update_id,
            )
            if replayed_status is not None:
                log_ingress_stage(
                    "persistence",
                    update_id,
                    reason_code="duplicate_business_replay",
                    logger=logger,
                )
                await self._retire_keyboard(raw_query)
                log_ingress_stage("publication", update_id, logger=logger)
                try:
                    await self._publish(session, service, replayed_status)
                except (OSError, RuntimeError, TypeError, ValueError):
                    log_ingress_stage(
                        "publication",
                        update_id,
                        reason_code="publication_failed",
                        logger=logger,
                    )
                    raise
                log_ingress_stage("receipt", update_id, logger=logger)
                return
            log_ingress_stage("validation", update_id, logger=logger)
            if (
                token.action == "prev"
                and status.state.value == "collecting"
                and status.answer_count > 0
            ):
                status = self._rewind_collection(
                    service=service,
                    authority=authority,
                    evidence=evidence,
                    target_cursor=status.answer_count - 1,
                )
            elif (
                token.action == "later"
                and status.state.value == "collecting"
            ):
                log_ingress_stage("persistence", update_id, logger=logger)
                await self._retire_keyboard(raw_query)
                await query.answer(text="현재 위치를 저장했습니다.")
                log_ingress_stage("publication", update_id, logger=logger)
                await self._continue_later(session, service, status)
                log_ingress_stage("receipt", update_id, logger=logger)
                return
            elif (
                token.action == "resume"
                and status.state.value == "collecting"
                and publication.payload.get("mode") == "paused"
            ):
                log_ingress_stage("persistence", update_id, logger=logger)
                await self._retire_keyboard(raw_query)
                await query.answer(text="온보딩을 이어서 진행합니다.")
                log_ingress_stage("publication", update_id, logger=logger)
                await self._resume(session, service)
                log_ingress_stage("receipt", update_id, logger=logger)
                return
            elif (
                token.action == "skip"
                and status.state.value == "collecting"
                and status.next_field in OPTIONAL_QUESTION_DEFAULTS
            ):
                status = service.submit_answer(
                    field=status.next_field,
                    value=OPTIONAL_QUESTION_DEFAULTS[status.next_field],
                    authority=authority,
                    evidence=evidence,
                )
            elif (
                token.action == "revise"
                and status.state.value == "customer_attestation"
                and status.answer_count > 0
            ):
                status = self._rewind_collection(
                    service=service,
                    authority=authority,
                    evidence=evidence,
                    target_cursor=status.answer_count - 1,
                )
            elif (
                token.action == "attest"
                and status.state.value == "customer_attestation"
            ):
                status = service.attest_baseline(
                    authority=authority,
                    evidence=evidence,
                )
            elif status.state.value == "owner_review" and token.action in {
                "owner_ok",
                "op_rev",
                "op_rej",
                "op_hold",
            }:
                decision = {
                    "owner_ok": "approved",
                    "op_rev": "revise",
                    "op_rej": "rejected",
                    "op_hold": "safety_hold",
                }[token.action]
                kwargs = {
                    "decision": decision,
                    "authority": authority,
                    "evidence": evidence,
                }
                if decision == "revise":
                    payload = getattr(publication, "payload", None)
                    binding = (
                        payload.get("revision_binding")
                        if isinstance(payload, dict)
                        else None
                    )
                    if not isinstance(binding, dict) or set(binding) != {
                        "issue_id",
                        "field",
                        "answers_digest",
                        "reconciliation_digest",
                    }:
                        raise ValueError("revision binding is unavailable")
                    field = binding.get("field")
                    issue_id = binding.get("issue_id")
                    answers_digest = binding.get("answers_digest")
                    reconciliation_digest = binding.get("reconciliation_digest")
                    if (
                        field not in self.domain.QUESTION_FIELDS
                        or not all(
                            isinstance(value, str)
                            and len(value) == 64
                            and not set(value).difference("0123456789abcdef")
                            for value in (
                                issue_id,
                                answers_digest,
                                reconciliation_digest,
                            )
                        )
                    ):
                        raise ValueError("revision binding is invalid")
                    kwargs.update(
                        revision_issue_id=issue_id,
                        revision_field=field,
                        expected_answers_digest=answers_digest,
                        expected_reconciliation_digest=reconciliation_digest,
                    )
                if decision == "approved":
                    self._preserve_owner_callback_receipt(
                        session=session,
                        service=service,
                        publication=publication,
                        authority=authority,
                        evidence=evidence,
                        callback_data=data,
                    )
                status = service.review_as_owner(**kwargs)
                if decision == "approved":
                    log_ingress_stage(
                        "persistence",
                        update_id,
                        reason_code="business_state_committed",
                        logger=logger,
                    )
                    status = self._finalize_owner_approval(
                        session=session,
                        service=service,
                        authority=authority,
                    )
            else:
                log_ingress_stage(
                    "validation",
                    update_id,
                    reason_code="action_state_invalid",
                    logger=logger,
                )
                await query.answer(
                    text="현재 단계에서 사용할 수 없는 버튼입니다."
                )
                return
        except ValueError:
            log_ingress_stage(
                "persistence",
                update_id,
                reason_code="transition_rejected",
                logger=logger,
            )
            await query.answer(
                text="온보딩 권한 또는 상태가 변경됐습니다."
            )
            return
        log_ingress_stage("persistence", update_id, logger=logger)
        logger.info(
            "nutrition_onboarding_callback_stage stage=transition_pass"
        )
        logger.info("nutrition_onboarding_callback_applied")
        await self._retire_keyboard(raw_query)
        await query.answer(text="확인했습니다.")
        log_ingress_stage("publication", update_id, logger=logger)
        try:
            if token.action == "attest":
                await self._send_customer_confirmation(session, status)
            lifecycle = {
                "op_rev": "revision",
                "op_rej": "rejection",
            }.get(token.action)
            if lifecycle is not None:
                await self._send_customer_lifecycle_notice(session, lifecycle)
            await self._publish(session, service, status)
        except (OSError, RuntimeError, TypeError, ValueError):
            log_ingress_stage(
                "publication",
                update_id,
                reason_code="publication_failed",
                logger=logger,
            )
            raise
        log_ingress_stage("receipt", update_id, logger=logger)

[Showing lines 23492-24854 of 24854 (50.0KB limit). Full output: /tmp/pi-bash-611f5d49dc3994bf.log]


=== gateway/platforms/telegram_nutrition_onboarding_publication_outbox.py:1-340 ===
1: """Gateway-owned, authenticated receipt ledger for nutrition-onboarding cards."""
2:
3: from __future__ import annotations
4:
5: import fcntl
6: import hashlib
7: import hmac
8: import json
9: import os
10: import re
11: import secrets
12: import stat
13: import tempfile
14: from collections.abc import Iterator, Mapping
15: from contextlib import contextmanager
16: from dataclasses import dataclass
17: from pathlib import Path
18: from typing import Final
19:
20: _SCHEMA: Final = "telegram-nutrition-onboarding-publication-outbox-v2"
21: _LEGACY_SCHEMA: Final = "telegram-nutrition-onboarding-publication-outbox-v1"
22: _CALLBACK_SCHEMA: Final = "telegram-nutrition-onboarding-owner-callback-v1"
23: _DIGEST: Final = re.compile(r"^[a-f0-9]{64}$")
24: _STATES: Final = frozenset({"DISPATCHING", "RECEIPTED", "COMMITTED"})
25: _EMERGENCY_STATES: Final = frozenset({"RECEIPTED", "COMMITTED"})
26: _RELATIVE_PATH: Final = Path("data/onboarding/telegram-publication-outbox-v1")
27: _PRIVATE_MODE: Final = 0o600
28: _DIRECTORY_MODE: Final = 0o700
29: _NOATIME: Final = getattr(os, "O_NOATIME", 0)
30:
31:
32: @dataclass(frozen=True, slots=True)
33: class GatewayPublicationReceipt:
34:     """One exact publication authority and its optional provider receipt."""
35:
36:     session_id: str
37:     generation: int
38:     payload: dict[str, object]
39:     route: tuple[str, str]
40:     role: str
41:     render_identity: str
42:     payload_digest: str
43:     dispatch_identity: str
44:     state: str
45:     message_id: int | None = None
46:     receipt_integrity: str | None = None
47:
48:
49: @dataclass(frozen=True, slots=True)
50: class GatewayOwnerCallbackReceipt:
51:     """One immutable authenticated owner callback event."""
52:
53:     session_id: str
54:     customer_key: str
55:     action: str
56:     actor_user_id: int
57:     authority: tuple[int, int, int]
58:     route: tuple[str, str]
59:     message_id: int
60:     update_id: int
61:     consumed_updates_before: tuple[int, ...]
62:     callback_data: str
63:     publication_generation: int
64:     publication_dispatch_identity: str
65:     publication_receipt_integrity: str
66:     event_integrity: str
67:
68:
69: class GatewayOnboardingPublicationOutbox:
70:     """Persist authenticated Telegram receipts before profile publication commit."""
71:
72:     def __init__(self, profile_root: Path | str, *, initialize: bool = True) -> None:
73:         self.state_dir = Path(profile_root) / _RELATIVE_PATH
74:         self.ledger_path = self.state_dir / "ledger.json"
75:         self.legacy_ledger_path = self.state_dir / "ledger-v1-untrusted.json"
76:         self.lock_path = self.state_dir / ".lock"
77:         self.emergency_path = self.state_dir / "emergency.json"
78:         self.emergency_lock_path = self.state_dir / ".emergency.lock"
79:         self.callback_path = self.state_dir / "owner-callbacks.json"
80:         self.callback_lock_path = self.state_dir / ".owner-callbacks.lock"
81:         self.key_path = self.state_dir / ".receipt-key"
82:         self._callback_authority_absent = False
83:         if initialize:
84:             self._prepare_paths()
85:         else:
86:             self._inspect_paths()
87:
88:     def claim(
89:         self,
90:         *,
91:         session_id: str,
92:         generation: int,
93:         payload: Mapping[str, object],
94:         route: tuple[str, str],
95:         role: str,
96:         render_identity: str,
97:     ) -> tuple[GatewayPublicationReceipt, bool]:
98:         """Claim one exact publication before the provider call."""
99:         candidate = self._candidate(
100:             session_id=session_id,
101:             generation=generation,
102:             payload=payload,
103:             route=route,
104:             role=role,
105:             render_identity=render_identity,
106:         )
107:         with self._locked(self.lock_path):
108:             records = list(self._read_primary_unlocked())
109:             match = self._matching_record(records, candidate.session_id, candidate.generation)
110:             if match is None:
111:                 records.append(candidate)
112:                 self._write_primary_unlocked(records)
113:                 return candidate, True
114:             if not self._same_authority(match, candidate):
115:                 raise ValueError("onboarding publication authority does not match")
116:             return match, False
117:
118:     def get(
119:         self,
120:         *,
121:         session_id: str,
122:         generation: int,
123:         payload: Mapping[str, object],
124:         route: tuple[str, str],
125:         role: str,
126:         render_identity: str,
127:     ) -> GatewayPublicationReceipt | None:
128:         """Resolve only an exact current publication authority."""
129:         candidate = self._candidate(
130:             session_id=session_id,
131:             generation=generation,
132:             payload=payload,
133:             route=route,
134:             role=role,
135:             render_identity=render_identity,
136:         )
137:         with self._locked(self.lock_path):
138:             record = self._matching_record(
139:                 self._read_primary_unlocked(), candidate.session_id, candidate.generation
140:             )
141:         if record is None:
142:             return None
143:         if not self._same_authority(record, candidate):
144:             raise ValueError("onboarding publication authority does not match")
145:         return record
146:
147:     def record_receipt(
148:         self,
149:         *,
150:         session_id: str,
151:         generation: int,
152:         chat_id: str,
153:         topic_id: str,
154:         message_id: int,
155:     ) -> GatewayPublicationReceipt:
156:         """Attach an authenticated Telegram receipt to one dispatch authority."""
157:         return self._record_receipt(
158:             session_id=session_id,
159:             generation=generation,
160:             chat_id=chat_id,
161:             topic_id=topic_id,
162:             message_id=message_id,
163:             emergency=False,
164:         )
165:
166:     def record_emergency_receipt(
167:         self,
168:         *,
169:         session_id: str,
170:         generation: int,
171:         chat_id: str,
172:         topic_id: str,
173:         message_id: int,
174:     ) -> GatewayPublicationReceipt:
175:         """Persist a separate receipt when the primary ledger write fails."""
176:         return self._record_receipt(
177:             session_id=session_id,
178:             generation=generation,
179:             chat_id=chat_id,
180:             topic_id=topic_id,
181:             message_id=message_id,
182:             emergency=True,
183:         )
184:
185:     def reconcile_emergency_receipts(self) -> int:
186:         """Merge exact emergency evidence into the primary receipt ledger."""
187:         with self._locked(self.lock_path):
188:             records = list(self._read_primary_unlocked())
189:             with self._locked(self.emergency_lock_path):
190:                 emergency_records = self._read_emergency_unlocked()
191:             changed = 0
192:             for emergency in emergency_records:
193:                 if emergency.state == "COMMITTED":
194:                     continue
195:                 index, primary = self._find(records, emergency.session_id, emergency.generation)
196:                 if not self._same_authority(primary, emergency):
197:                     raise ValueError("emergency publication authority does not match")
198:                 if not self._valid_receipt(emergency):
199:                     raise ValueError("emergency publication receipt integrity is invalid")
200:                 if primary.state == "DISPATCHING":
201:                     records[index] = emergency
202:                     changed += 1
203:                     continue
204:                 if (
205:                     primary.state != "RECEIPTED"
206:                     or primary.message_id != emergency.message_id
207:                     or not hmac.compare_digest(
208:                         primary.receipt_integrity or "", emergency.receipt_integrity or ""
209:                     )
210:                 ):
211:                     raise ValueError("emergency publication receipt conflicts with primary evidence")
212:             if changed:
213:                 self._write_primary_unlocked(records)
214:             return changed
215:
216:     def mark_committed(
217:         self,
218:         *,
219:         session_id: str,
220:         generation: int,
221:         payload: Mapping[str, object],
222:         route: tuple[str, str],
223:         role: str,
224:         render_identity: str,
225:         message_id: int,
226:     ) -> GatewayPublicationReceipt:
227:         """Retire an exact receipt only after the profile commit succeeds."""
228:         candidate = self._candidate(
229:             session_id=session_id,
230:             generation=generation,
231:             payload=payload,
232:             route=route,
233:             role=role,
234:             render_identity=render_identity,
235:         )
236:         with self._locked(self.lock_path):
237:             records = list(self._read_primary_unlocked())
238:             index, current = self._find(records, session_id, generation)
239:             if not self._same_authority(current, candidate):
240:                 raise ValueError("onboarding publication authority does not match")
241:             if current.message_id != message_id or not self._valid_receipt(current):
242:                 raise ValueError("onboarding publication receipt integrity is invalid")
243:             if current.state == "COMMITTED":
244:                 return current
245:             if current.state != "RECEIPTED":
246:                 raise ValueError("onboarding publication has no committed receipt")
247:             updated = self._with_state(current, "COMMITTED")
248:             records[index] = updated
249:             self._write_primary_unlocked(records)
250:         self._mark_emergency_committed(updated)
251:         return updated
252:
253:     def receipted(self) -> tuple[GatewayPublicationReceipt, ...]:
254:         with self._locked(self.lock_path):
255:             return tuple(
256:                 record
257:                 for record in self._read_primary_unlocked()
258:                 if record.state == "RECEIPTED"
259:             )
260:
261:     def records(self) -> tuple[GatewayPublicationReceipt, ...]:
262:         with self._locked(self.lock_path):
263:             return self._read_primary_unlocked()
264:
265:     def emergency_records(self) -> tuple[GatewayPublicationReceipt, ...]:
266:         with self._locked(self.emergency_lock_path):
267:             return self._read_emergency_unlocked()
268:
269:     def record_owner_callback(
270:         self,
271:         *,
272:         session_id: str,
273:         customer_key: str,
274:         action: str,
275:         actor_user_id: int,
276:         authority: tuple[int, int, int],
277:         route: tuple[str, str],
278:         message_id: int,
279:         update_id: int,
280:         consumed_updates_before: tuple[int, ...],
281:         callback_data: str,
282:         publication_generation: int,
283:     ) -> GatewayOwnerCallbackReceipt:
284:         """Persist the full authenticated event before owner state finalization."""
285:         with self._locked(self.lock_path):
286:             publication = self._matching_record(
287:                 self._read_primary_unlocked(), session_id, publication_generation
288:             )
289:         if (
290:             publication is None
291:             or publication.state != "COMMITTED"
292:             or publication.role != "owner"
293:             or publication.route != self._route(*route)
294:             or publication.message_id != self._message_id(message_id)
295:             or not self._valid_receipt(publication)
296:         ):
297:             raise ValueError("owner callback publication authority does not match")
298:         receipt = self._owner_callback_candidate(
299:             session_id=session_id,
300:             customer_key=customer_key,
301:             action=action,
302:             actor_user_id=actor_user_id,
303:             authority=authority,
304:             route=route,
305:             message_id=message_id,
306:             update_id=update_id,
307:             consumed_updates_before=consumed_updates_before,
308:             callback_data=callback_data,
309:             publication=publication,
310:         )
311:         with self._locked(self.callback_lock_path):
312:             records = list(self._read_owner_callbacks_unlocked())
313:             matches = [item for item in records if item.session_id == receipt.session_id]
314:             if matches:
315:                 if len(matches) != 1 or matches[0] != receipt:
316:                     raise ValueError("owner callback receipt conflicts with immutable evidence")
317:                 return matches[0]
318:             records.append(receipt)
319:             self._write_owner_callbacks_unlocked(records)
320:         return receipt
321:
322:     def owner_callback(self, session_id: str) -> GatewayOwnerCallbackReceipt | None:
323:         """Return the sole integrity-checked owner callback for one session."""
324:         session = self._session_id(session_id)
325:         if self._callback_authority_absent:
326:             if self._entry_exists(self.callback_path) or self._entry_exists(self.callback_lock_path):
327:                 raise ValueError("owner callback receipt authority changed during inspection")
328:             return None
329:         with self._locked(self.callback_lock_path):
330:             matches = [
331:                 item
332:                 for item in self._read_owner_callbacks_unlocked()
333:                 if item.session_id == session
334:             ]
335:         if not matches:
336:             return None
337:         if len(matches) != 1:
338:             raise ValueError("owner callback receipt is ambiguous")
339:         return matches[0]
340:
=== gateway/platforms/telegram_nutrition_onboarding_publication_outbox.py:630-970 ===
630:         record: GatewayPublicationReceipt,
631:         message_id: int,
632:     ) -> str:
633:         payload = self._canonical({
634:             "domain": "telegram-nutrition-onboarding-receipt-v1",
635:             "dispatch_identity": record.dispatch_identity,
636:             "payload_digest": record.payload_digest,
637:             "route": list(record.route),
638:             "message_id": message_id,
639:         })
640:         return hmac.new(self._read_key(), payload, hashlib.sha256).hexdigest()
641:
642:     def _prepare_paths(self) -> None:
643:         self._validate_path_ancestry(require_state_dir=False)
644:         self.state_dir.mkdir(parents=True, exist_ok=True, mode=_DIRECTORY_MODE)
645:         directory = os.stat(self.state_dir, follow_symlinks=False)
646:         if not stat.S_ISDIR(directory.st_mode) or stat.S_ISLNK(directory.st_mode):
647:             raise ValueError("onboarding publication outbox directory is invalid")
648:         os.chmod(self.state_dir, _DIRECTORY_MODE)
649:         self._ensure_private_file(self.lock_path)
650:         self._ensure_private_file(self.emergency_lock_path)
651:         self._ensure_private_file(self.callback_lock_path)
652:         self._ensure_key()
653:         self._ensure_ledger(self.ledger_path, emergency=False)
654:         self._ensure_ledger(self.emergency_path, emergency=True)
655:         self._ensure_callback_ledger()
656:
657:     def _inspect_paths(self) -> None:
658:         """Validate existing authorities without creating or repairing any path."""
659:         self._validate_path_ancestry(require_state_dir=True)
660:         directory = os.stat(self.state_dir, follow_symlinks=False)
661:         if (
662:             not stat.S_ISDIR(directory.st_mode)
663:             or stat.S_ISLNK(directory.st_mode)
664:             or stat.S_IMODE(directory.st_mode) != _DIRECTORY_MODE
665:         ):
666:             raise ValueError("onboarding publication outbox directory is invalid")
667:         for path in (self.lock_path, self.emergency_lock_path):
668:             self._inspect_private_file(path, "onboarding publication outbox authority")
669:         self._inspect_private_file(self.key_path, "onboarding publication receipt key")
670:         self._read_key()
671:         self._inspect_private_file(self.ledger_path, "onboarding publication outbox ledger")
672:         self._inspect_private_file(self.emergency_path, "onboarding publication outbox ledger")
673:         if self._ledger_schema(self.ledger_path) == _LEGACY_SCHEMA:
674:             raise ValueError("onboarding publication outbox ledger requires explicit migration")
675:         self._read_primary_unlocked()
676:         self._read_emergency_unlocked()
677:         callback_exists = self._entry_exists(self.callback_path)
678:         callback_lock_exists = self._entry_exists(self.callback_lock_path)
679:         if callback_exists != callback_lock_exists:
680:             raise ValueError("owner callback receipt authority is incomplete")
681:         self._callback_authority_absent = not callback_exists
682:         if callback_exists:
683:             self._inspect_private_file(
684:                 self.callback_lock_path, "onboarding publication outbox authority"
685:             )
686:             self._inspect_private_file(self.callback_path, "owner callback receipt ledger")
687:             self._read_owner_callbacks_unlocked()
688:
689:     def _validate_path_ancestry(self, *, require_state_dir: bool) -> None:
690:         for path in (*reversed(self.state_dir.parents), self.state_dir):
691:             try:
692:                 info = os.lstat(path)
693:             except FileNotFoundError:
694:                 if require_state_dir and path == self.state_dir:
695:                     raise ValueError("onboarding publication outbox directory is unavailable") from None
696:                 continue
697:             if stat.S_ISLNK(info.st_mode):
698:                 raise ValueError("onboarding publication outbox path must not contain symlinks")
699:
700:     @staticmethod
701:     def _entry_exists(path: Path) -> bool:
702:         try:
703:             os.lstat(path)
704:         except FileNotFoundError:
705:             return False
706:         return True
707:
708:     def _inspect_private_file(self, path: Path, label: str) -> None:
709:         try:
710:             descriptor = os.open(
711:                 path,
712:                 os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0) | _NOATIME,
713:             )
714:         except OSError as exc:
715:             raise ValueError(f"{label} is unavailable") from exc
716:         try:
717:             self._validate_private_descriptor(descriptor, label)
718:         finally:
719:             os.close(descriptor)
720:
721:     def _ensure_callback_ledger(self) -> None:
722:         try:
723:             descriptor = os.open(
724:                 self.callback_path,
725:                 os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0) | _NOATIME,
726:             )
727:         except FileNotFoundError:
728:             self._write_owner_callbacks_unlocked(())
729:             return
730:         except OSError as exc:
731:             raise ValueError("owner callback receipt ledger is unavailable") from exc
732:         try:
733:             self._validate_private_descriptor(descriptor, "owner callback receipt ledger")
734:         finally:
735:             os.close(descriptor)
736:         self._read_owner_callbacks_unlocked()
737:
738:     def _ensure_private_file(self, path: Path) -> None:
739:         try:
740:             descriptor = os.open(
741:                 path,
742:                 os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0),
743:                 _PRIVATE_MODE,
744:             )
745:         except FileExistsError:
746:             descriptor = os.open(
747:                 path,
748:                 os.O_RDWR | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0),
749:             )
750:         except OSError as exc:
751:             raise ValueError("onboarding publication outbox authority is unavailable") from exc
752:         try:
753:             self._validate_private_descriptor(descriptor, "onboarding publication outbox authority")
754:         finally:
755:             os.close(descriptor)
756:
757:     def _ensure_key(self) -> None:
758:         try:
759:             descriptor = os.open(
760:                 self.key_path,
761:                 os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0),
762:                 _PRIVATE_MODE,
763:             )
764:         except FileExistsError:
765:             self._read_key()
766:             return
767:         except OSError as exc:
768:             raise ValueError("onboarding publication receipt key is unavailable") from exc
769:         try:
770:             os.write(descriptor, secrets.token_bytes(32))
771:             os.fsync(descriptor)
772:         finally:
773:             os.close(descriptor)
774:
775:     def _read_key(self) -> bytes:
776:         try:
777:             descriptor = os.open(
778:                 self.key_path,
779:                 os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0) | _NOATIME,
780:             )
781:         except OSError as exc:
782:             raise ValueError("onboarding publication receipt key is unavailable") from exc
783:         try:
784:             self._validate_private_descriptor(descriptor, "onboarding publication receipt key")
785:             key = os.read(descriptor, 33)
786:         finally:
787:             os.close(descriptor)
788:         if len(key) != 32:
789:             raise ValueError("onboarding publication receipt key is invalid")
790:         return key
791:
792:     def _ensure_ledger(self, path: Path, *, emergency: bool) -> None:
793:         try:
794:             descriptor = os.open(
795:                 path,
796:                 os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0) | _NOATIME,
797:             )
798:         except FileNotFoundError:
799:             if emergency:
800:                 self._write_emergency_unlocked(())
801:             else:
802:                 self._write_primary_unlocked(())
803:             return
804:         except OSError as exc:
805:             raise ValueError("onboarding publication outbox ledger is unavailable") from exc
806:         try:
807:             self._validate_private_descriptor(descriptor, "onboarding publication outbox ledger")
808:         finally:
809:             os.close(descriptor)
810:         if emergency:
811:             self._read_emergency_unlocked()
812:         elif self._ledger_schema(path) == _LEGACY_SCHEMA:
813:             self._quarantine_legacy_primary_ledger()
814:         else:
815:             self._read_primary_unlocked()
816:
817:     def _ledger_schema(self, path: Path) -> str | None:
818:         try:
819:             descriptor = os.open(
820:                 path,
821:                 os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0) | _NOATIME,
822:             )
823:         except OSError as exc:
824:             raise ValueError("onboarding publication outbox ledger is unavailable") from exc
825:         try:
826:             self._validate_private_descriptor(
827:                 descriptor,
828:                 "onboarding publication outbox ledger",
829:             )
830:             with os.fdopen(
831:                 descriptor,
832:                 "r",
833:                 encoding="utf-8",
834:                 closefd=False,
835:             ) as handle:
836:                 value = json.load(handle)
837:         finally:
838:             os.close(descriptor)
839:         if not isinstance(value, Mapping):
840:             return None
841:         schema = value.get("schema")
842:         return schema if isinstance(schema, str) else None
843:
844:     def _quarantine_legacy_primary_ledger(self) -> None:
845:         """Preserve unauthenticated v1 evidence without letting it authorize sends."""
846:         with self._locked(self.lock_path):
847:             schema = self._ledger_schema(self.ledger_path)
848:             if schema == _SCHEMA:
849:                 return
850:             if schema != _LEGACY_SCHEMA:
851:                 raise ValueError("onboarding publication outbox ledger integrity is invalid")
852:             try:
853:                 info = os.lstat(self.legacy_ledger_path)
854:             except FileNotFoundError:
855:                 pass
856:             else:
857:                 if stat.S_ISLNK(info.st_mode):
858:                     raise ValueError("onboarding publication outbox legacy path is invalid")
859:                 raise ValueError("onboarding publication outbox legacy ledger already exists")
860:             os.replace(self.ledger_path, self.legacy_ledger_path)
861:             directory = os.open(
862:                 self.state_dir,
863:                 os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC,
864:             )
865:             try:
866:                 os.fsync(directory)
867:             finally:
868:                 os.close(directory)
869:             self._write_primary_unlocked(())
870:
871:     @contextmanager
872:     def _locked(self, path: Path) -> Iterator[None]:
873:         try:
874:             descriptor = os.open(
875:                 path,
876:                 os.O_RDWR | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0),
877:             )
878:         except OSError as exc:
879:             raise ValueError("onboarding publication outbox lock is unavailable") from exc
880:         try:
881:             self._validate_private_descriptor(descriptor, "onboarding publication outbox lock")
882:             fcntl.flock(descriptor, fcntl.LOCK_EX)
883:             yield
884:         finally:
885:             os.close(descriptor)
886:
887:     @staticmethod
888:     def _validate_private_descriptor(descriptor: int, label: str) -> None:
889:         info = os.fstat(descriptor)
890:         if (
891:             not stat.S_ISREG(info.st_mode)
892:             or info.st_nlink != 1
893:             or stat.S_IMODE(info.st_mode) != _PRIVATE_MODE
894:         ):
895:             raise ValueError(f"{label} is invalid")
896:
897:     def _read_primary_unlocked(self) -> tuple[GatewayPublicationReceipt, ...]:
898:         return self._read_records_unlocked(self.ledger_path, emergency=False)
899:
900:     def _read_emergency_unlocked(self) -> tuple[GatewayPublicationReceipt, ...]:
901:         return self._read_records_unlocked(self.emergency_path, emergency=True)
902:
903:     def _read_records_unlocked(
904:         self,
905:         path: Path,
906:         *,
907:         emergency: bool,
908:     ) -> tuple[GatewayPublicationReceipt, ...]:
909:         try:
910:             descriptor = os.open(
911:                 path,
912:                 os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0) | _NOATIME,
913:             )
914:         except OSError as exc:
915:             raise ValueError("onboarding publication outbox ledger is unavailable") from exc
916:         try:
917:             self._validate_private_descriptor(descriptor, "onboarding publication outbox ledger")
918:             with os.fdopen(descriptor, "r", encoding="utf-8", closefd=False) as handle:
919:                 value = json.load(handle)
920:         finally:
921:             os.close(descriptor)
922:         if not isinstance(value, Mapping) or value.get("schema") != _SCHEMA:
923:             raise ValueError("onboarding publication outbox ledger integrity is invalid")
924:         records = value.get("records")
925:         if not isinstance(records, list):
926:             raise ValueError("onboarding publication outbox ledger integrity is invalid")
927:         parsed = tuple(self._record_from_dict(item, emergency=emergency) for item in records)
928:         keys = {(record.session_id, record.generation) for record in parsed}
929:         if len(keys) != len(parsed):
930:             raise ValueError("onboarding publication outbox ledger is ambiguous")
931:         return parsed
932:
933:     def _write_primary_unlocked(
934:         self,
935:         records: tuple[GatewayPublicationReceipt, ...] | list[GatewayPublicationReceipt],
936:     ) -> None:
937:         self._write_records_unlocked(self.ledger_path, records, emergency=False)
938:
939:     def _write_emergency_unlocked(
940:         self,
941:         records: tuple[GatewayPublicationReceipt, ...] | list[GatewayPublicationReceipt],
942:     ) -> None:
943:         self._write_records_unlocked(self.emergency_path, records, emergency=True)
944:
945:     def _write_records_unlocked(
946:         self,
947:         path: Path,
948:         records: tuple[GatewayPublicationReceipt, ...] | list[GatewayPublicationReceipt],
949:         *,
950:         emergency: bool,
951:     ) -> None:
952:         document = {
953:             "schema": _SCHEMA,
954:             "records": [self._record_to_dict(record) for record in records],
955:         }
956:         descriptor, temporary = tempfile.mkstemp(prefix=".ledger-", suffix=".tmp", dir=self.state_dir)
957:         try:
958:             os.fchmod(descriptor, _PRIVATE_MODE)
959:             with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
960:                 json.dump(document, handle, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
961:                 handle.write("\n")
962:                 handle.flush()
963:                 os.fsync(handle.fileno())
964:             os.replace(temporary, path)
965:             directory = os.open(self.state_dir, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
966:             try:
967:                 os.fsync(directory)
968:             finally:
969:                 os.close(directory)
970:         except BaseException:
=== gateway/platforms/telegram_nutrition_onboarding_publication_outbox.py:970-1360 ===
970:         except BaseException:
971:             try:
972:                 os.unlink(temporary)
973:             except FileNotFoundError:
974:                 pass
975:             raise
976:
977:     def _owner_callback_candidate(
978:         self,
979:         *,
980:         session_id: str,
981:         customer_key: str,
982:         action: str,
983:         actor_user_id: int,
984:         authority: tuple[int, int, int],
985:         route: tuple[str, str],
986:         message_id: int,
987:         update_id: int,
988:         consumed_updates_before: tuple[int, ...],
989:         callback_data: str,
990:         publication: GatewayPublicationReceipt,
991:     ) -> GatewayOwnerCallbackReceipt:
992:         session = self._session_id(session_id)
993:         if not isinstance(customer_key, str) or not customer_key or len(customer_key) > 256:
994:             raise ValueError("owner callback customer key is invalid")
995:         if not isinstance(action, str) or not action or len(action) > 64:
996:             raise ValueError("owner callback action is invalid")
997:         if (
998:             type(actor_user_id) is not int
999:             or actor_user_id < 0
1000:             or len(authority) != 3
1001:             or type(authority[0]) is not int
1002:             or authority[0] < 0
1003:             or type(authority[1]) is not int
1004:             or type(authority[2]) is not int
1005:             or authority[2] < 0
1006:             or type(update_id) is not int
1007:             or update_id < 0
1008:         ):
1009:             raise ValueError("owner callback authority is invalid")
1010:         current_route = self._route(*route)
1011:         current_message_id = self._message_id(message_id)
1012:         if (
1013:             not isinstance(callback_data, str)
1014:             or not callback_data
1015:             or len(callback_data) > 256
1016:             or any(type(item) is not int or item < 0 for item in consumed_updates_before)
1017:             or len(set(consumed_updates_before)) != len(consumed_updates_before)
1018:             or update_id in consumed_updates_before
1019:             or not isinstance(publication.receipt_integrity, str)
1020:         ):
1021:             raise ValueError("owner callback event is invalid")
1022:         values: dict[str, object] = {
1023:             "session_id": session,
1024:             "customer_key": customer_key,
1025:             "action": action,
1026:             "actor_user_id": actor_user_id,
1027:             "authority": list(authority),
1028:             "route": list(current_route),
1029:             "message_id": current_message_id,
1030:             "update_id": update_id,
1031:             "consumed_updates_before": list(consumed_updates_before),
1032:             "callback_data": callback_data,
1033:             "publication_generation": publication.generation,
1034:             "publication_dispatch_identity": publication.dispatch_identity,
1035:             "publication_receipt_integrity": publication.receipt_integrity,
1036:         }
1037:         integrity = hmac.new(
1038:             self._read_key(),
1039:             self._canonical({"domain": "telegram-owner-approve-event-v1", **values}),
1040:             hashlib.sha256,
1041:         ).hexdigest()
1042:         return GatewayOwnerCallbackReceipt(
1043:             session,
1044:             customer_key,
1045:             action,
1046:             actor_user_id,
1047:             authority,
1048:             current_route,
1049:             current_message_id,
1050:             update_id,
1051:             consumed_updates_before,
1052:             callback_data,
1053:             publication.generation,
1054:             publication.dispatch_identity,
1055:             publication.receipt_integrity,
1056:             integrity,
1057:         )
1058:
1059:     def _read_owner_callbacks_unlocked(self) -> tuple[GatewayOwnerCallbackReceipt, ...]:
1060:         try:
1061:             descriptor = os.open(
1062:                 self.callback_path,
1063:                 os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0) | _NOATIME,
1064:             )
1065:         except OSError as exc:
1066:             raise ValueError("owner callback receipt ledger is unavailable") from exc
1067:         try:
1068:             self._validate_private_descriptor(descriptor, "owner callback receipt ledger")
1069:             with os.fdopen(descriptor, "r", encoding="utf-8", closefd=False) as handle:
1070:                 value = json.load(handle)
1071:         finally:
1072:             os.close(descriptor)
1073:         if not isinstance(value, Mapping) or value.get("schema") != _CALLBACK_SCHEMA:
1074:             raise ValueError("owner callback receipt ledger integrity is invalid")
1075:         records = value.get("records")
1076:         if not isinstance(records, list):
1077:             raise ValueError("owner callback receipt ledger integrity is invalid")
1078:         parsed = tuple(self._owner_callback_from_dict(item) for item in records)
1079:         if len({item.session_id for item in parsed}) != len(parsed):
1080:             raise ValueError("owner callback receipt is ambiguous")
1081:         return parsed
1082:
1083:     def _write_owner_callbacks_unlocked(
1084:         self,
1085:         records: tuple[GatewayOwnerCallbackReceipt, ...] | list[GatewayOwnerCallbackReceipt],
1086:     ) -> None:
1087:         document = {
1088:             "schema": _CALLBACK_SCHEMA,
1089:             "records": [self._owner_callback_to_dict(item) for item in records],
1090:         }
1091:         descriptor, temporary = tempfile.mkstemp(
1092:             prefix=".owner-callbacks-", suffix=".tmp", dir=self.state_dir
1093:         )
1094:         try:
1095:             os.fchmod(descriptor, _PRIVATE_MODE)
1096:             with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
1097:                 json.dump(document, handle, sort_keys=True, separators=(",", ":"))
1098:                 handle.write("\n")
1099:                 handle.flush()
1100:                 os.fsync(handle.fileno())
1101:             os.replace(temporary, self.callback_path)
1102:             directory = os.open(self.state_dir, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
1103:             try:
1104:                 os.fsync(directory)
1105:             finally:
1106:                 os.close(directory)
1107:         except BaseException:
1108:             try:
1109:                 os.unlink(temporary)
1110:             except FileNotFoundError:
1111:                 pass
1112:             raise
1113:
1114:     @staticmethod
1115:     def _owner_callback_to_dict(record: GatewayOwnerCallbackReceipt) -> dict[str, object]:
1116:         return {
1117:             "session_id": record.session_id,
1118:             "customer_key": record.customer_key,
1119:             "action": record.action,
1120:             "actor_user_id": record.actor_user_id,
1121:             "authority": list(record.authority),
1122:             "route": list(record.route),
1123:             "message_id": record.message_id,
1124:             "update_id": record.update_id,
1125:             "consumed_updates_before": list(record.consumed_updates_before),
1126:             "callback_data": record.callback_data,
1127:             "publication_generation": record.publication_generation,
1128:             "publication_dispatch_identity": record.publication_dispatch_identity,
1129:             "publication_receipt_integrity": record.publication_receipt_integrity,
1130:             "event_integrity": record.event_integrity,
1131:         }
1132:
1133:     def _owner_callback_from_dict(self, value: object) -> GatewayOwnerCallbackReceipt:
1134:         if not isinstance(value, Mapping):
1135:             raise ValueError("owner callback receipt is invalid")
1136:         record: dict[str, object] = {}
1137:         for key, item in value.items():
1138:             if not isinstance(key, str):
1139:                 raise ValueError("owner callback receipt is invalid")
1140:             record[key] = item
1141:         if set(record) != {
1142:             "session_id", "customer_key", "action", "actor_user_id", "authority", "route",
1143:             "message_id", "update_id", "consumed_updates_before", "callback_data",
1144:             "publication_generation", "publication_dispatch_identity",
1145:             "publication_receipt_integrity", "event_integrity",
1146:         }:
1147:             raise ValueError("owner callback receipt is invalid")
1148:         session_id = record["session_id"]
1149:         customer_key = record["customer_key"]
1150:         action = record["action"]
1151:         actor_user_id = record["actor_user_id"]
1152:         authority = record["authority"]
1153:         route = record["route"]
1154:         message_id = record["message_id"]
1155:         update_id = record["update_id"]
1156:         consumed = record["consumed_updates_before"]
1157:         callback_data = record["callback_data"]
1158:         generation = record["publication_generation"]
1159:         dispatch_identity = record["publication_dispatch_identity"]
1160:         receipt_integrity = record["publication_receipt_integrity"]
1161:         event_integrity = record["event_integrity"]
1162:         if (
1163:             not isinstance(session_id, str)
1164:             or not isinstance(customer_key, str)
1165:             or not isinstance(action, str)
1166:             or type(actor_user_id) is not int
1167:             or not isinstance(authority, list)
1168:             or len(authority) != 3
1169:             or not isinstance(route, list)
1170:             or len(route) != 2
1171:             or type(message_id) is not int
1172:             or type(update_id) is not int
1173:             or not isinstance(consumed, list)
1174:             or not isinstance(callback_data, str)
1175:             or type(generation) is not int
1176:             or not isinstance(dispatch_identity, str)
1177:             or not isinstance(receipt_integrity, str)
1178:             or not isinstance(event_integrity, str)
1179:         ):
1180:             raise ValueError("owner callback receipt is invalid")
1181:         owner_user_id, owner_chat_id, owner_topic_id = authority
1182:         route_chat_id, route_topic_id = route
1183:         if (
1184:             type(owner_user_id) is not int
1185:             or type(owner_chat_id) is not int
1186:             or type(owner_topic_id) is not int
1187:             or not isinstance(route_chat_id, str)
1188:             or not isinstance(route_topic_id, str)
1189:         ):
1190:             raise ValueError("owner callback receipt is invalid")
1191:         consumed_updates: list[int] = []
1192:         for item in consumed:
1193:             if type(item) is not int:
1194:                 raise ValueError("owner callback receipt is invalid")
1195:             consumed_updates.append(item)
1196:         publication = GatewayPublicationReceipt(
1197:             "placeholder",
1198:             generation,
1199:             {},
1200:             (route_chat_id, route_topic_id),
1201:             "owner",
1202:             "0" * 64,
1203:             "0" * 64,
1204:             dispatch_identity,
1205:             "COMMITTED",
1206:             message_id,
1207:             receipt_integrity,
1208:         )
1209:         candidate = self._owner_callback_candidate(
1210:             session_id=session_id,
1211:             customer_key=customer_key,
1212:             action=action,
1213:             actor_user_id=actor_user_id,
1214:             authority=(owner_user_id, owner_chat_id, owner_topic_id),
1215:             route=(route_chat_id, route_topic_id),
1216:             message_id=message_id,
1217:             update_id=update_id,
1218:             consumed_updates_before=tuple(consumed_updates),
1219:             callback_data=callback_data,
1220:             publication=publication,
1221:         )
1222:         if not hmac.compare_digest(event_integrity, candidate.event_integrity):
1223:             raise ValueError("owner callback receipt integrity is invalid")
1224:         return candidate
1225:
1226:     @staticmethod
1227:     def _record_to_dict(record: GatewayPublicationReceipt) -> dict[str, object]:
1228:         return {
1229:             "session_id": record.session_id,
1230:             "generation": record.generation,
1231:             "payload": record.payload,
1232:             "route": list(record.route),
1233:             "role": record.role,
1234:             "render_identity": record.render_identity,
1235:             "payload_digest": record.payload_digest,
1236:             "dispatch_identity": record.dispatch_identity,
1237:             "state": record.state,
1238:             "message_id": record.message_id,
1239:             "receipt_integrity": record.receipt_integrity,
1240:         }
1241:
1242:     def _record_from_dict(
1243:         self,
1244:         value: object,
1245:         *,
1246:         emergency: bool,
1247:     ) -> GatewayPublicationReceipt:
1248:         if not isinstance(value, Mapping):
1249:             raise ValueError("onboarding publication outbox record is invalid")
1250:         record: dict[str, object] = {}
1251:         for key, item in value.items():
1252:             if not isinstance(key, str):
1253:                 raise ValueError("onboarding publication outbox record is invalid")
1254:             record[key] = item
1255:         if set(record) != {
1256:             "session_id", "generation", "payload", "route", "role", "render_identity",
1257:             "payload_digest", "dispatch_identity", "state", "message_id", "receipt_integrity",
1258:         }:
1259:             raise ValueError("onboarding publication outbox record is invalid")
1260:         session_id = record["session_id"]
1261:         generation = record["generation"]
1262:         payload = record["payload"]
1263:         route = record["route"]
1264:         role = record["role"]
1265:         render_identity = record["render_identity"]
1266:         payload_digest = record["payload_digest"]
1267:         dispatch_identity = record["dispatch_identity"]
1268:         state = record["state"]
1269:         message_id = record["message_id"]
1270:         receipt_integrity = record["receipt_integrity"]
1271:         if (
1272:             not isinstance(session_id, str)
1273:             or not isinstance(generation, int)
1274:             or isinstance(generation, bool)
1275:             or not isinstance(payload, Mapping)
1276:             or not isinstance(route, list)
1277:             or len(route) != 2
1278:             or not isinstance(role, str)
1279:             or not isinstance(render_identity, str)
1280:             or not isinstance(payload_digest, str)
1281:             or not isinstance(dispatch_identity, str)
1282:             or not isinstance(state, str)
1283:         ):
1284:             raise ValueError("onboarding publication outbox record is invalid")
1285:         route_first, route_second = route
1286:         if not isinstance(route_first, str) or not isinstance(route_second, str):
1287:             raise ValueError("onboarding publication outbox record is invalid")
1288:         candidate = self._candidate(
1289:             session_id=session_id,
1290:             generation=generation,
1291:             payload={str(key): item for key, item in payload.items() if isinstance(key, str)},
1292:             route=(route_first, route_second),
1293:             role=role,
1294:             render_identity=render_identity,
1295:         )
1296:         if (
1297:             not hmac.compare_digest(payload_digest, candidate.payload_digest)
1298:             or not hmac.compare_digest(dispatch_identity, candidate.dispatch_identity)
1299:         ):
1300:             raise ValueError("onboarding publication authority integrity is invalid")
1301:         allowed_states = _EMERGENCY_STATES if emergency else _STATES
1302:         if state not in allowed_states:
1303:             raise ValueError("onboarding publication outbox record is invalid")
1304:         if state == "DISPATCHING":
1305:             if message_id is not None or receipt_integrity is not None:
1306:                 raise ValueError("dispatching publication cannot have a receipt")
1307:             return candidate
1308:         if (
1309:             type(message_id) is not int
1310:             or isinstance(message_id, bool)
1311:             or message_id <= 0
1312:             or not isinstance(receipt_integrity, str)
1313:             or _DIGEST.fullmatch(receipt_integrity) is None
1314:         ):
1315:             raise ValueError("receipted publication requires valid receipt integrity")
1316:         received = self._with_receipt(candidate, message_id)
1317:         if not hmac.compare_digest(receipt_integrity, received.receipt_integrity or ""):
1318:             raise ValueError("onboarding publication receipt integrity is invalid")
1319:         return self._with_state(received, state)
1320:
1321:     @staticmethod
1322:     def _matching_record(
1323:         records: tuple[GatewayPublicationReceipt, ...] | list[GatewayPublicationReceipt],
1324:         session_id: str,
1325:         generation: int,
1326:     ) -> GatewayPublicationReceipt | None:
1327:         matches = [
1328:             record
1329:             for record in records
1330:             if record.session_id == session_id and record.generation == generation
1331:         ]
1332:         if not matches:
1333:             return None
1334:         if len(matches) != 1:
1335:             raise ValueError("onboarding publication receipt is ambiguous")
1336:         return matches[0]
1337:
1338:     @classmethod
1339:     def _find(
1340:         cls,
1341:         records: list[GatewayPublicationReceipt],
1342:         session_id: str,
1343:         generation: int,
1344:     ) -> tuple[int, GatewayPublicationReceipt]:
1345:         for index, record in enumerate(records):
1346:             if record.session_id == session_id and record.generation == generation:
1347:                 return index, record
1348:         raise ValueError("onboarding publication receipt is unavailable")