hyperpocket 0.0.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. hyperpocket/__init__.py +7 -0
  2. hyperpocket/auth/README.KR.md +309 -0
  3. hyperpocket/auth/README.md +323 -0
  4. hyperpocket/auth/__init__.py +24 -0
  5. hyperpocket/auth/calendly/__init__.py +0 -0
  6. hyperpocket/auth/calendly/context.py +13 -0
  7. hyperpocket/auth/calendly/oauth2_context.py +25 -0
  8. hyperpocket/auth/calendly/oauth2_handler.py +146 -0
  9. hyperpocket/auth/calendly/oauth2_schema.py +16 -0
  10. hyperpocket/auth/context.py +38 -0
  11. hyperpocket/auth/github/__init__.py +0 -0
  12. hyperpocket/auth/github/context.py +13 -0
  13. hyperpocket/auth/github/oauth2_context.py +25 -0
  14. hyperpocket/auth/github/oauth2_handler.py +143 -0
  15. hyperpocket/auth/github/oauth2_schema.py +16 -0
  16. hyperpocket/auth/github/token_context.py +12 -0
  17. hyperpocket/auth/github/token_handler.py +79 -0
  18. hyperpocket/auth/github/token_schema.py +9 -0
  19. hyperpocket/auth/google/__init__.py +0 -0
  20. hyperpocket/auth/google/context.py +15 -0
  21. hyperpocket/auth/google/oauth2_context.py +31 -0
  22. hyperpocket/auth/google/oauth2_handler.py +137 -0
  23. hyperpocket/auth/google/oauth2_schema.py +18 -0
  24. hyperpocket/auth/handler.py +171 -0
  25. hyperpocket/auth/linear/__init__.py +0 -0
  26. hyperpocket/auth/linear/context.py +15 -0
  27. hyperpocket/auth/linear/token_context.py +15 -0
  28. hyperpocket/auth/linear/token_handler.py +68 -0
  29. hyperpocket/auth/linear/token_schema.py +9 -0
  30. hyperpocket/auth/provider.py +16 -0
  31. hyperpocket/auth/schema.py +19 -0
  32. hyperpocket/auth/slack/__init__.py +0 -0
  33. hyperpocket/auth/slack/context.py +15 -0
  34. hyperpocket/auth/slack/oauth2_context.py +40 -0
  35. hyperpocket/auth/slack/oauth2_handler.py +151 -0
  36. hyperpocket/auth/slack/oauth2_schema.py +40 -0
  37. hyperpocket/auth/slack/tests/__init__.py +0 -0
  38. hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
  39. hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
  40. hyperpocket/auth/slack/token_context.py +14 -0
  41. hyperpocket/auth/slack/token_handler.py +64 -0
  42. hyperpocket/auth/slack/token_schema.py +9 -0
  43. hyperpocket/auth/tests/__init__.py +0 -0
  44. hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
  45. hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
  46. hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
  47. hyperpocket/cli/__init__.py +0 -0
  48. hyperpocket/cli/__main__.py +12 -0
  49. hyperpocket/cli/pull.py +18 -0
  50. hyperpocket/cli/sync.py +17 -0
  51. hyperpocket/config/__init__.py +9 -0
  52. hyperpocket/config/auth.py +36 -0
  53. hyperpocket/config/git.py +17 -0
  54. hyperpocket/config/logger.py +81 -0
  55. hyperpocket/config/session.py +35 -0
  56. hyperpocket/config/settings.py +62 -0
  57. hyperpocket/constants.py +0 -0
  58. hyperpocket/curated_tools.py +10 -0
  59. hyperpocket/external/__init__.py +7 -0
  60. hyperpocket/external/github_client.py +19 -0
  61. hyperpocket/futures/__init__.py +7 -0
  62. hyperpocket/futures/futurestore.py +48 -0
  63. hyperpocket/pocket_auth.py +344 -0
  64. hyperpocket/pocket_main.py +351 -0
  65. hyperpocket/prompts.py +15 -0
  66. hyperpocket/repository/__init__.py +5 -0
  67. hyperpocket/repository/lock.py +156 -0
  68. hyperpocket/repository/lockfile.py +56 -0
  69. hyperpocket/repository/repository.py +18 -0
  70. hyperpocket/server/__init__.py +3 -0
  71. hyperpocket/server/auth/__init__.py +15 -0
  72. hyperpocket/server/auth/calendly.py +16 -0
  73. hyperpocket/server/auth/github.py +25 -0
  74. hyperpocket/server/auth/google.py +16 -0
  75. hyperpocket/server/auth/linear.py +18 -0
  76. hyperpocket/server/auth/slack.py +28 -0
  77. hyperpocket/server/auth/token.py +51 -0
  78. hyperpocket/server/proxy.py +63 -0
  79. hyperpocket/server/server.py +178 -0
  80. hyperpocket/server/tool/__init__.py +10 -0
  81. hyperpocket/server/tool/dto/__init__.py +0 -0
  82. hyperpocket/server/tool/dto/script.py +15 -0
  83. hyperpocket/server/tool/wasm.py +31 -0
  84. hyperpocket/session/README.KR.md +62 -0
  85. hyperpocket/session/README.md +61 -0
  86. hyperpocket/session/__init__.py +4 -0
  87. hyperpocket/session/in_memory.py +76 -0
  88. hyperpocket/session/interface.py +118 -0
  89. hyperpocket/session/redis.py +126 -0
  90. hyperpocket/session/tests/__init__.py +0 -0
  91. hyperpocket/session/tests/test_in_memory.py +145 -0
  92. hyperpocket/session/tests/test_redis.py +151 -0
  93. hyperpocket/tests/__init__.py +0 -0
  94. hyperpocket/tests/test_pocket.py +118 -0
  95. hyperpocket/tests/test_pocket_auth.py +982 -0
  96. hyperpocket/tool/README.KR.md +68 -0
  97. hyperpocket/tool/README.md +75 -0
  98. hyperpocket/tool/__init__.py +13 -0
  99. hyperpocket/tool/builtins/__init__.py +0 -0
  100. hyperpocket/tool/builtins/example/__init__.py +0 -0
  101. hyperpocket/tool/builtins/example/add_tool.py +18 -0
  102. hyperpocket/tool/function/README.KR.md +159 -0
  103. hyperpocket/tool/function/README.md +169 -0
  104. hyperpocket/tool/function/__init__.py +9 -0
  105. hyperpocket/tool/function/annotation.py +30 -0
  106. hyperpocket/tool/function/tool.py +87 -0
  107. hyperpocket/tool/tests/__init__.py +0 -0
  108. hyperpocket/tool/tests/test_function_tool.py +266 -0
  109. hyperpocket/tool/tool.py +106 -0
  110. hyperpocket/tool/wasm/README.KR.md +144 -0
  111. hyperpocket/tool/wasm/README.md +144 -0
  112. hyperpocket/tool/wasm/__init__.py +3 -0
  113. hyperpocket/tool/wasm/browser.py +63 -0
  114. hyperpocket/tool/wasm/invoker.py +41 -0
  115. hyperpocket/tool/wasm/script.py +82 -0
  116. hyperpocket/tool/wasm/templates/__init__.py +28 -0
  117. hyperpocket/tool/wasm/templates/node.py +87 -0
  118. hyperpocket/tool/wasm/templates/python.py +75 -0
  119. hyperpocket/tool/wasm/tool.py +147 -0
  120. hyperpocket/util/__init__.py +1 -0
  121. hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
  122. hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
  123. hyperpocket/util/find_all_subclass_in_package.py +29 -0
  124. hyperpocket/util/flatten_json_schema.py +45 -0
  125. hyperpocket/util/function_to_model.py +46 -0
  126. hyperpocket/util/get_objects_from_subpackage.py +28 -0
  127. hyperpocket/util/json_schema_to_model.py +69 -0
  128. hyperpocket-0.0.1.dist-info/METADATA +304 -0
  129. hyperpocket-0.0.1.dist-info/RECORD +131 -0
  130. hyperpocket-0.0.1.dist-info/WHEEL +4 -0
  131. hyperpocket-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,7 @@
