jac-scale 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jac_scale/abstractions/config/app_config.jac +5 -2
- jac_scale/context.jac +2 -1
- jac_scale/factories/storage_factory.jac +75 -0
- jac_scale/google_sso_provider.jac +85 -0
- jac_scale/impl/context.impl.jac +3 -0
- jac_scale/impl/serve.impl.jac +82 -234
- jac_scale/impl/user_manager.impl.jac +349 -0
- jac_scale/memory_hierarchy.jac +3 -1
- jac_scale/plugin.jac +46 -3
- jac_scale/plugin_config.jac +27 -0
- jac_scale/serve.jac +3 -12
- jac_scale/sso_provider.jac +72 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +9 -15
- jac_scale/targets/kubernetes/kubernetes_target.jac +174 -15
- jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac +32 -0
- jac_scale/tests/fixtures/scale-feats/main.jac +147 -0
- jac_scale/tests/fixtures/test_api.jac +29 -0
- jac_scale/tests/fixtures/test_restspec.jac +37 -0
- jac_scale/tests/test_deploy_k8s.py +2 -1
- jac_scale/tests/test_examples.py +180 -5
- jac_scale/tests/test_hooks.py +39 -0
- jac_scale/tests/test_restspec.py +192 -0
- jac_scale/tests/test_serve.py +54 -0
- jac_scale/tests/test_sso.py +273 -284
- jac_scale/tests/test_storage.py +274 -0
- jac_scale/user_manager.jac +49 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/METADATA +9 -2
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/RECORD +31 -20
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/WHEEL +1 -1
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/entry_points.txt +0 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -7,7 +7,8 @@ class AppConfig {
|
|
|
7
7
|
file_name: str = 'none',
|
|
8
8
|
build: bool = False,
|
|
9
9
|
app_name: (str | None) = None,
|
|
10
|
-
testing: bool = False
|
|
10
|
+
testing: bool = False,
|
|
11
|
+
experimental: bool = False;
|
|
11
12
|
|
|
12
13
|
def init(
|
|
13
14
|
self: AppConfig,
|
|
@@ -15,13 +16,15 @@ class AppConfig {
|
|
|
15
16
|
file_name: str = 'none',
|
|
16
17
|
build: bool = False,
|
|
17
18
|
app_name: (str | None) = None,
|
|
18
|
-
testing: bool = False
|
|
19
|
+
testing: bool = False,
|
|
20
|
+
experimental: bool = False
|
|
19
21
|
) -> None {
|
|
20
22
|
self.code_folder = code_folder;
|
|
21
23
|
self.file_name = file_name;
|
|
22
24
|
self.build = build;
|
|
23
25
|
self.app_name = app_name;
|
|
24
26
|
self.testing = testing;
|
|
27
|
+
self.experimental = experimental;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
def get_code_path(self: AppConfig) -> Path {
|
jac_scale/context.jac
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import from contextvars { ContextVar }
|
|
1
2
|
import from dataclasses { MISSING }
|
|
2
3
|
import from typing { Any }
|
|
3
4
|
import from uuid { UUID }
|
|
4
5
|
import from jaclang.pycore.constant { Constants as Con }
|
|
5
|
-
import from jaclang.runtimelib.context { ExecutionContext }
|
|
6
|
+
import from jaclang.runtimelib.context { ExecutionContext, CallState }
|
|
6
7
|
|
|
7
8
|
"""Jac Scale Execution Context with custom memory backend.
|
|
8
9
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Factory for creating storage instances."""
|
|
2
|
+
import from typing { Any }
|
|
3
|
+
import os;
|
|
4
|
+
import from jaclang.runtimelib.storage { Storage }
|
|
5
|
+
import from jaclang.project.config { get_config }
|
|
6
|
+
|
|
7
|
+
enum StorageType {
|
|
8
|
+
LOCAL = "local"
|
|
9
|
+
# Future: S3, GCS, AZURE - add when implemented
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
"""Factory for creating storage instances.
|
|
13
|
+
|
|
14
|
+
Configuration priority: jac.toml > environment variable > default
|
|
15
|
+
|
|
16
|
+
In jac.toml:
|
|
17
|
+
[storage]
|
|
18
|
+
type = "local" # or "s3", "gcs", "azure"
|
|
19
|
+
base_path = "/data/storage"
|
|
20
|
+
create_dirs = true
|
|
21
|
+
|
|
22
|
+
Environment variables:
|
|
23
|
+
JAC_STORAGE_TYPE: Storage type (local, s3, gcs, azure)
|
|
24
|
+
JAC_STORAGE_PATH: Base directory for local storage
|
|
25
|
+
JAC_STORAGE_CREATE_DIRS: Whether to auto-create directories
|
|
26
|
+
"""
|
|
27
|
+
class StorageFactory {
|
|
28
|
+
static def create(
|
|
29
|
+
storage_type: (StorageType | str), config: (dict[str, Any] | None) = None
|
|
30
|
+
) -> Storage {
|
|
31
|
+
# Convert string to enum if needed
|
|
32
|
+
if isinstance(storage_type, str) {
|
|
33
|
+
storage_type = StorageType(storage_type);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if storage_type == StorageType.LOCAL {
|
|
37
|
+
import from jaclang.runtimelib.storage { LocalStorage }
|
|
38
|
+
cfg = config or {};
|
|
39
|
+
return LocalStorage(
|
|
40
|
+
base_path=cfg.get("base_path", "./storage"),
|
|
41
|
+
create_dirs=cfg.get("create_dirs", True)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
raise ValueError(f"Unsupported storage type: {storage_type}") ;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static def get_default(
|
|
48
|
+
base_path: str = "./storage", create_dirs: bool = True
|
|
49
|
+
) -> Storage {
|
|
50
|
+
# Get config from jac.toml if available
|
|
51
|
+
jac_config = get_config();
|
|
52
|
+
|
|
53
|
+
# Config priority: jac.toml > env var > parameter
|
|
54
|
+
if jac_config and jac_config.storage {
|
|
55
|
+
storage_type = StorageType(jac_config.storage.storage_type);
|
|
56
|
+
base_path = jac_config.storage.base_path;
|
|
57
|
+
create_dirs = jac_config.storage.create_dirs;
|
|
58
|
+
} elif os.environ.get("JAC_STORAGE_TYPE") or os.environ.get("JAC_STORAGE_PATH") {
|
|
59
|
+
storage_type_str = os.environ.get(
|
|
60
|
+
"JAC_STORAGE_TYPE", StorageType.LOCAL.value
|
|
61
|
+
);
|
|
62
|
+
storage_type = StorageType(storage_type_str);
|
|
63
|
+
base_path = os.environ.get("JAC_STORAGE_PATH", base_path);
|
|
64
|
+
create_dirs_str = os.environ.get(
|
|
65
|
+
"JAC_STORAGE_CREATE_DIRS", str(create_dirs)
|
|
66
|
+
);
|
|
67
|
+
create_dirs = create_dirs_str.lower() == "true";
|
|
68
|
+
} else {
|
|
69
|
+
storage_type = StorageType.LOCAL;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
storage_config = {"base_path": base_path, "create_dirs": create_dirs};
|
|
73
|
+
return StorageFactory.create(storage_type, storage_config);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Google SSO Provider implementation for jac-scale.
|
|
2
|
+
|
|
3
|
+
This module implements the SSOProvider interface for Google OAuth authentication.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import from fastapi { Request, Response }
|
|
7
|
+
import from fastapi_sso.sso.google { GoogleSSO }
|
|
8
|
+
import from jac_scale.sso_provider { SSOProvider, SSOUserInfo }
|
|
9
|
+
|
|
10
|
+
"""Google OAuth SSO Provider.
|
|
11
|
+
|
|
12
|
+
This class wraps the fastapi_sso GoogleSSO implementation to conform to
|
|
13
|
+
our SSOProvider interface, enabling consistent handling across all SSO vendors.
|
|
14
|
+
"""
|
|
15
|
+
obj GoogleSSOProvider(SSOProvider) {
|
|
16
|
+
has client_id: str,
|
|
17
|
+
client_secret: str,
|
|
18
|
+
redirect_uri: str,
|
|
19
|
+
allow_insecure_http: bool = True,
|
|
20
|
+
_google_sso: (GoogleSSO | None) = None;
|
|
21
|
+
|
|
22
|
+
"""Initialize the Google SSO provider."""
|
|
23
|
+
def postinit -> None {
|
|
24
|
+
self._google_sso = GoogleSSO(
|
|
25
|
+
client_id=self.client_id,
|
|
26
|
+
client_secret=self.client_secret,
|
|
27
|
+
redirect_uri=self.redirect_uri,
|
|
28
|
+
allow_insecure_http=self.allow_insecure_http
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
"""Initiate Google OAuth authentication flow.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
operation: The operation type ('login' or 'register')
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
RedirectResponse to Google's OAuth authorization page
|
|
39
|
+
"""
|
|
40
|
+
async def initiate_auth(operation: str) -> Response {
|
|
41
|
+
if not self._google_sso {
|
|
42
|
+
raise RuntimeError("GoogleSSO not initialized") ;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
with self._google_sso {
|
|
46
|
+
return await self._google_sso.get_login_redirect();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
"""Handle the OAuth callback from Google.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
request: The FastAPI request object containing OAuth callback data
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
SSOUserInfo: Standardized user information from Google
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
Exception: If authentication fails or user info cannot be retrieved
|
|
60
|
+
"""
|
|
61
|
+
async def handle_callback(request: Request) -> SSOUserInfo {
|
|
62
|
+
if not self._google_sso {
|
|
63
|
+
raise RuntimeError("GoogleSSO not initialized") ;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
with self._google_sso {
|
|
67
|
+
user_info = await self._google_sso.verify_and_process(request);
|
|
68
|
+
return SSOUserInfo(
|
|
69
|
+
email=user_info.email,
|
|
70
|
+
external_id=user_info.id,
|
|
71
|
+
platform=self.get_platform_name(),
|
|
72
|
+
display_name=user_info?.display_name
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
"""Get the platform identifier.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
str: 'google'
|
|
81
|
+
"""
|
|
82
|
+
def get_platform_name -> str {
|
|
83
|
+
return "google";
|
|
84
|
+
}
|
|
85
|
+
}
|
jac_scale/impl/context.impl.jac
CHANGED
|
@@ -21,4 +21,7 @@ impl JScaleExecutionContext.init(self: JScaleExecutionContext) -> None {
|
|
|
21
21
|
# Default user_root and entry_node to system_root
|
|
22
22
|
self.user_root = self.system_root;
|
|
23
23
|
self.entry_node = self.system_root;
|
|
24
|
+
self.call_state: ContextVar[CallState] = ContextVar(
|
|
25
|
+
'call_state', default=CallState()
|
|
26
|
+
);
|
|
24
27
|
}
|
jac_scale/impl/serve.impl.jac
CHANGED
|
@@ -486,10 +486,17 @@ impl JacAPIServer.render_base_route_callback(
|
|
|
486
486
|
|
|
487
487
|
impl JacAPIServer.register_functions_endpoints -> None {
|
|
488
488
|
for func_name in self.get_functions() {
|
|
489
|
+
func_obj = self.get_functions()[func_name];
|
|
490
|
+
restspec = func_obj.restspec if func_obj?.restspec else None;
|
|
491
|
+
spec_method = restspec.method if restspec else HTTPMethod.POST;
|
|
492
|
+
spec_path = restspec.path if restspec else None;
|
|
493
|
+
|
|
494
|
+
final_path = spec_path or f"/function/{func_name}";
|
|
495
|
+
|
|
489
496
|
self.server.add_endpoint(
|
|
490
497
|
JEndPoint(
|
|
491
|
-
method=
|
|
492
|
-
path=
|
|
498
|
+
method=spec_method,
|
|
499
|
+
path=final_path,
|
|
493
500
|
callback=self.create_function_callback(func_name),
|
|
494
501
|
parameters=self.create_function_parameters(func_name),
|
|
495
502
|
response_model=None,
|
|
@@ -546,7 +553,7 @@ impl JacAPIServer.create_function_callback(
|
|
|
546
553
|
) -> Callable[..., TransportResponse] {
|
|
547
554
|
import from jaclang.runtimelib.transport { TransportResponse, Meta }
|
|
548
555
|
requires_auth = self.introspector.is_auth_required_for_function(func_name);
|
|
549
|
-
def callback(**kwargs: JsonValue) -> TransportResponse {
|
|
556
|
+
async def callback(**kwargs: JsonValue) -> TransportResponse {
|
|
550
557
|
username: (str | None) = None;
|
|
551
558
|
if requires_auth {
|
|
552
559
|
authorization = kwargs.pop('Authorization', None);
|
|
@@ -558,7 +565,7 @@ impl JacAPIServer.create_function_callback(
|
|
|
558
565
|
) {
|
|
559
566
|
token = authorization[7:];
|
|
560
567
|
}
|
|
561
|
-
username = self.validate_jwt_token(token) if token else None;
|
|
568
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
562
569
|
if not username {
|
|
563
570
|
return TransportResponse.fail(
|
|
564
571
|
code='UNAUTHORIZED',
|
|
@@ -568,9 +575,21 @@ impl JacAPIServer.create_function_callback(
|
|
|
568
575
|
}
|
|
569
576
|
}
|
|
570
577
|
print(f"Executing function '{func_name}' with params: {kwargs}");
|
|
571
|
-
result = self.execution_manager.execute_function(
|
|
578
|
+
result = await self.execution_manager.execute_function(
|
|
572
579
|
self.get_functions()[func_name], kwargs, (username or '__guest__')
|
|
573
580
|
);
|
|
581
|
+
# Handle streaming responses (generators/async generators)
|
|
582
|
+
if (isgenerator(result) or isinstance(result, AsyncGenerator)) {
|
|
583
|
+
return StreamingResponse(
|
|
584
|
+
result,
|
|
585
|
+
media_type='text/event-stream',
|
|
586
|
+
headers={
|
|
587
|
+
'Cache-Control': 'no-cache',
|
|
588
|
+
'Connection': 'close',
|
|
589
|
+
'X-Accel-Buffering': 'no'
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
}
|
|
574
593
|
if 'error' in result {
|
|
575
594
|
return TransportResponse.fail(
|
|
576
595
|
code='EXECUTION_ERROR',
|
|
@@ -588,10 +607,15 @@ impl JacAPIServer.create_function_callback(
|
|
|
588
607
|
|
|
589
608
|
impl JacAPIServer.register_walkers_endpoints -> None {
|
|
590
609
|
for walker_name in self.get_walkers() {
|
|
610
|
+
walker_cls = self.get_walkers()[walker_name];
|
|
611
|
+
restspec = walker_cls.restspec if walker_cls?.restspec else None;
|
|
612
|
+
spec_method = restspec.method if restspec?.method else HTTPMethod.POST;
|
|
613
|
+
spec_path = restspec.path if restspec?.path else f"/walker/{walker_name}";
|
|
614
|
+
|
|
591
615
|
self.server.add_endpoint(
|
|
592
616
|
JEndPoint(
|
|
593
|
-
method=
|
|
594
|
-
path=f"
|
|
617
|
+
method=spec_method,
|
|
618
|
+
path=f"{spec_path}/{{node}}",
|
|
595
619
|
callback=self.create_walker_callback(walker_name, has_node_param=True),
|
|
596
620
|
parameters=self.create_walker_parameters(
|
|
597
621
|
walker_name, invoke_on_root=False
|
|
@@ -604,8 +628,8 @@ impl JacAPIServer.register_walkers_endpoints -> None {
|
|
|
604
628
|
);
|
|
605
629
|
self.server.add_endpoint(
|
|
606
630
|
JEndPoint(
|
|
607
|
-
method=
|
|
608
|
-
path=
|
|
631
|
+
method=spec_method,
|
|
632
|
+
path=spec_path,
|
|
609
633
|
callback=self.create_walker_callback(walker_name, has_node_param=False),
|
|
610
634
|
parameters=self.create_walker_parameters(
|
|
611
635
|
walker_name, invoke_on_root=True
|
|
@@ -685,7 +709,7 @@ impl JacAPIServer.create_walker_callback(
|
|
|
685
709
|
) {
|
|
686
710
|
token = authorization[7:];
|
|
687
711
|
}
|
|
688
|
-
username = self.validate_jwt_token(token) if token else None;
|
|
712
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
689
713
|
if not username {
|
|
690
714
|
return TransportResponse.fail(
|
|
691
715
|
code='UNAUTHORIZED',
|
|
@@ -700,6 +724,17 @@ impl JacAPIServer.create_walker_callback(
|
|
|
700
724
|
result = await self.execution_manager.spawn_walker(
|
|
701
725
|
self.get_walkers()[walker_name], kwargs, (username or '__guest__')
|
|
702
726
|
);
|
|
727
|
+
if (isgenerator(result) or isinstance(result, AsyncGenerator)) {
|
|
728
|
+
return StreamingResponse(
|
|
729
|
+
result,
|
|
730
|
+
media_type='text/event-stream',
|
|
731
|
+
headers={
|
|
732
|
+
'Cache-Control': 'no-cache',
|
|
733
|
+
'Connection': 'close',
|
|
734
|
+
'X-Accel-Buffering': 'no'
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
}
|
|
703
738
|
if 'error' in result {
|
|
704
739
|
return TransportResponse.fail(
|
|
705
740
|
code='EXECUTION_ERROR',
|
|
@@ -783,7 +818,7 @@ impl JacAPIServer.refresh_token(token: (str | None) = None) -> TransportResponse
|
|
|
783
818
|
if token.startswith('Bearer ') {
|
|
784
819
|
token = token[7:];
|
|
785
820
|
}
|
|
786
|
-
new_token = self.refresh_jwt_token(token);
|
|
821
|
+
new_token = self.user_manager.refresh_jwt_token(token);
|
|
787
822
|
if (not new_token) {
|
|
788
823
|
return TransportResponse.fail(
|
|
789
824
|
code='UNAUTHORIZED',
|
|
@@ -809,7 +844,7 @@ impl JacAPIServer.create_user(username: str, password: str) -> TransportResponse
|
|
|
809
844
|
meta=Meta(extra={'http_status': 400})
|
|
810
845
|
);
|
|
811
846
|
}
|
|
812
|
-
res['token'] = self.create_jwt_token(username);
|
|
847
|
+
res['token'] = self.user_manager.create_jwt_token(username);
|
|
813
848
|
return TransportResponse.success(
|
|
814
849
|
data=res, meta=Meta(extra={'http_status': 201})
|
|
815
850
|
);
|
|
@@ -837,7 +872,7 @@ impl JacAPIServer.update_username(
|
|
|
837
872
|
) {
|
|
838
873
|
token = Authorization[7:];
|
|
839
874
|
}
|
|
840
|
-
token_username = self.validate_jwt_token(token) if token else None;
|
|
875
|
+
token_username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
841
876
|
if not token_username {
|
|
842
877
|
return TransportResponse.fail(
|
|
843
878
|
code='UNAUTHORIZED',
|
|
@@ -869,7 +904,7 @@ impl JacAPIServer.update_username(
|
|
|
869
904
|
);
|
|
870
905
|
}
|
|
871
906
|
# Generate new JWT token with updated username
|
|
872
|
-
result['token'] = self.create_jwt_token(new_username);
|
|
907
|
+
result['token'] = self.user_manager.create_jwt_token(new_username);
|
|
873
908
|
return TransportResponse.success(
|
|
874
909
|
data=result, meta=Meta(extra={'http_status': 200})
|
|
875
910
|
);
|
|
@@ -891,7 +926,7 @@ impl JacAPIServer.update_password(
|
|
|
891
926
|
) {
|
|
892
927
|
token = Authorization[7:];
|
|
893
928
|
}
|
|
894
|
-
token_username = self.validate_jwt_token(token) if token else None;
|
|
929
|
+
token_username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
895
930
|
if not token_username {
|
|
896
931
|
return TransportResponse.fail(
|
|
897
932
|
code='UNAUTHORIZED',
|
|
@@ -1066,7 +1101,7 @@ impl JacAPIServer.login(username: str, password: str) -> TransportResponse {
|
|
|
1066
1101
|
meta=Meta(extra={'http_status': 401})
|
|
1067
1102
|
);
|
|
1068
1103
|
}
|
|
1069
|
-
result['token'] = self.create_jwt_token(username);
|
|
1104
|
+
result['token'] = self.user_manager.create_jwt_token(username);
|
|
1070
1105
|
return TransportResponse.success(
|
|
1071
1106
|
data=dict[(str, JsonValue)](result), meta=Meta(extra={'http_status': 200})
|
|
1072
1107
|
);
|
|
@@ -1109,219 +1144,6 @@ impl JacAPIServer.postinit -> None {
|
|
|
1109
1144
|
}
|
|
1110
1145
|
self.server.app.add_middleware(CustomHeadersMiddleware);
|
|
1111
1146
|
}
|
|
1112
|
-
self.SUPPORTED_PLATFORMS: dict = {};
|
|
1113
|
-
# Load SSO config fresh (not from cached global) to support env var overrides at runtime
|
|
1114
|
-
sso_config = get_scale_config().get_sso_config();
|
|
1115
|
-
for platform in Platforms {
|
|
1116
|
-
key = platform.lower();
|
|
1117
|
-
platform_config = sso_config.get(key, {});
|
|
1118
|
-
|
|
1119
|
-
client_id = platform_config.get('client_id', '');
|
|
1120
|
-
client_secret = platform_config.get('client_secret', '');
|
|
1121
|
-
|
|
1122
|
-
if not client_id or not client_secret {
|
|
1123
|
-
continue;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
self.SUPPORTED_PLATFORMS[platform.value] = {
|
|
1127
|
-
"client_id": client_id,
|
|
1128
|
-
"client_secret": client_secret
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
impl JacAPIServer.refresh_jwt_token(token: str) -> (str | None) {
|
|
1134
|
-
try {
|
|
1135
|
-
decoded = jwt.decode(
|
|
1136
|
-
token, JWT_SECRET, algorithms=[JWT_ALGORITHM], options={"verify_exp": True}
|
|
1137
|
-
);
|
|
1138
|
-
username = decoded.get('username');
|
|
1139
|
-
|
|
1140
|
-
if not username {
|
|
1141
|
-
return None;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
return JacAPIServer.create_jwt_token(username);
|
|
1145
|
-
} except Exception {
|
|
1146
|
-
return None;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
impl JacAPIServer.validate_jwt_token(token: str) -> (str | None) {
|
|
1151
|
-
try {
|
|
1152
|
-
decoded = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]);
|
|
1153
|
-
return decoded['username'];
|
|
1154
|
-
} except Exception {
|
|
1155
|
-
return None;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
impl JacAPIServer.create_jwt_token(username: str) -> str {
|
|
1160
|
-
now = datetime.now(UTC);
|
|
1161
|
-
payload: dict[(str, Any)] = {
|
|
1162
|
-
'username': username,
|
|
1163
|
-
'exp': (now + timedelta(days=JWT_EXP_DELTA_DAYS)),
|
|
1164
|
-
'iat': now.timestamp()
|
|
1165
|
-
};
|
|
1166
|
-
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM);
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
impl JacAPIServer.get_sso(platform: str, operation: str) -> (GoogleSSO | None) {
|
|
1170
|
-
if (platform not in self.SUPPORTED_PLATFORMS) {
|
|
1171
|
-
return None;
|
|
1172
|
-
}
|
|
1173
|
-
credentials = self.SUPPORTED_PLATFORMS[platform];
|
|
1174
|
-
redirect_uri = f"{SSO_HOST}/{platform}/{operation}/callback";
|
|
1175
|
-
if (platform == Platforms.GOOGLE.value) {
|
|
1176
|
-
return GoogleSSO(
|
|
1177
|
-
client_id=credentials['client_id'],
|
|
1178
|
-
client_secret=credentials['client_secret'],
|
|
1179
|
-
redirect_uri=redirect_uri,
|
|
1180
|
-
allow_insecure_http=True
|
|
1181
|
-
);
|
|
1182
|
-
}
|
|
1183
|
-
return None;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
impl JacAPIServer.sso_initiate(
|
|
1187
|
-
platform: str, operation: str
|
|
1188
|
-
) -> (Response | TransportResponse) {
|
|
1189
|
-
import from jaclang.runtimelib.transport { TransportResponse, Meta }
|
|
1190
|
-
if (platform not in [p.value for p in Platforms]) {
|
|
1191
|
-
return TransportResponse.fail(
|
|
1192
|
-
code='INVALID_PLATFORM',
|
|
1193
|
-
message=f"Invalid platform '{platform}'. Supported platforms: {', '.join(
|
|
1194
|
-
[p.value for p in Platforms]
|
|
1195
|
-
)}",
|
|
1196
|
-
meta=Meta(extra={'http_status': 400})
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
if (platform not in self.SUPPORTED_PLATFORMS) {
|
|
1200
|
-
return TransportResponse.fail(
|
|
1201
|
-
code='SSO_NOT_CONFIGURED',
|
|
1202
|
-
message=f"SSO for platform '{platform}' is not configured. Please set SSO_{platform.upper()}_CLIENT_ID and SSO_{platform.upper()}_CLIENT_SECRET environment variables.",
|
|
1203
|
-
meta=Meta(extra={'http_status': 501})
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
1206
|
-
if (operation not in [o.value for o in Operations]) {
|
|
1207
|
-
return TransportResponse.fail(
|
|
1208
|
-
code='INVALID_OPERATION',
|
|
1209
|
-
message=f"Invalid operation '{operation}'. Must be 'login' or 'register'",
|
|
1210
|
-
meta=Meta(extra={'http_status': 400})
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
sso = self.get_sso(platform, operation);
|
|
1214
|
-
if not sso {
|
|
1215
|
-
return TransportResponse.fail(
|
|
1216
|
-
code='SSO_INIT_FAILED',
|
|
1217
|
-
message=f"Failed to initialize SSO for platform '{platform}'",
|
|
1218
|
-
meta=Meta(extra={'http_status': 500})
|
|
1219
|
-
);
|
|
1220
|
-
}
|
|
1221
|
-
with sso {
|
|
1222
|
-
return await sso.get_login_redirect();
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
impl JacAPIServer.sso_callback(
|
|
1227
|
-
request: Request, platform: str, operation: str
|
|
1228
|
-
) -> TransportResponse {
|
|
1229
|
-
import from jaclang.runtimelib.transport { TransportResponse, Meta }
|
|
1230
|
-
if (platform not in [p.value for p in Platforms]) {
|
|
1231
|
-
return TransportResponse.fail(
|
|
1232
|
-
code='INVALID_PLATFORM',
|
|
1233
|
-
message=f"Invalid platform '{platform}'. Supported platforms: {', '.join(
|
|
1234
|
-
[p.value for p in Platforms]
|
|
1235
|
-
)}",
|
|
1236
|
-
meta=Meta(extra={'http_status': 400})
|
|
1237
|
-
);
|
|
1238
|
-
}
|
|
1239
|
-
if (platform not in self.SUPPORTED_PLATFORMS) {
|
|
1240
|
-
return TransportResponse.fail(
|
|
1241
|
-
code='SSO_NOT_CONFIGURED',
|
|
1242
|
-
message=f"SSO for platform '{platform}' is not configured. Please set SSO_{platform.upper()}_CLIENT_ID and SSO_{platform.upper()}_CLIENT_SECRET environment variables.",
|
|
1243
|
-
meta=Meta(extra={'http_status': 501})
|
|
1244
|
-
);
|
|
1245
|
-
}
|
|
1246
|
-
if (operation not in [o.value for o in Operations]) {
|
|
1247
|
-
return TransportResponse.fail(
|
|
1248
|
-
code='INVALID_OPERATION',
|
|
1249
|
-
message=f"Invalid operation '{operation}'. Must be 'login' or 'register'",
|
|
1250
|
-
meta=Meta(extra={'http_status': 400})
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
sso = self.get_sso(platform, operation);
|
|
1254
|
-
if not sso {
|
|
1255
|
-
return TransportResponse.fail(
|
|
1256
|
-
code='SSO_INIT_FAILED',
|
|
1257
|
-
message=f"Failed to initialize SSO for platform '{platform}'",
|
|
1258
|
-
meta=Meta(extra={'http_status': 500})
|
|
1259
|
-
);
|
|
1260
|
-
}
|
|
1261
|
-
try {
|
|
1262
|
-
with sso {
|
|
1263
|
-
user_info = await sso.verify_and_process(request);
|
|
1264
|
-
email = user_info.email;
|
|
1265
|
-
if not email {
|
|
1266
|
-
return TransportResponse.fail(
|
|
1267
|
-
code='EMAIL_MISSING',
|
|
1268
|
-
message=f"Email not provided by {platform}",
|
|
1269
|
-
meta=Meta(extra={'http_status': 400})
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
if (operation == Operations.LOGIN.value) {
|
|
1273
|
-
user = self.user_manager.get_user(email);
|
|
1274
|
-
if not user {
|
|
1275
|
-
return TransportResponse.fail(
|
|
1276
|
-
code='USER_NOT_FOUND',
|
|
1277
|
-
message='User not found. Please register first.',
|
|
1278
|
-
meta=Meta(extra={'http_status': 404})
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1281
|
-
token = self.create_jwt_token(email);
|
|
1282
|
-
return TransportResponse.success(
|
|
1283
|
-
data={
|
|
1284
|
-
'message': 'Login successful',
|
|
1285
|
-
'email': email,
|
|
1286
|
-
'token': token,
|
|
1287
|
-
'platform': platform,
|
|
1288
|
-
'user': dict[(str, JsonValue)](user)
|
|
1289
|
-
},
|
|
1290
|
-
meta=Meta(extra={'http_status': 200})
|
|
1291
|
-
);
|
|
1292
|
-
} elif (operation == Operations.REGISTER.value) {
|
|
1293
|
-
existing_user = self.user_manager.get_user(email);
|
|
1294
|
-
if existing_user {
|
|
1295
|
-
return TransportResponse.fail(
|
|
1296
|
-
code='USER_EXISTS',
|
|
1297
|
-
message='User already exists. Please login instead.',
|
|
1298
|
-
meta=Meta(extra={'http_status': 400})
|
|
1299
|
-
);
|
|
1300
|
-
}
|
|
1301
|
-
random_password = generate_random_password();
|
|
1302
|
-
result = self.user_manager.create_user(email, random_password);
|
|
1303
|
-
if ('error' in result) {
|
|
1304
|
-
return TransportResponse.fail(
|
|
1305
|
-
code='USER_CREATION_FAILED',
|
|
1306
|
-
message=result.get('error', 'User creation failed'),
|
|
1307
|
-
meta=Meta(extra={'http_status': 400})
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
token = self.create_jwt_token(email);
|
|
1311
|
-
result['token'] = token;
|
|
1312
|
-
result['platform'] = platform;
|
|
1313
|
-
return TransportResponse.success(
|
|
1314
|
-
data=result, meta=Meta(extra={'http_status': 201})
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
} except Exception as e {
|
|
1319
|
-
return TransportResponse.fail(
|
|
1320
|
-
code='AUTHENTICATION_FAILED',
|
|
1321
|
-
message=f"Authentication failed: {str(e)}",
|
|
1322
|
-
meta=Meta(extra={'http_status': 500})
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
1147
|
}
|
|
1326
1148
|
|
|
1327
1149
|
impl JacAPIServer.register_sso_endpoints -> None {
|
|
@@ -1329,7 +1151,7 @@ impl JacAPIServer.register_sso_endpoints -> None {
|
|
|
1329
1151
|
JEndPoint(
|
|
1330
1152
|
method=HTTPMethod.GET,
|
|
1331
1153
|
path='/sso/{platform}/{operation}',
|
|
1332
|
-
callback=self.sso_initiate,
|
|
1154
|
+
callback=self.user_manager.sso_initiate,
|
|
1333
1155
|
parameters=[
|
|
1334
1156
|
APIParameter(
|
|
1335
1157
|
name='platform',
|
|
@@ -1358,7 +1180,7 @@ impl JacAPIServer.register_sso_endpoints -> None {
|
|
|
1358
1180
|
JEndPoint(
|
|
1359
1181
|
method=HTTPMethod.GET,
|
|
1360
1182
|
path='/sso/{platform}/{operation}/callback',
|
|
1361
|
-
callback=self.sso_callback,
|
|
1183
|
+
callback=self.user_manager.sso_callback,
|
|
1362
1184
|
parameters=[
|
|
1363
1185
|
APIParameter(
|
|
1364
1186
|
name='platform',
|
|
@@ -1455,7 +1277,7 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1455
1277
|
) {
|
|
1456
1278
|
token = Authorization[7:];
|
|
1457
1279
|
}
|
|
1458
|
-
username = self.validate_jwt_token(token) if token else None;
|
|
1280
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1459
1281
|
if not username {
|
|
1460
1282
|
return TransportResponse.fail(
|
|
1461
1283
|
code='UNAUTHORIZED',
|
|
@@ -1473,6 +1295,20 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1473
1295
|
result = await self.execution_manager.spawn_walker(
|
|
1474
1296
|
walkers[walker_name], kwargs, (username or '__guest__')
|
|
1475
1297
|
);
|
|
1298
|
+
|
|
1299
|
+
# Handle streaming responses (generators/async generators)
|
|
1300
|
+
if (isgenerator(result) or isinstance(result, AsyncGenerator)) {
|
|
1301
|
+
return StreamingResponse(
|
|
1302
|
+
result,
|
|
1303
|
+
media_type='text/event-stream',
|
|
1304
|
+
headers={
|
|
1305
|
+
'Cache-Control': 'no-cache',
|
|
1306
|
+
'Connection': 'close',
|
|
1307
|
+
'X-Accel-Buffering': 'no'
|
|
1308
|
+
}
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1476
1312
|
if 'error' in result {
|
|
1477
1313
|
return TransportResponse.fail(
|
|
1478
1314
|
code='EXECUTION_ERROR',
|
|
@@ -1604,7 +1440,7 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1604
1440
|
) {
|
|
1605
1441
|
token = Authorization[7:];
|
|
1606
1442
|
}
|
|
1607
|
-
username = self.validate_jwt_token(token) if token else None;
|
|
1443
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1608
1444
|
if not username {
|
|
1609
1445
|
return TransportResponse.fail(
|
|
1610
1446
|
code='UNAUTHORIZED',
|
|
@@ -1614,9 +1450,21 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1614
1450
|
}
|
|
1615
1451
|
}
|
|
1616
1452
|
|
|
1617
|
-
result = self.execution_manager.execute_function(
|
|
1453
|
+
result = await self.execution_manager.execute_function(
|
|
1618
1454
|
functions[function_name], kwargs, (username or '__guest__')
|
|
1619
1455
|
);
|
|
1456
|
+
# Handle streaming responses (generators/async generators)
|
|
1457
|
+
if (isgenerator(result) or isinstance(result, AsyncGenerator)) {
|
|
1458
|
+
return StreamingResponse(
|
|
1459
|
+
result,
|
|
1460
|
+
media_type='text/event-stream',
|
|
1461
|
+
headers={
|
|
1462
|
+
'Cache-Control': 'no-cache',
|
|
1463
|
+
'Connection': 'close',
|
|
1464
|
+
'X-Accel-Buffering': 'no'
|
|
1465
|
+
}
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1620
1468
|
if 'error' in result {
|
|
1621
1469
|
return TransportResponse.fail(
|
|
1622
1470
|
code='EXECUTION_ERROR',
|