main
py 146 lines 3.45 KB
Raw
1 from __future__ import annotations
2
3 import threading
4 import time
5 from dataclasses import dataclass, field
6 from typing import Any
7
8
9 LOGIN_TTL_SECONDS = 10 * 60
10
11
12 @dataclass(frozen=True)
13 class LoginAttempt:
14 state: str
15 verifier: str
16 redirect_uri: str
17 created_at: float
18 provider_id: str = "codex_oauth"
19 extra: dict[str, Any] = field(default_factory=dict)
20
21 @property
22 def expires_at(self) -> float:
23 return self.created_at + LOGIN_TTL_SECONDS
24
25 def expired(self) -> bool:
26 return time.time() > self.expires_at
27
28
29 @dataclass(frozen=True)
30 class DeviceAttempt:
31 attempt_id: str
32 device_auth_id: str
33 user_code: str
34 interval: int
35 expires_at_value: float
36 provider_id: str = "codex_oauth"
37 extra: dict[str, Any] = field(default_factory=dict)
38
39 @property
40 def expires_at(self) -> float:
41 return self.expires_at_value
42
43 def expired(self) -> bool:
44 return time.time() > self.expires_at
45
46
47 _lock = threading.RLock()
48 _attempts: dict[str, LoginAttempt] = {}
49 _device_attempts: dict[str, DeviceAttempt] = {}
50
51
52 def put_attempt(
53 state: str,
54 verifier: str,
55 redirect_uri: str,
56 *,
57 provider_id: str = "codex_oauth",
58 extra: dict[str, Any] | None = None,
59 ) -> LoginAttempt:
60 cleanup_expired()
61 attempt = LoginAttempt(
62 state=state,
63 verifier=verifier,
64 redirect_uri=redirect_uri,
65 created_at=time.time(),
66 provider_id=provider_id,
67 extra=dict(extra or {}),
68 )
69 with _lock:
70 _attempts[state] = attempt
71 return attempt
72
73
74 def get_attempt(state: str) -> LoginAttempt | None:
75 cleanup_expired()
76 with _lock:
77 attempt = _attempts.get(state)
78 if attempt is None or attempt.expired():
79 return None
80 return attempt
81
82
83 def pop_attempt(state: str) -> LoginAttempt | None:
84 cleanup_expired()
85 with _lock:
86 attempt = _attempts.pop(state, None)
87 if attempt is None or attempt.expired():
88 return None
89 return attempt
90
91
92 def put_device_attempt(
93 attempt_id: str,
94 device_auth_id: str,
95 user_code: str,
96 interval: int,
97 expires_at: float,
98 *,
99 provider_id: str = "codex_oauth",
100 extra: dict[str, Any] | None = None,
101 ) -> DeviceAttempt:
102 cleanup_expired()
103 attempt = DeviceAttempt(
104 attempt_id=attempt_id,
105 device_auth_id=device_auth_id,
106 user_code=user_code,
107 interval=interval,
108 expires_at_value=expires_at,
109 provider_id=provider_id,
110 extra=dict(extra or {}),
111 )
112 with _lock:
113 _device_attempts[attempt_id] = attempt
114 return attempt
115
116
117 def get_device_attempt(attempt_id: str) -> DeviceAttempt | None:
118 cleanup_expired()
119 with _lock:
120 attempt = _device_attempts.get(attempt_id)
121 if attempt is None or attempt.expired():
122 return None
123 return attempt
124
125
126 def pop_device_attempt(attempt_id: str) -> DeviceAttempt | None:
127 cleanup_expired()
128 with _lock:
129 return _device_attempts.pop(attempt_id, None)
130
131
132 def cleanup_expired() -> None:
133 now = time.time()
134 with _lock:
135 expired = [
136 state for state, attempt in _attempts.items() if now > attempt.expires_at
137 ]
138 for state in expired:
139 _attempts.pop(state, None)
140 expired_devices = [
141 attempt_id
142 for attempt_id, attempt in _device_attempts.items()
143 if now > attempt.expires_at
144 ]
145 for attempt_id in expired_devices:
146 _device_attempts.pop(attempt_id, None)