1
+ from hyperpocket.pocket_main import Pocket
2
+ from hyperpocket.pocket_auth import PocketAuth
3
+
4
+ __all__ = [
5
+ 'Pocket',
6
+ 'PocketAuth',
7
+ ]
@@ -0,0 +1,309 @@
1
+ ## Auth
2
+
3
+ 각 provider에서 유저를 식별하기 위한 프로세스
4
+
5
+ ### AuthContext
6
+
7
+ 실제 유저의 인증 정보(e.g., access token)을 담고 있는 객체
8
+
9
+ ```python
10
+ class AuthContext(BaseModel, ABC):
11
+ """
12
+ This class is used to define the interface of the authentication model.
13
+ """
14
+ access_token: str = Field(description="user's access token")
15
+ description: str = Field(description="description of this authentication context")
16
+ expires_at: Optional[datetime] = Field(description="expiration datetime")
17
+ detail: Optional[Any] = Field(default=None, description="detailed information")
18
+ ```
19
+
20
+ 일반적으로 해당 객체에서 access token의 key 정보나 response 객체를 AuthContext로 변환해주는 역할을 수행한다.
21
+
22
+ ### AuthenticateRequest
23
+
24
+ authentication 수행 시에 필요한 정보를 담고 있는 객체
25
+
26
+ ```python
27
+ class AuthenticateRequest(BaseModel):
28
+ auth_scopes: Optional[list[str]] = Field(default_factory=list,
29
+ description="authentication scopes. if the authentication handler is non scoped, it isn't needed")
30
+ ```
31
+
32
+ - 일반적으로 인증 시에 필요한 client id 및 secret id 등과 같은 정보를 request에 같이 전달한다.
33
+
34
+ **examples**
35
+
36
+ ```python
37
+ class GitHubOAuth2Request(AuthenticateRequest):
38
+ client_id: str
39
+ client_secret: str
40
+ ```
41
+
42
+ ### AuthenticateResponse(Optional)
43
+
44
+ authentication response에서 필수 정보를 담고 있는 객체
45
+
46
+ ```python
47
+ class AuthenticateResponse(BaseModel):
48
+ """
49
+ This class is used to define the interface of the authentication response.
50
+ """
51
+ pass
52
+ ```
53
+
54
+ - handler에서 response를 dict 형태로 처리한다면, 구현할 필요가 없다.
55
+
56
+ ### AuthHandlerInterface
57
+
58
+ 실제 authentication을 수행하는 객체
59
+
60
+ ```python
61
+ class AuthHandlerInterface(ABC):
62
+ name: str = Field(description="name of the authentication handler")
63
+ description: str = Field(description="description of the authentication handler")
64
+ scoped: bool = Field(description="Indicates whether the handler requires an auth_scope for access control")
65
+
66
+
67
+ @staticmethod
68
+ def provider() -> AuthProvider:
69
+ """
70
+ Returns the authentication provider enum.
71
+
72
+ This method is used to determine the appropriate authentication handler
73
+ based on the authentication provider.
74
+ """
75
+ raise NotImplementedError()
76
+
77
+ @staticmethod
78
+ def provider_default() -> bool:
79
+ """
80
+ Indicates whether this authentication handler is the default handler.
81
+
82
+ If no specific handler is designated, the default handler will be used.
83
+
84
+ Returns:
85
+ bool: True if this handler is the default, False otherwise.
86
+ """
87
+ return False
88
+
89
+ @staticmethod
90
+ def recommended_scopes() -> set[str]:
91
+ """
92
+ Returns the recommended authentication scopes.
93
+
94
+ If `use_recommended_scope` is set to True in the `AuthConfig`,
95
+ this method should return the proper recommended scopes. Otherwise,
96
+ it should return an empty set.
97
+
98
+ Returns:
99
+ set[str]: A set of recommended scopes, or an empty set if not applicable.
100
+
101
+ Examples:
102
+ Slack OAuth2 recommended_scopes::
103
+
104
+ def recommended_scopes() -> set[str]:
105
+ if config.auth.slack.use_recommended_scope:
106
+ recommended_scopes = {
107
+ "channels:history",
108
+ "channels:read",
109
+ "chat:write",
110
+ "groups:history",
111
+ "groups:read",
112
+ "im:history",
113
+ "mpim:history",
114
+ "reactions:read",
115
+ "reactions:write",
116
+ }
117
+ else:
118
+ recommended_scopes = {}
119
+ return recommended_scopes
120
+ """
121
+ raise NotImplementedError()
122
+
123
+ def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> AuthenticateRequest:
124
+ """
125
+ Make an AuthenticationRequest.
126
+
127
+ Usually, this method only requires `auth_scopes`.
128
+ If additional static information is needed (e.g., clientID, secretID),
129
+ retrieve it from the configuration.
130
+
131
+ Args:
132
+ auth_scopes (Optional[list[str]]): list of auth scopes
133
+
134
+ Returns:
135
+ AuthenticateRequest: A authentication request object with the necessary details.
136
+
137
+ Examples:
138
+ Create a Slack OAuth2 request::
139
+
140
+ def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> SlackOAuth2Request:
141
+ return SlackOAuth2Request(
142
+ auth_scopes=auth_scopes,
143
+ client_id=config.auth.slack.client_id,
144
+ client_secret=config.auth.slack.client_secret
145
+ )
146
+ """
147
+ raise NotImplementedError()
148
+
149
+ def prepare(self, auth_req: AuthenticateRequest, thread_id: str, profile: str,
150
+ future_uid: str, *args, **kwargs) -> str:
151
+ """
152
+ Performs preliminary tasks required for authentication.
153
+
154
+ This method typically performs the following actions:
155
+ - Creates a future to wait for user authentication completion during the authentication process.
156
+ - Issues an authentication URI that the user can access.
157
+
158
+ Args:
159
+ auth_req (AuthenticateRequest): The authentication request object.
160
+ thread_id (str): The thread ID.
161
+ profile (str): The profile name.
162
+ future_uid (str): A unique identifier for each future.
163
+
164
+ Returns:
165
+ str: The authentication URI that the user can access.
166
+ """
167
+ raise NotImplementedError()
168
+
169
+ @abstractmethod
170
+ async def authenticate(self, auth_req: AuthenticateRequest, future_uid: str, *args, **kwargs) -> AuthContext:
171
+ """
172
+ Performs the actual authentication process.
173
+
174
+ This function assumes that the user has completed the authentication during the `prepare` step,
175
+ and the associated future has been resolved. At this point, the result contains the required
176
+ values for authentication (e.g., an auth code).
177
+
178
+ Typically, this process involves:
179
+ - Accessing the resolved future to retrieve the necessary values for authentication.
180
+ - Performing the actual authentication using these values.
181
+ - Converting the returned response into an appropriate `AuthContext` object and returning it.
182
+
183
+ Args:
184
+ auth_req (AuthenticateRequest): The authentication request object.
185
+ future_uid (str): A unique identifier for the future, used to retrieve the correct
186
+ result issued during the `prepare` step.
187
+
188
+ Returns:
189
+ AuthContext: The authentication context object containing the authentication result.
190
+ """
191
+ raise NotImplementedError()
192
+
193
+ @abstractmethod
194
+ async def refresh(self, auth_req: AuthenticateRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
195
+ """
196
+ Performs re-authentication for an expired session.
197
+
198
+ This method is optional and does not need to be implemented for handlers that do not require re-authentication.
199
+
200
+ Typically, the information needed for re-authentication (e.g., a refresh token) should be stored
201
+ within the `AuthContext` during the previous authentication step.
202
+ In the `refresh` step, this method accesses the necessary re-authentication details from the provided `context`,
203
+ performs the re-authentication, and returns an updated `AuthContext`.
204
+
205
+ Args:
206
+ auth_req (AuthenticateRequest): The authentication request object.
207
+ context (AuthContext): The current authentication context that it should contain data required for re-authentication.
208
+
209
+ Returns:
210
+ AuthContext: An updated authentication context object.
211
+ """
212
+ raise NotImplementedError()
213
+ ```
214
+
215
+ ## How To Implement Auth
216
+
217
+ 1. `AuthenticateRequest`을 상속받은 구현체 구현
218
+ - 해당 클래스에서는 실제 prepare 및 authenticate를 할 때 필요한 정보를 저장하고 있어야 합니다.
219
+
220
+ 2. `AuthContext`을 상속받은 구현체 구현
221
+ - Context 내에는 다음과 같은 정보들이 존재해야 합니다.
222
+ - 각 tool에서 접근할 수 있는 access_key
223
+ - response를 context로 변환할 수 있는 함수
224
+
225
+ 3. `Response`(Optional) 객체 구현
226
+ - 필요시 authentication을 완료 후 받은 정보를 parsing할 구현체를 구현
227
+
228
+ 4. `AuthHandlerInterface`을 상속받은 구현체 구현
229
+ - 실제 prepare, authentication, refresh를 수행할 객체를 구현합니다.
230
+
231
+ 5. (신규 auth provider인 경우) `AuthProvider`에 새로운 enum value를 추가
232
+ - 만약 새로운 `AuthProvider`가 추가되는 경우라면, `pocket/auth/provider.py` 내에 새로운 AuthProvider Enum을 등록해야 합니다.
233
+
234
+ 6. 신규 Auth callback server enpoint 추가
235
+ - `pocket/server/auth/` 하위에 유저가 authentication을 완료하면 callback될 server endpoint를 추가해야 합니다.
236
+ - 해당 패키지 하위에 적절한 `APIRouter`를 선언해놓으면 pocket 초기화 시에 자동으로 end point를 pocket server에 등록해주게 됩니다.
237
+
238
+ 7. Auth Test code 추가(Optional)
239
+ - `pocket/auth/tests` 하위에 테스트 코드 추가
240
+
241
+ ---
242
+
243
+ ## Auth Flow(Advanced)
244
+
245
+ ### Session State 정의
246
+
247
+ - SKIP_AUTH : Auth가 존재하고, 들어온 요청에 대한 권한도 가지고 있는 상태
248
+ - DO_AUTH : Auth가 존재하지만, 들어온 요청에 대한 권한이 없기 때문에 auth가 필요한 상태
249
+ - DO_REFRESH : Auth가 존재하지만, 만료가 되어 refresh가 필요한 상태
250
+ - NO_SESSION : Auth가 존재하지 않는 상태
251
+ - PENDING_RESOLVE : Resource Owner에게 authorization URI를 전달하고 인증을 기다리는 상태
252
+ - RESOLVED : Resource Owner가 authorization을 완료해 서버에서 code를 받은 상태
253
+
254
+ ### 01. check
255
+
256
+ 처음 요청이 들어왔을 때 현재 사용자의 session이 어느 상태인지를 check하게 된다.
257
+
258
+ 1. 사용자의 현재 session 조회
259
+ - 존재 하지 않으면 NO_SESSION 상태 반환
260
+ 2. session의 auth resolve uid가 존재하는지 확인
261
+ - auth resolve uid가 아직 존재하는 것은 사용자에게 authentication URI는 전달하였지만 서버에서 access token을 아직 얻지는 못 한 상태
262
+ - auth resolve uid가 존재하면서 현재 사용자가 인증을 완료한 상태인지 확인
263
+ - 사용자가 인증을 완료한 상태라면 RESOLVED 상태 반환
264
+ - 사용자가 인증을 완료하지 못 한 상태라면 PENDING_RESOLVE 상태 반환
265
+ 3. 현재 인증된 session이 현재 요청에 적용이 가능한지를 확인
266
+ - 현재 session의 auth provider와 필요한 session의 auth provider가 동일한지를 확인
267
+ - 다르다면 반드시 재인증이 필요
268
+ - 현재 session을 인증한 auto provider가 non scoped인지 확인
269
+ - scoped가 아닌 경우에는 모든 권한이 있기 때문에 항상 사용 가능
270
+ - scoped인 경우 필요한 scope을 이미 가지고 있는지 확인
271
+ - 이미 가지고 있는 경우 그대로 사용하면 된다.
272
+ - 가지고 있지 않은 경우 새로운 인증 요청이 필요. 이때 기존 갖고 있던 scope과 새로 필요한 scope의 합집합을 다시 요청하게 된다.
273
+ - 위 과정을 통해 현재 session에서 적용이 불가능 하다고 판단되면 DO_AUTH 상태 반환
274
+ 4. 현재 인증된 session이 만료되었는지 확인
275
+ - 만료되었다면 DO_REFRESH 상태 반환
276
+ 5. 위 단계를 모두 통과한 경우에만 SKIP_AUTH 상태 반환
277
+
278
+ ### 02. prepare
279
+
280
+ 1. auth state가 SKIP_AUTH, DO_REFRESH, RESOLVED인 경우에는 그대로 함수를 종료
281
+ 2. DO_AUTH, NO_SESSION인 경우에는 새로운 session과 future를 만들고, 사용자에게 authenticate URI를 반환
282
+ 3. PENDING_RESOLVED인 경우에는 기존 session의 authenticate URI를 다시 반환
283
+
284
+ ```mermaid
285
+ flowchart TD
286
+ Check["Prepare Auth"] --> A["Is AuthState<br>SKIP_AUTH, DO_REFRESH or RESOLVED?"]
287
+ A -->|Yes| B["Return<br>None"]
288
+ A -->|No| C["Is AuthState PENDING_RESOLVED?"]
289
+ C -->|YES| D["Return<br>Previous Authorization URI"]
290
+ C -->|NO| E["Make New Session"]
291
+ E --> F["Return<br>Authorization URI"]
292
+ ```
293
+
294
+ ### 03. Authenticate
295
+
296
+ 1. auth state가 SKIP_AUTH인 경우에는 기존 session을 그대로 반환
297
+ 2. auth state가 DO_REFRESH인 경우에는 refresh를 수행하고 기존 session을 교체
298
+ 3. auth state가 PENDING_RESOLVE 또는 RESOLVED인 경우에는 사용자로부터 전달받은 code로 authenticate를 수행
299
+ 4. 그 외 auth state(NO_SESSION, DO_AUTH)는 해당 단계로 진입하지 않는다.
300
+
301
+ ```mermaid
302
+ flowchart TD
303
+ Check["Authenticate"]
304
+ Check -->|Is AuthState<br>SKIP_AUTH?| A2["Return<br>Existing Session"]
305
+ Check -->|Is AuthState<br>DO_REFRESH?| B2["Refresh<br>Access Token"]
306
+ Check -->|Is AuthState PENDING_RESOLVE<br>or<br>RESOLVED?| C2["Wait for Code<br>And<br>Authenticate"]
307
+ Check -->|Is AuthState<br>NO_SESION<br>or<br>DO_AUTH?| D2["Can't be reached<br>while in state"]
308
+ ```
309
+
@@ -0,0 +1,323 @@
1
+ # Auth
2
+
3
+ A Process to identify users for each provider.
4
+
5
+ ---
6
+
7
+ ## Interface
8
+
9
+ ### AuthContext
10
+
11
+ An object containing the actual authentication information of the user (e.g., access token).
12
+
13
+ ```python
14
+ class AuthContext(BaseModel, ABC):
15
+ """
16
+ This class is used to define the interface of the authentication model.
17
+ """
18
+ access_token: str = Field(description="user's access token")
19
+ description: str = Field(description="description of this authentication context")
20
+ expires_at: Optional[datetime] = Field(description="expiration datetime")
21
+ detail: Optional[Any] = Field(default=None, description="detailed information")
22
+ ```
23
+
24
+ - The primary purpose of this object is to transform key details, such as the access token or response object, into an
25
+ AuthContext.
26
+
27
+ ### AuthenticateRequest
28
+
29
+ An object containing the necessary information to perform authentication.
30
+
31
+ ```python
32
+ class AuthenticateRequest(BaseModel):
33
+ auth_scopes: Optional[list[str]] = Field(
34
+ default_factory=list,
35
+ description="authentication scopes. if the authentication handler is non scoped, it isn't needed")
36
+ ```
37
+
38
+ - Typically, this object also includes information like client ID and secret ID required for authentication
39
+
40
+ **examples**
41
+
42
+ ```python
43
+ class GitHubOAuth2Request(AuthenticateRequest):
44
+ client_id: str
45
+ client_secret: str
46
+ ```
47
+
48
+ ### AuthenticateResponse(Optional)
49
+
50
+ An object containing the necessary information from authentication response.
51
+
52
+ ```python
53
+ class AuthenticateResponse(BaseModel):
54
+ """
55
+ This class is used to define the interface of the authentication response.
56
+ """
57
+ pass
58
+ ```
59
+
60
+ - If the response is handled as dict in handler, don't have to implement this.
61
+
62
+ ### AuthHandlerInterface
63
+
64
+ An interface for the object that performs the actual authentication process.
65
+
66
+ ```python
67
+ class AuthHandlerInterface(ABC):
68
+ name: str = Field(description="name of the authentication handler")
69
+ description: str = Field(description="description of the authentication handler")
70
+ scoped: bool = Field(description="Indicates whether the handler requires an auth_scope for access control")
71
+
72
+ @staticmethod
73
+ def provider() -> AuthProvider:
74
+ """
75
+ Returns the authentication provider enum.
76
+
77
+ This method is used to determine the appropriate authentication handler
78
+ based on the authentication provider.
79
+ """
80
+ raise NotImplementedError()
81
+
82
+ @staticmethod
83
+ def provider_default() -> bool:
84
+ """
85
+ Indicates whether this authentication handler is the default handler.
86
+
87
+ If no specific handler is designated, the default handler will be used.
88
+
89
+ Returns:
90
+ bool: True if this handler is the default, False otherwise.
91
+ """
92
+ return False
93
+
94
+ @staticmethod
95
+ def recommended_scopes() -> set[str]:
96
+ """
97
+ Returns the recommended authentication scopes.
98
+
99
+ If `use_recommended_scope` is set to True in the `AuthConfig`,
100
+ this method should return the proper recommended scopes. Otherwise,
101
+ it should return an empty set.
102
+
103
+ Returns:
104
+ set[str]: A set of recommended scopes, or an empty set if not applicable.
105
+
106
+ Examples:
107
+ Slack OAuth2 recommended_scopes::
108
+
109
+ def recommended_scopes() -> set[str]:
110
+ if config.auth.slack.use_recommended_scope:
111
+ recommended_scopes = {
112
+ "channels:history",
113
+ "channels:read",
114
+ "chat:write",
115
+ "groups:history",
116
+ "groups:read",
117
+ "im:history",
118
+ "mpim:history",
119
+ "reactions:read",
120
+ "reactions:write",
121
+ }
122
+ else:
123
+ recommended_scopes = {}
124
+ return recommended_scopes
125
+ """
126
+ raise NotImplementedError()
127
+
128
+ def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> AuthenticateRequest:
129
+ """
130
+ Make an AuthenticationRequest.
131
+
132
+ Usually, this method only requires `auth_scopes`.
133
+ If additional static information is needed (e.g., clientID, secretID),
134
+ retrieve it from the configuration.
135
+
136
+ Args:
137
+ auth_scopes (Optional[list[str]]): list of auth scopes
138
+
139
+ Returns:
140
+ AuthenticateRequest: A authentication request object with the necessary details.
141
+
142
+ Examples:
143
+ Create a Slack OAuth2 request::
144
+
145
+ def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> SlackOAuth2Request:
146
+ return SlackOAuth2Request(
147
+ auth_scopes=auth_scopes,
148
+ client_id=config.auth.slack.client_id,
149
+ client_secret=config.auth.slack.client_secret
150
+ )
151
+ """
152
+ raise NotImplementedError()
153
+
154
+ def prepare(self, auth_req: AuthenticateRequest, thread_id: str, profile: str,
155
+ future_uid: str, *args, **kwargs) -> str:
156
+ """
157
+ Performs preliminary tasks required for authentication.
158
+
159
+ This method typically performs the following actions:
160
+ - Creates a future to wait for user authentication completion during the authentication process.
161
+ - Issues an authentication URI that the user can access.
162
+
163
+ Args:
164
+ auth_req (AuthenticateRequest): The authentication request object.
165
+ thread_id (str): The thread ID.
166
+ profile (str): The profile name.
167
+ future_uid (str): A unique identifier for each future.
168
+
169
+ Returns:
170
+ str: The authentication URI that the user can access.
171
+ """
172
+ raise NotImplementedError()
173
+
174
+ @abstractmethod
175
+ async def authenticate(self, auth_req: AuthenticateRequest, future_uid: str, *args, **kwargs) -> AuthContext:
176
+ """
177
+ Performs the actual authentication process.
178
+
179
+ This function assumes that the user has completed the authentication during the `prepare` step,
180
+ and the associated future has been resolved. At this point, the result contains the required
181
+ values for authentication (e.g., an auth code).
182
+
183
+ Typically, this process involves:
184
+ - Accessing the resolved future to retrieve the necessary values for authentication.
185
+ - Performing the actual authentication using these values.
186
+ - Converting the returned response into an appropriate `AuthContext` object and returning it.
187
+
188
+ Args:
189
+ auth_req (AuthenticateRequest): The authentication request object.
190
+ future_uid (str): A unique identifier for the future, used to retrieve the correct
191
+ result issued during the `prepare` step.
192
+
193
+ Returns:
194
+ AuthContext: The authentication context object containing the authentication result.
195
+ """
196
+ raise NotImplementedError()
197
+
198
+ @abstractmethod
199
+ async def refresh(self, auth_req: AuthenticateRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
200
+ """
201
+ Performs re-authentication for an expired session.
202
+
203
+ This method is optional and does not need to be implemented for handlers that do not require re-authentication.
204
+
205
+ Typically, the information needed for re-authentication (e.g., a refresh token) should be stored
206
+ within the `AuthContext` during the previous authentication step.
207
+ In the `refresh` step, this method accesses the necessary re-authentication details from the provided `context`,
208
+ performs the re-authentication, and returns an updated `AuthContext`.
209
+
210
+ Args:
211
+ auth_req (AuthenticateRequest): The authentication request object.
212
+ context (AuthContext): The current authentication context that it should contain data required for re-authentication.
213
+
214
+ Returns:
215
+ AuthContext: An updated authentication context object.
216
+ """
217
+ raise NotImplementedError()
218
+ ```
219
+
220
+ ---
221
+
222
+ ## How To Implement Auth
223
+
224
+ 1. Implement a class that inherits `AuthenticateRequest`
225
+ - This class should store the necessary information for the prepare and authenticate steps.
226
+
227
+ 2. Implement a class that inherits `AuthContext`
228
+ - This Context should include:
229
+ - The access key that tools can access.
230
+ - A function to convert a response into an `AuthContext`
231
+
232
+ 3. Optionally, implement a `Response` class
233
+ - This class can parse the response data received after completing authentication.
234
+
235
+ 4. Implement a class that inherits `AuthHandlerInterface`
236
+ - This class perform the actual prepare, authenticate, and refresh steps.
237
+
238
+ 5. Add a new enum value to the `AuthProvider` Enum(only implement new auth provider)
239
+ - If a new `AuthProvider` is added, update the `AuthProvider` enum in `pocket/auth/provider.py`
240
+
241
+ 6. Add new auth callback server endpoint
242
+ - Add the endpoint under `pocket/server/auth/` packages.
243
+ - Declare an appropriate `APIRouter` in the package. Pocket will automatically register the endpoint during
244
+ initialization.
245
+
246
+ 7. Add test code(optional)
247
+ - Add the test code under `pocket/auth/tests/` packages.
248
+
249
+ ---
250
+
251
+ ## Auth Flow(Advanced)
252
+
253
+ ### Session States
254
+
255
+ - SKIP_AUTH: The authentication exists, and the request has the necessary permissions.
256
+
257
+ - DO_AUTH: Authentication exists, but the request requires additional permissions.
258
+
259
+ - DO_REFRESH: Authentication exists but has expired, requiring a refresh.
260
+
261
+ - NO_SESSION: No authentication exists.
262
+
263
+ - PENDING_RESOLVE: Waiting for the user to complete the authentication process via the authorization URI.
264
+
265
+ - RESOLVED: The user has completed authentication, and the server has received the authorization code.
266
+
267
+ ### 01. check
268
+
269
+ Determines the current session state when a request is received.
270
+
271
+ 1. Check if a session exists.
272
+ - If not, return NO_SESSION.
273
+
274
+ 2. Check for an auth resolve uid.
275
+ - If it exists but authentication isn’t complete, return PENDING_RESOLVE.
276
+ - If completed, return RESOLVED.
277
+
278
+ 3. Check if the session is valid for the request:
279
+ - Compare the auth provider.
280
+ - Validate scopes (for scoped providers).
281
+ - If the session doesn’t satisfy requirements, return DO_AUTH.
282
+
283
+ 4. Check if the session has expired.
284
+ - If expired, return DO_REFRESH.
285
+
286
+ 5. If all checks pass, return SKIP_AUTH.
287
+
288
+ ### 02. prepare
289
+
290
+ 1. If the auth state is SKIP_AUTH, DO_REFRESH, or RESOLVED, do nothing.
291
+
292
+ 2. If the auth state is DO_AUTH or NO_SESSION:
293
+ - Create a new session and future.
294
+ - Return the authorization URI.
295
+
296
+ 3. If the state is PENDING_RESOLVE, return the existing authorization URI.
297
+
298
+ ```mermaid
299
+ flowchart TD
300
+ Check["Prepare Auth"] --> A["Is AuthState<br>SKIP_AUTH, DO_REFRESH or RESOLVED?"]
301
+ A -->|Yes| B["Return<br>None"]
302
+ A -->|No| C["Is AuthState PENDING_RESOLVED?"]
303
+ C -->|YES| D["Return<br>Previous Authorization URI"]
304
+ C -->|NO| E["Make New Session"]
305
+ E --> F["Return<br>Authorization URI"]
306
+ ```
307
+
308
+ ### 03. Authenticate
309
+
310
+ 1. If the auth state is SKIP_AUTH, return the existing session.
311
+ 2. If the state is DO_REFRESH, refresh the session.
312
+ 3. If the state is PENDING_RESOLVE or RESOLVED, perform authentication using the authorization code.
313
+ 4. If the state is NO_SESSION or DO_AUTH, this step should not be reached.
314
+
315
+ ```mermaid
316
+ flowchart TD
317
+ Check["Authenticate"]
318
+ Check -->|Is AuthState<br>SKIP_AUTH?| A2["Return<br>Existing Session"]
319
+ Check -->|Is AuthState<br>DO_REFRESH?| B2["Refresh<br>Access Token"]
320
+ Check -->|Is AuthState PENDING_RESOLVE<br>or<br>RESOLVED?| C2["Wait for Code<br>And<br>Authenticate"]
321
+ Check -->|Is AuthState<br>NO_SESION<br>or<br>DO_AUTH?| D2["Can't be reached<br>while in state"]
322
+ ```
323
+
@@ -0,0 +1,24 @@
1
+ from hyperpocket.auth.context import AuthContext
2
+ from hyperpocket.auth.github.context import GitHubAuthContext
3
+ from hyperpocket.auth.github.oauth2_context import GitHubOAuth2AuthContext
4
+ from hyperpocket.auth.github.token_context import GitHubTokenAuthContext
5
+ from hyperpocket.auth.google.context import GoogleAuthContext
6
+ from hyperpocket.auth.google.oauth2_context import GoogleOAuth2AuthContext
7
+ from hyperpocket.auth.handler import AuthHandlerInterface
8
+ from hyperpocket.auth.linear.token_context import LinearTokenAuthContext
9
+ from hyperpocket.auth.provider import AuthProvider
10
+ from hyperpocket.auth.slack.oauth2_context import SlackOAuth2AuthContext
11
+ from hyperpocket.auth.slack.token_context import SlackTokenAuthContext
12
+ from hyperpocket.util.find_all_leaf_class_in_package import find_all_leaf_class_in_package
13
+
14
+ PREBUILT_AUTH_HANDLERS = find_all_leaf_class_in_package("hyperpocket.auth", AuthHandlerInterface)
15
+ AUTH_CONTEXT_MAP = {
16
+ leaf.__name__: leaf for leaf in find_all_leaf_class_in_package("hyperpocket.auth", AuthContext)
17
+ }
18
+
19
+ __all__ = [
20
+ 'PREBUILT_AUTH_HANDLERS',
21
+ 'AUTH_CONTEXT_MAP',
22
+ 'AuthProvider',
23
+ 'AuthHandlerInterface',
24
+ ]
File without changes
@@ -0,0 +1,13 @@
1
+ from hyperpocket.auth.context import AuthContext
2
+
3
+
4
+ class CalendlyAuthContext(AuthContext):
5
+ _ACCESS_TOKEN_KEY: str = "CALENDLY_TOKEN"
6
+
7
+ def to_dict(self) -> dict[str, str]:
8
+ return {self._ACCESS_TOKEN_KEY: self.access_token}
9
+
10
+ def to_profiled_dict(self, profile: str) -> dict[str, str]:
11
+ return {
12
+ f"{profile.upper()}_{self._ACCESS_TOKEN_KEY}": self.access_token,
13
+ }