from __future__ import annotations import secrets import time from dataclasses import dataclass, field from yuxi.channels.adapters.slack.security import SecurityDecision def _normalize_code(code: str) -> str: for prefix in ("slack:", "Slack:", "SLACK:"): if code.startswith(prefix): return code[len(prefix) :] return code @dataclass class PendingPairing: user_id: str pairing_code: str created_at: float = field(default_factory=time.monotonic) def is_expired(self, ttl_seconds: float = 300.0) -> bool: return time.monotonic() - self.created_at > ttl_seconds class PairingManager: def __init__(self, ttl_seconds: float = 300.0): self._ttl = ttl_seconds self._pending: dict[str, PendingPairing] = {} self._approved_users: set[str] = set() def generate_pairing(self, user_id: str) -> SecurityDecision: if user_id in self._approved_users: return SecurityDecision(allowed=True, reason="user_approved") existing = self._pending.get(user_id) if existing and not existing.is_expired(self._ttl): return SecurityDecision( allowed=False, requires_pairing=True, pairing_code=existing.pairing_code, reason="pending_pairing_exists", ) code = secrets.token_hex(3).upper() pairing = PendingPairing(user_id=user_id, pairing_code=code) self._pending[user_id] = pairing return SecurityDecision( allowed=False, requires_pairing=True, pairing_code=code, reason="pairing_required", ) def approve(self, pairing_code: str) -> str | None: normalized = _normalize_code(pairing_code) for user_id, pairing in list(self._pending.items()): if pairing.pairing_code == normalized: if pairing.is_expired(self._ttl): self._pending.pop(user_id, None) return None self._approved_users.add(user_id) self._pending.pop(user_id, None) return user_id return None def is_approved(self, user_id: str) -> bool: return user_id in self._approved_users def clear_expired(self) -> None: for user_id, pairing in list(self._pending.items()): if pairing.is_expired(self._ttl): self._pending.pop(user_id, None) def clear_all(self) -> None: self._pending.clear() @property def pending_count(self) -> int: self.clear_expired() return len(self._pending)