Coverage for src / lexigram / contracts / auth / models.py: 100%
40 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
1"""Authentication data models and protocols.
3Shared auth data models (pure dataclasses with no external deps) live here
4so both lexigram-auth and other packages can reference them without
5creating cross-extension imports.
6"""
8from __future__ import annotations
10from dataclasses import dataclass, field
11from datetime import UTC, datetime
12from typing import Any, Protocol, runtime_checkable
15@runtime_checkable
16class UserIdentityProtocol(Protocol):
17 """Protocol for a user's identity."""
19 @property
20 def user_id(self) -> str: ...
22 @property
23 def email(self) -> str: ...
26@dataclass(frozen=True)
27class UserSession:
28 """User session data model.
30 Tracks a specific device/session for a user. Defined in contracts so
31 both lexigram-auth and other packages can share the type without
32 creating a cross-extension dependency.
33 """
35 session_id: str
36 user_id: str
37 device_id: str
38 ip_address: str | None = None
39 user_agent: str | None = None
40 geo_location: dict[str, Any] = field(default_factory=dict)
41 fingerprint: dict[str, Any] = field(default_factory=dict)
42 is_active: bool = True
43 expires_at: datetime | None = None
44 last_active_at: datetime | None = None
45 mfa_verified_at: datetime | None = None
46 created_at: datetime | None = None
47 updated_at: datetime | None = None
49 def is_expired(self) -> bool:
50 """Return True if the session has passed its expiry timestamp."""
51 if not self.expires_at:
52 return False
53 now = datetime.now(UTC) if self.expires_at.tzinfo else datetime.now()
54 return self.expires_at < now
57@dataclass(frozen=True)
58class VerifiedIdentityClaims:
59 """Verified external identity claims returned by OAuth providers.
61 This value object captures the normalized claims extracted after a
62 provider-specific verification step. It is intentionally provider-neutral
63 so apps can hand the result to an identity resolver without touching raw
64 HTTP responses or provider-specific dict shapes.
65 """
67 provider: str
68 provider_user_id: str
69 email: str | None = None
70 email_verified: bool = False
71 name: str | None = None
72 picture: str | None = None
73 issuer: str | None = None
74 audience: str | None = None
75 expires_at: datetime | None = None
76 issued_at: datetime | None = None
77 raw_data: dict[str, Any] = field(default_factory=dict)
80__all__ = ["UserIdentityProtocol", "UserSession", "VerifiedIdentityClaims"]