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.
- hyperpocket/__init__.py +7 -0
- hyperpocket/auth/README.KR.md +309 -0
- hyperpocket/auth/README.md +323 -0
- hyperpocket/auth/__init__.py +24 -0
- hyperpocket/auth/calendly/__init__.py +0 -0
- hyperpocket/auth/calendly/context.py +13 -0
- hyperpocket/auth/calendly/oauth2_context.py +25 -0
- hyperpocket/auth/calendly/oauth2_handler.py +146 -0
- hyperpocket/auth/calendly/oauth2_schema.py +16 -0
- hyperpocket/auth/context.py +38 -0
- hyperpocket/auth/github/__init__.py +0 -0
- hyperpocket/auth/github/context.py +13 -0
- hyperpocket/auth/github/oauth2_context.py +25 -0
- hyperpocket/auth/github/oauth2_handler.py +143 -0
- hyperpocket/auth/github/oauth2_schema.py +16 -0
- hyperpocket/auth/github/token_context.py +12 -0
- hyperpocket/auth/github/token_handler.py +79 -0
- hyperpocket/auth/github/token_schema.py +9 -0
- hyperpocket/auth/google/__init__.py +0 -0
- hyperpocket/auth/google/context.py +15 -0
- hyperpocket/auth/google/oauth2_context.py +31 -0
- hyperpocket/auth/google/oauth2_handler.py +137 -0
- hyperpocket/auth/google/oauth2_schema.py +18 -0
- hyperpocket/auth/handler.py +171 -0
- hyperpocket/auth/linear/__init__.py +0 -0
- hyperpocket/auth/linear/context.py +15 -0
- hyperpocket/auth/linear/token_context.py +15 -0
- hyperpocket/auth/linear/token_handler.py +68 -0
- hyperpocket/auth/linear/token_schema.py +9 -0
- hyperpocket/auth/provider.py +16 -0
- hyperpocket/auth/schema.py +19 -0
- hyperpocket/auth/slack/__init__.py +0 -0
- hyperpocket/auth/slack/context.py +15 -0
- hyperpocket/auth/slack/oauth2_context.py +40 -0
- hyperpocket/auth/slack/oauth2_handler.py +151 -0
- hyperpocket/auth/slack/oauth2_schema.py +40 -0
- hyperpocket/auth/slack/tests/__init__.py +0 -0
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
- hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
- hyperpocket/auth/slack/token_context.py +14 -0
- hyperpocket/auth/slack/token_handler.py +64 -0
- hyperpocket/auth/slack/token_schema.py +9 -0
- hyperpocket/auth/tests/__init__.py +0 -0
- hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
- hyperpocket/cli/__init__.py +0 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/pull.py +18 -0
- hyperpocket/cli/sync.py +17 -0
- hyperpocket/config/__init__.py +9 -0
- hyperpocket/config/auth.py +36 -0
- hyperpocket/config/git.py +17 -0
- hyperpocket/config/logger.py +81 -0
- hyperpocket/config/session.py +35 -0
- hyperpocket/config/settings.py +62 -0
- hyperpocket/constants.py +0 -0
- hyperpocket/curated_tools.py +10 -0
- hyperpocket/external/__init__.py +7 -0
- hyperpocket/external/github_client.py +19 -0
- hyperpocket/futures/__init__.py +7 -0
- hyperpocket/futures/futurestore.py +48 -0
- hyperpocket/pocket_auth.py +344 -0
- hyperpocket/pocket_main.py +351 -0
- hyperpocket/prompts.py +15 -0
- hyperpocket/repository/__init__.py +5 -0
- hyperpocket/repository/lock.py +156 -0
- hyperpocket/repository/lockfile.py +56 -0
- hyperpocket/repository/repository.py +18 -0
- hyperpocket/server/__init__.py +3 -0
- hyperpocket/server/auth/__init__.py +15 -0
- hyperpocket/server/auth/calendly.py +16 -0
- hyperpocket/server/auth/github.py +25 -0
- hyperpocket/server/auth/google.py +16 -0
- hyperpocket/server/auth/linear.py +18 -0
- hyperpocket/server/auth/slack.py +28 -0
- hyperpocket/server/auth/token.py +51 -0
- hyperpocket/server/proxy.py +63 -0
- hyperpocket/server/server.py +178 -0
- hyperpocket/server/tool/__init__.py +10 -0
- hyperpocket/server/tool/dto/__init__.py +0 -0
- hyperpocket/server/tool/dto/script.py +15 -0
- hyperpocket/server/tool/wasm.py +31 -0
- hyperpocket/session/README.KR.md +62 -0
- hyperpocket/session/README.md +61 -0
- hyperpocket/session/__init__.py +4 -0
- hyperpocket/session/in_memory.py +76 -0
- hyperpocket/session/interface.py +118 -0
- hyperpocket/session/redis.py +126 -0
- hyperpocket/session/tests/__init__.py +0 -0
- hyperpocket/session/tests/test_in_memory.py +145 -0
- hyperpocket/session/tests/test_redis.py +151 -0
- hyperpocket/tests/__init__.py +0 -0
- hyperpocket/tests/test_pocket.py +118 -0
- hyperpocket/tests/test_pocket_auth.py +982 -0
- hyperpocket/tool/README.KR.md +68 -0
- hyperpocket/tool/README.md +75 -0
- hyperpocket/tool/__init__.py +13 -0
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +18 -0
- hyperpocket/tool/function/README.KR.md +159 -0
- hyperpocket/tool/function/README.md +169 -0
- hyperpocket/tool/function/__init__.py +9 -0
- hyperpocket/tool/function/annotation.py +30 -0
- hyperpocket/tool/function/tool.py +87 -0
- hyperpocket/tool/tests/__init__.py +0 -0
- hyperpocket/tool/tests/test_function_tool.py +266 -0
- hyperpocket/tool/tool.py +106 -0
- hyperpocket/tool/wasm/README.KR.md +144 -0
- hyperpocket/tool/wasm/README.md +144 -0
- hyperpocket/tool/wasm/__init__.py +3 -0
- hyperpocket/tool/wasm/browser.py +63 -0
- hyperpocket/tool/wasm/invoker.py +41 -0
- hyperpocket/tool/wasm/script.py +82 -0
- hyperpocket/tool/wasm/templates/__init__.py +28 -0
- hyperpocket/tool/wasm/templates/node.py +87 -0
- hyperpocket/tool/wasm/templates/python.py +75 -0
- hyperpocket/tool/wasm/tool.py +147 -0
- hyperpocket/util/__init__.py +1 -0
- hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
- hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
- hyperpocket/util/find_all_subclass_in_package.py +29 -0
- hyperpocket/util/flatten_json_schema.py +45 -0
- hyperpocket/util/function_to_model.py +46 -0
- hyperpocket/util/get_objects_from_subpackage.py +28 -0
- hyperpocket/util/json_schema_to_model.py +69 -0
- hyperpocket-0.0.1.dist-info/METADATA +304 -0
- hyperpocket-0.0.1.dist-info/RECORD +131 -0
- hyperpocket-0.0.1.dist-info/WHEEL +4 -0
- hyperpocket-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
2
|
+
|
3
|
+
from hyperpocket.auth.calendly.context import CalendlyAuthContext
|
4
|
+
from hyperpocket.auth.calendly.oauth2_schema import CalendlyOAuth2Response
|
5
|
+
|
6
|
+
|
7
|
+
class CalendlyOAuth2AuthContext(CalendlyAuthContext):
|
8
|
+
@classmethod
|
9
|
+
def from_calendly_oauth2_response(
|
10
|
+
cls, response: CalendlyOAuth2Response
|
11
|
+
) -> "CalendlyOAuth2AuthContext":
|
12
|
+
description = f"Calendly OAuth2 Context logged in with {response.scope} scopes"
|
13
|
+
now = datetime.now(tz=timezone.utc)
|
14
|
+
|
15
|
+
if response.expires_in:
|
16
|
+
expires_at = now + timedelta(seconds=response.expires_in)
|
17
|
+
else:
|
18
|
+
expires_at = None
|
19
|
+
|
20
|
+
return cls(
|
21
|
+
access_token=response.access_token,
|
22
|
+
description=description,
|
23
|
+
expires_at=expires_at,
|
24
|
+
detail=response,
|
25
|
+
)
|
@@ -0,0 +1,146 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
from urllib.parse import urlencode, urljoin
|
3
|
+
|
4
|
+
import httpx
|
5
|
+
|
6
|
+
from hyperpocket.auth.calendly.oauth2_context import CalendlyOAuth2AuthContext
|
7
|
+
from hyperpocket.auth.calendly.oauth2_schema import CalendlyOAuth2Request, CalendlyOAuth2Response
|
8
|
+
from hyperpocket.auth.context import AuthContext
|
9
|
+
from hyperpocket.auth.handler import AuthHandlerInterface, AuthProvider
|
10
|
+
from hyperpocket.config import config
|
11
|
+
from hyperpocket.futures import FutureStore
|
12
|
+
|
13
|
+
|
14
|
+
class CalendlyOAuth2AuthHandler(AuthHandlerInterface):
|
15
|
+
_CALENDLY_AUTH_URL = "https://auth.calendly.com/oauth/authorize"
|
16
|
+
_CALENDLY_TOKEN_URL = "https://auth.calendly.com/oauth/token"
|
17
|
+
|
18
|
+
name: str = "calendly-oauth2"
|
19
|
+
description: str = "This handler is used to authenticate users using Calendly OAuth."
|
20
|
+
scoped: bool = False
|
21
|
+
|
22
|
+
@staticmethod
|
23
|
+
def provider() -> AuthProvider:
|
24
|
+
return AuthProvider.CALENDLY
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def provider_default() -> bool:
|
28
|
+
return True
|
29
|
+
|
30
|
+
@staticmethod
|
31
|
+
def recommended_scopes() -> set[str]:
|
32
|
+
return set()
|
33
|
+
|
34
|
+
def prepare(
|
35
|
+
self,
|
36
|
+
auth_req: CalendlyOAuth2Request,
|
37
|
+
thread_id: str,
|
38
|
+
profile: str,
|
39
|
+
future_uid: str,
|
40
|
+
*args,
|
41
|
+
**kwargs,
|
42
|
+
) -> str:
|
43
|
+
redirect_uri = urljoin(
|
44
|
+
config.public_base_url + "/",
|
45
|
+
f"{config.callback_url_rewrite_prefix}/auth/calendly/oauth2/callback",
|
46
|
+
)
|
47
|
+
auth_url = self._make_auth_url(auth_req, redirect_uri, future_uid)
|
48
|
+
|
49
|
+
FutureStore.create_future(
|
50
|
+
future_uid,
|
51
|
+
data={
|
52
|
+
"redirect_uri": redirect_uri,
|
53
|
+
"thread_id": thread_id,
|
54
|
+
"profile": profile,
|
55
|
+
},
|
56
|
+
)
|
57
|
+
|
58
|
+
return f"User needs to authenticate using the following URL: {auth_url}"
|
59
|
+
|
60
|
+
async def authenticate(
|
61
|
+
self, auth_req: CalendlyOAuth2Request, future_uid: str, *args, **kwargs
|
62
|
+
) -> AuthContext:
|
63
|
+
future_data = FutureStore.get_future(future_uid)
|
64
|
+
auth_code = await future_data.future
|
65
|
+
|
66
|
+
async with httpx.AsyncClient() as client:
|
67
|
+
resp = await client.post(
|
68
|
+
url=self._CALENDLY_TOKEN_URL,
|
69
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
70
|
+
auth=(auth_req.client_id, auth_req.client_secret),
|
71
|
+
data={
|
72
|
+
"code": auth_code,
|
73
|
+
"client_id": auth_req.client_id,
|
74
|
+
"client_secret": auth_req.client_secret,
|
75
|
+
"redirect_uri": future_data.data["redirect_uri"],
|
76
|
+
"grant_type": "authorization_code",
|
77
|
+
},
|
78
|
+
)
|
79
|
+
|
80
|
+
if resp.status_code != 200:
|
81
|
+
raise Exception(f"failed to authenticate. status_code : {resp.status_code}")
|
82
|
+
result = resp.json()
|
83
|
+
|
84
|
+
auth_response = CalendlyOAuth2Response(
|
85
|
+
access_token=result["access_token"],
|
86
|
+
token_type=result["token_type"],
|
87
|
+
scope=result["scope"],
|
88
|
+
refresh_token=(
|
89
|
+
result["refresh_token"] if "refresh_token" in result else None
|
90
|
+
),
|
91
|
+
expires_in=(
|
92
|
+
result["refresh_token_expires_in"]
|
93
|
+
if "refresh_token_expires_in" in result
|
94
|
+
else None
|
95
|
+
),
|
96
|
+
)
|
97
|
+
return CalendlyOAuth2AuthContext.from_calendly_oauth2_response(auth_response)
|
98
|
+
|
99
|
+
async def refresh(
|
100
|
+
self, auth_req: CalendlyOAuth2Request, context: AuthContext, *args, **kwargs
|
101
|
+
) -> AuthContext:
|
102
|
+
last_oauth2_resp: CalendlyOAuth2Response = context.detail
|
103
|
+
refresh_token = last_oauth2_resp.refresh_token
|
104
|
+
if refresh_token is None:
|
105
|
+
raise Exception(
|
106
|
+
f"refresh token is None. last_oauth2_resp: {last_oauth2_resp}"
|
107
|
+
)
|
108
|
+
|
109
|
+
async with httpx.AsyncClient() as client:
|
110
|
+
resp = await client.post(
|
111
|
+
url=self._CALENDLY_TOKEN_URL,
|
112
|
+
data={
|
113
|
+
"client_id": auth_req.client_id,
|
114
|
+
"client_secret": auth_req.client_secret,
|
115
|
+
"refresh_token": refresh_token,
|
116
|
+
"grant_type": "refresh_token",
|
117
|
+
},
|
118
|
+
)
|
119
|
+
|
120
|
+
if resp.status_code != 200:
|
121
|
+
raise Exception(
|
122
|
+
f"failed to authenticate. status_code : {resp.status_code}"
|
123
|
+
)
|
124
|
+
|
125
|
+
resp_json = resp.json()
|
126
|
+
resp_json["refresh_token"] = refresh_token
|
127
|
+
response = CalendlyOAuth2Response(**resp_json)
|
128
|
+
return CalendlyOAuth2AuthContext.from_calendly_oauth2_response(response)
|
129
|
+
|
130
|
+
def _make_auth_url(self, auth_req: CalendlyOAuth2Request, redirect_uri: str, state: str):
|
131
|
+
params = {
|
132
|
+
"client_id": auth_req.client_id,
|
133
|
+
"redirect_uri": redirect_uri,
|
134
|
+
"response_type": "code",
|
135
|
+
"state": state,
|
136
|
+
}
|
137
|
+
return f"{self._CALENDLY_AUTH_URL}?{urlencode(params)}"
|
138
|
+
|
139
|
+
def make_request(
|
140
|
+
self, auth_scopes: Optional[list[str]] = None, **kwargs
|
141
|
+
) -> CalendlyOAuth2Request:
|
142
|
+
return CalendlyOAuth2Request(
|
143
|
+
auth_scopes=auth_scopes,
|
144
|
+
client_id=config.auth.calendly.client_id,
|
145
|
+
client_secret=config.auth.calendly.client_secret,
|
146
|
+
)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
from hyperpocket.auth.handler import AuthenticateRequest
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
|
6
|
+
class CalendlyOAuth2Request(AuthenticateRequest):
|
7
|
+
client_id: str
|
8
|
+
client_secret: str
|
9
|
+
|
10
|
+
|
11
|
+
class CalendlyOAuth2Response(BaseModel):
|
12
|
+
access_token: str
|
13
|
+
expires_in: Optional[int]
|
14
|
+
refresh_token: Optional[str]
|
15
|
+
scope: str
|
16
|
+
token_type: str
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from datetime import datetime
|
3
|
+
from typing import Optional, Any
|
4
|
+
|
5
|
+
from pydantic import BaseModel, Field
|
6
|
+
|
7
|
+
|
8
|
+
class AuthContext(BaseModel, ABC):
|
9
|
+
"""
|
10
|
+
This class is used to define the interface of the authentication model.
|
11
|
+
"""
|
12
|
+
access_token: str = Field(description="user's access token")
|
13
|
+
description: str = Field(description="description of this authentication context")
|
14
|
+
expires_at: Optional[datetime] = Field(description="expiration datetime")
|
15
|
+
detail: Optional[Any] = Field(default=None, description="detailed information")
|
16
|
+
|
17
|
+
@abstractmethod
|
18
|
+
def to_dict(self) -> dict[str, str]:
|
19
|
+
"""
|
20
|
+
This method is used to convert the authentication context to a dictionary.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
dict[str, str]: The dictionary representation of the authentication context.
|
24
|
+
"""
|
25
|
+
raise NotImplementedError
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
def to_profiled_dict(self, profile: str) -> dict[str, str]:
|
29
|
+
"""
|
30
|
+
This method is used to convert the authentication context to a profiled dictionary.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
profile (str): The profile name.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
dict[str, str]: The profiled dictionary representation of the authentication context.
|
37
|
+
"""
|
38
|
+
raise NotImplementedError
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from hyperpocket.auth.context import AuthContext
|
2
|
+
|
3
|
+
|
4
|
+
class GitHubAuthContext(AuthContext):
|
5
|
+
_ACCESS_TOKEN_KEY: str = "GITHUB_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
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
2
|
+
|
3
|
+
from hyperpocket.auth.github.context import GitHubAuthContext
|
4
|
+
from hyperpocket.auth.github.oauth2_schema import GitHubOAuth2Response
|
5
|
+
|
6
|
+
|
7
|
+
class GitHubOAuth2AuthContext(GitHubAuthContext):
|
8
|
+
@classmethod
|
9
|
+
def from_github_oauth2_response(
|
10
|
+
cls, response: GitHubOAuth2Response
|
11
|
+
) -> "GitHubOAuth2AuthContext":
|
12
|
+
description = f"Github OAuth2 Context logged in with {response.scope} scopes"
|
13
|
+
now = datetime.now(tz=timezone.utc)
|
14
|
+
|
15
|
+
if response.expires_in:
|
16
|
+
expires_at = now + timedelta(seconds=response.expires_in)
|
17
|
+
else:
|
18
|
+
expires_at = None
|
19
|
+
|
20
|
+
return cls(
|
21
|
+
access_token=response.access_token,
|
22
|
+
description=description,
|
23
|
+
expires_at=expires_at,
|
24
|
+
detail=response,
|
25
|
+
)
|
@@ -0,0 +1,143 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
from urllib.parse import parse_qs, urlencode, urljoin
|
3
|
+
|
4
|
+
import httpx
|
5
|
+
|
6
|
+
from hyperpocket.auth.context import AuthContext
|
7
|
+
from hyperpocket.auth.github.oauth2_context import GitHubOAuth2AuthContext
|
8
|
+
from hyperpocket.auth.github.oauth2_schema import GitHubOAuth2Request, GitHubOAuth2Response
|
9
|
+
from hyperpocket.auth.handler import AuthHandlerInterface, AuthProvider
|
10
|
+
from hyperpocket.config import config
|
11
|
+
from hyperpocket.futures import FutureStore
|
12
|
+
|
13
|
+
|
14
|
+
class GitHubOAuth2AuthHandler(AuthHandlerInterface):
|
15
|
+
_GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize"
|
16
|
+
_GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
17
|
+
|
18
|
+
name: str = "github-oauth2"
|
19
|
+
description: str = "This handler is used to authenticate users using Github OAuth."
|
20
|
+
scoped: bool = True
|
21
|
+
|
22
|
+
@staticmethod
|
23
|
+
def provider() -> AuthProvider:
|
24
|
+
return AuthProvider.GITHUB
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def provider_default() -> bool:
|
28
|
+
return True
|
29
|
+
|
30
|
+
@staticmethod
|
31
|
+
def recommended_scopes() -> set[str]:
|
32
|
+
return {"repo"}
|
33
|
+
|
34
|
+
def prepare(
|
35
|
+
self,
|
36
|
+
auth_req: GitHubOAuth2Request,
|
37
|
+
thread_id: str,
|
38
|
+
profile: str,
|
39
|
+
future_uid: str,
|
40
|
+
*args,
|
41
|
+
**kwargs,
|
42
|
+
) -> str:
|
43
|
+
redirect_uri = urljoin(
|
44
|
+
config.public_base_url + "/",
|
45
|
+
f"{config.callback_url_rewrite_prefix}/auth/github/oauth2/callback",
|
46
|
+
)
|
47
|
+
auth_url = self._make_auth_url(auth_req, redirect_uri, future_uid)
|
48
|
+
|
49
|
+
FutureStore.create_future(
|
50
|
+
future_uid,
|
51
|
+
data={
|
52
|
+
"redirect_uri": redirect_uri,
|
53
|
+
"thread_id": thread_id,
|
54
|
+
"profile": profile,
|
55
|
+
},
|
56
|
+
)
|
57
|
+
|
58
|
+
return f"User needs to authenticate using the following URL: {auth_url}"
|
59
|
+
|
60
|
+
async def authenticate(
|
61
|
+
self, auth_req: GitHubOAuth2Request, future_uid: str, *args, **kwargs
|
62
|
+
) -> AuthContext:
|
63
|
+
future_data = FutureStore.get_future( future_uid)
|
64
|
+
auth_code = await future_data.future
|
65
|
+
|
66
|
+
async with httpx.AsyncClient() as client:
|
67
|
+
resp = await client.post(
|
68
|
+
url=self._GITHUB_TOKEN_URL,
|
69
|
+
data={
|
70
|
+
"code": auth_code,
|
71
|
+
"client_id": auth_req.client_id,
|
72
|
+
"client_secret": auth_req.client_secret,
|
73
|
+
"redirect_uri": future_data.data["redirect_uri"],
|
74
|
+
},
|
75
|
+
)
|
76
|
+
|
77
|
+
if resp.status_code != 200:
|
78
|
+
raise Exception(f"failed to authenticate. status_code : {resp.status_code}")
|
79
|
+
|
80
|
+
result = parse_qs(resp.text)
|
81
|
+
auth_response = GitHubOAuth2Response(
|
82
|
+
access_token=result["access_token"][0],
|
83
|
+
token_type=result["token_type"][0],
|
84
|
+
scope=result["scope"][0],
|
85
|
+
refresh_token=(
|
86
|
+
result["refresh_token"][0] if "refresh_token" in result else None
|
87
|
+
),
|
88
|
+
expires_in=(
|
89
|
+
result["refresh_token_expires_in"][0]
|
90
|
+
if "refresh_token_expires_in" in result
|
91
|
+
else None
|
92
|
+
),
|
93
|
+
)
|
94
|
+
return GitHubOAuth2AuthContext.from_github_oauth2_response(auth_response)
|
95
|
+
|
96
|
+
async def refresh(
|
97
|
+
self, auth_req: GitHubOAuth2Request, context: AuthContext, *args, **kwargs
|
98
|
+
) -> AuthContext:
|
99
|
+
last_oauth2_resp: GitHubOAuth2Response = context.detail
|
100
|
+
refresh_token = last_oauth2_resp.refresh_token
|
101
|
+
if refresh_token is None:
|
102
|
+
raise Exception(
|
103
|
+
f"refresh token is None. last_oauth2_resp: {last_oauth2_resp}"
|
104
|
+
)
|
105
|
+
|
106
|
+
async with httpx.AsyncClient() as client:
|
107
|
+
resp = await client.post(
|
108
|
+
url=self._GITHUB_TOKEN_URL,
|
109
|
+
data={
|
110
|
+
"client_id": auth_req.client_id,
|
111
|
+
"client_secret": auth_req.client_secret,
|
112
|
+
"refresh_token": refresh_token,
|
113
|
+
"grant_type": "refresh_token",
|
114
|
+
},
|
115
|
+
)
|
116
|
+
|
117
|
+
if resp.status_code != 200:
|
118
|
+
raise Exception(
|
119
|
+
f"failed to authenticate. status_code : {resp.status_code}"
|
120
|
+
)
|
121
|
+
|
122
|
+
resp_json = resp.json()
|
123
|
+
resp_json["refresh_token"] = refresh_token
|
124
|
+
response = GitHubOAuth2Response(**resp_json)
|
125
|
+
return GitHubOAuth2AuthContext.from_github_oauth2_response(response)
|
126
|
+
|
127
|
+
def _make_auth_url(self, auth_req: GitHubOAuth2Request, redirect_uri: str, state: str):
|
128
|
+
params = {
|
129
|
+
"client_id": auth_req.client_id,
|
130
|
+
"redirect_uri": redirect_uri,
|
131
|
+
"scope": ",".join(auth_req.auth_scopes),
|
132
|
+
"state": state,
|
133
|
+
}
|
134
|
+
return f"{self._GITHUB_AUTH_URL}?{urlencode(params)}"
|
135
|
+
|
136
|
+
def make_request(
|
137
|
+
self, auth_scopes: Optional[list[str]] = None, **kwargs
|
138
|
+
) -> GitHubOAuth2Request:
|
139
|
+
return GitHubOAuth2Request(
|
140
|
+
auth_scopes=auth_scopes,
|
141
|
+
client_id=config.auth.github.client_id,
|
142
|
+
client_secret=config.auth.github.client_secret,
|
143
|
+
)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from hyperpocket.auth.schema import AuthenticateRequest, AuthenticateResponse
|
4
|
+
|
5
|
+
|
6
|
+
class GitHubOAuth2Request(AuthenticateRequest):
|
7
|
+
client_id: str
|
8
|
+
client_secret: str
|
9
|
+
|
10
|
+
|
11
|
+
class GitHubOAuth2Response(AuthenticateResponse):
|
12
|
+
access_token: str
|
13
|
+
expires_in: Optional[int]
|
14
|
+
refresh_token: Optional[str]
|
15
|
+
scope: str
|
16
|
+
token_type: str
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from hyperpocket.auth.github.context import GitHubAuthContext
|
2
|
+
from hyperpocket.auth.github.token_schema import GitHubTokenResponse
|
3
|
+
|
4
|
+
|
5
|
+
class GitHubTokenAuthContext(GitHubAuthContext):
|
6
|
+
@classmethod
|
7
|
+
def from_github_token_response(cls, response: GitHubTokenResponse):
|
8
|
+
description = "GitHub Token Context logged in"
|
9
|
+
|
10
|
+
return cls(
|
11
|
+
access_token=response.access_token, description=description, expires_at=None
|
12
|
+
)
|
@@ -0,0 +1,79 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
from urllib.parse import urljoin, urlencode
|
3
|
+
|
4
|
+
from hyperpocket.auth import AuthProvider, AuthHandlerInterface
|
5
|
+
from hyperpocket.auth.context import AuthContext
|
6
|
+
from hyperpocket.auth.github.token_context import GitHubTokenAuthContext
|
7
|
+
from hyperpocket.auth.github.token_schema import GitHubTokenRequest, GitHubTokenResponse
|
8
|
+
from hyperpocket.auth.schema import AuthenticateRequest
|
9
|
+
from hyperpocket.config import config
|
10
|
+
from hyperpocket.futures import FutureStore
|
11
|
+
|
12
|
+
|
13
|
+
class GitHubTokenAuthHandler(AuthHandlerInterface):
|
14
|
+
name: str = "github-token"
|
15
|
+
description: str = (
|
16
|
+
"This handler is used to authenticate users using the GitHub token."
|
17
|
+
)
|
18
|
+
scoped: bool = False
|
19
|
+
|
20
|
+
_TOKEN_URL: str = urljoin(config.public_base_url + "/", f"{config.callback_url_rewrite_prefix}/auth/token")
|
21
|
+
|
22
|
+
@staticmethod
|
23
|
+
def provider() -> AuthProvider:
|
24
|
+
return AuthProvider.GITHUB
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def recommended_scopes() -> set[str]:
|
28
|
+
return set()
|
29
|
+
|
30
|
+
def prepare(
|
31
|
+
self,
|
32
|
+
auth_req: AuthenticateRequest,
|
33
|
+
thread_id: str,
|
34
|
+
profile: str,
|
35
|
+
future_uid: str,
|
36
|
+
*args,
|
37
|
+
**kwargs,
|
38
|
+
) -> str:
|
39
|
+
redirect_uri = urljoin(
|
40
|
+
config.public_base_url + "/",
|
41
|
+
f"{config.callback_url_rewrite_prefix}/auth/github/token/callback",
|
42
|
+
)
|
43
|
+
auth_url = self._make_auth_url(auth_req=auth_req, redirect_uri=redirect_uri, state=future_uid)
|
44
|
+
FutureStore.create_future(
|
45
|
+
future_uid,
|
46
|
+
data={
|
47
|
+
"redirect_uri": redirect_uri,
|
48
|
+
"thread_id": thread_id,
|
49
|
+
"profile": profile,
|
50
|
+
},
|
51
|
+
)
|
52
|
+
|
53
|
+
return f"User needs to authenticate using the following URL: {auth_url}"
|
54
|
+
|
55
|
+
async def authenticate(
|
56
|
+
self, auth_req: GitHubTokenRequest, future_uid: str, *args, **kwargs
|
57
|
+
) -> GitHubTokenAuthContext:
|
58
|
+
future_data = FutureStore.get_future(future_uid)
|
59
|
+
access_token = await future_data.future
|
60
|
+
|
61
|
+
response = GitHubTokenResponse(access_token=access_token)
|
62
|
+
context = GitHubTokenAuthContext.from_github_token_response(response)
|
63
|
+
|
64
|
+
return context
|
65
|
+
|
66
|
+
async def refresh(
|
67
|
+
self, auth_req: GitHubTokenRequest, context: AuthContext, *args, **kwargs
|
68
|
+
) -> AuthContext:
|
69
|
+
raise Exception("GitHub token doesn't support refresh")
|
70
|
+
|
71
|
+
def _make_auth_url(self, auth_req: GitHubTokenRequest, redirect_uri: str, state: str):
|
72
|
+
params = {
|
73
|
+
"redirect_uri": redirect_uri,
|
74
|
+
"state": state
|
75
|
+
}
|
76
|
+
return f"{self._TOKEN_URL}?{urlencode(params)}"
|
77
|
+
|
78
|
+
def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> GitHubTokenRequest:
|
79
|
+
return GitHubTokenRequest()
|
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from hyperpocket.auth.context import AuthContext
|
2
|
+
|
3
|
+
|
4
|
+
class GoogleAuthContext(AuthContext):
|
5
|
+
_ACCESS_TOKEN_KEY: str = "GOOGLE_TOKEN"
|
6
|
+
|
7
|
+
def to_dict(self) -> dict[str, str]:
|
8
|
+
return {
|
9
|
+
self._ACCESS_TOKEN_KEY: self.access_token
|
10
|
+
}
|
11
|
+
|
12
|
+
def to_profiled_dict(self, profile: str) -> dict[str, str]:
|
13
|
+
return {
|
14
|
+
f"{profile.upper()}_{self._ACCESS_TOKEN_KEY}": self.access_token,
|
15
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from pydantic import Field
|
5
|
+
|
6
|
+
from hyperpocket.auth.google.context import GoogleAuthContext
|
7
|
+
from hyperpocket.auth.google.oauth2_schema import GoogleOAuth2Response
|
8
|
+
|
9
|
+
|
10
|
+
class GoogleOAuth2AuthContext(GoogleAuthContext):
|
11
|
+
refresh_token: Optional[str] = Field(default=None, description="Refresh token")
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
def from_google_oauth2_response(
|
15
|
+
cls, response: GoogleOAuth2Response
|
16
|
+
) -> "GoogleOAuth2AuthContext":
|
17
|
+
description = f"Google OAuth2 Context logged in with {response.scope} scopes"
|
18
|
+
now = datetime.now(tz=timezone.utc)
|
19
|
+
|
20
|
+
if response.expires_in:
|
21
|
+
expires_at = now + timedelta(seconds=response.expires_in)
|
22
|
+
else:
|
23
|
+
expires_at = None
|
24
|
+
|
25
|
+
return cls(
|
26
|
+
access_token=response.access_token,
|
27
|
+
refresh_token=response.refresh_token,
|
28
|
+
description=description,
|
29
|
+
expires_at=expires_at,
|
30
|
+
detail=response,
|
31
|
+
)
|