jac-scale 0.1.1__py3-none-any.whl → 0.1.4__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/config_loader.jac +2 -1
- 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/config_loader.impl.jac +28 -3
- jac_scale/impl/context.impl.jac +1 -0
- jac_scale/impl/serve.impl.jac +749 -266
- jac_scale/impl/user_manager.impl.jac +349 -0
- jac_scale/impl/webhook.impl.jac +212 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +4 -0
- jac_scale/memory_hierarchy.jac +3 -1
- jac_scale/plugin.jac +46 -3
- jac_scale/plugin_config.jac +28 -1
- jac_scale/serve.jac +33 -16
- 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 +89 -0
- jac_scale/tests/fixtures/test_restspec.jac +88 -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 +289 -0
- jac_scale/tests/test_serve.py +411 -4
- 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/webhook.jac +93 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/METADATA +11 -4
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/RECORD +36 -23
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/WHEEL +1 -1
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/entry_points.txt +0 -0
- {jac_scale-0.1.1.dist-info → jac_scale-0.1.4.dist-info}/top_level.txt +0 -0
jac_scale/impl/serve.impl.jac
CHANGED
|
@@ -57,10 +57,10 @@ def _transport_response_to_json_response(
|
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
impl JacAPIServer.start(dev: bool = False) -> None {
|
|
60
|
+
impl JacAPIServer.start(dev: bool = False, no_client: bool = False) -> None {
|
|
61
61
|
self.introspector.load();
|
|
62
|
-
# Eagerly build client bundle if there are client exports (skip in dev mode)
|
|
63
|
-
if not dev {
|
|
62
|
+
# Eagerly build client bundle if there are client exports (skip in dev or no_client mode)
|
|
63
|
+
if not dev and not no_client {
|
|
64
64
|
client_exports = self.introspector._client_manifest.get('exports', []);
|
|
65
65
|
if client_exports {
|
|
66
66
|
import time;
|
|
@@ -96,14 +96,17 @@ impl JacAPIServer.start(dev: bool = False) -> None {
|
|
|
96
96
|
self.register_static_file_endpoint();
|
|
97
97
|
self.register_update_username_endpoint();
|
|
98
98
|
self.register_update_password_endpoint();
|
|
99
|
+
self.register_api_key_endpoints();
|
|
99
100
|
# Use dynamic routing for HMR support, static routing for production
|
|
100
101
|
if dev {
|
|
101
102
|
self.register_dynamic_walker_endpoint();
|
|
102
103
|
self.register_dynamic_function_endpoint();
|
|
103
104
|
self.register_dynamic_introspection_endpoints();
|
|
105
|
+
self.register_dynamic_webhook_endpoint();
|
|
104
106
|
} else {
|
|
105
107
|
self.register_walkers_endpoints();
|
|
106
108
|
self.register_functions_endpoints();
|
|
109
|
+
self.register_webhook_endpoints();
|
|
107
110
|
}
|
|
108
111
|
self.register_root_asset_endpoint();
|
|
109
112
|
self._configure_openapi_security();
|
|
@@ -177,19 +180,35 @@ impl JacAPIServer._configure_openapi_security -> None {
|
|
|
177
180
|
self.server.app.openapi = custom_openapi;
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
"""Serve root-level assets like /img.png, /icons/logo.svg, etc.
|
|
183
|
+
"""Serve root-level assets like /img.png, /icons/logo.svg, etc.
|
|
184
|
+
Falls back to SPA HTML for extensionless paths when base_route_app is configured."""
|
|
181
185
|
impl JacAPIServer.serve_root_asset(file_path: str) -> Response {
|
|
182
186
|
allowed_extensions = {'.png','.jpg','.jpeg','.gif','.webp','.svg','.ico','.woff','.woff2','.ttf','.otf','.eot','.mp4','.webm','.mp3','.wav','.css','.js','.json','.pdf','.txt','.xml'};
|
|
183
187
|
file_ext = Path(file_path).suffix.lower();
|
|
184
|
-
if (not file_ext or (file_ext not in allowed_extensions)) {
|
|
185
|
-
return Response(status_code=404, content='Not found', media_type='text/plain');
|
|
186
|
-
}
|
|
187
188
|
import from jaclang.project.config { get_config }
|
|
188
189
|
config = get_config();
|
|
189
190
|
cl_route_prefix = config.serve.cl_route_prefix if config else "cl";
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
api_prefixes = (f'{cl_route_prefix}/', 'walker/', 'function/', 'user/', 'static/');
|
|
192
|
+
if (not file_ext or (file_ext not in allowed_extensions)) {
|
|
193
|
+
# SPA catch-all: serve base_route_app for extensionless paths
|
|
194
|
+
base_route_app = config.serve.base_route_app if config else "";
|
|
195
|
+
if (not file_ext and base_route_app and not file_path.startswith(api_prefixes)) {
|
|
196
|
+
try {
|
|
197
|
+
render_payload = self.introspector.render_page(
|
|
198
|
+
base_route_app, {}, '__guest__'
|
|
199
|
+
);
|
|
200
|
+
return HTMLResponse(content=render_payload['html']);
|
|
201
|
+
} except ValueError {
|
|
202
|
+
return HTMLResponse(content="<h1>404 Not Found</h1>", status_code=404);
|
|
203
|
+
} except RuntimeError {
|
|
204
|
+
return HTMLResponse(
|
|
205
|
+
content="<h1>503 Service Unavailable</h1>", status_code=503
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return Response(status_code=404, content='Not found', media_type='text/plain');
|
|
210
|
+
}
|
|
211
|
+
if file_path.startswith(api_prefixes) {
|
|
193
212
|
return Response(status_code=404, content='Not found', media_type='text/plain');
|
|
194
213
|
}
|
|
195
214
|
# Find project root (where jac.toml is) instead of using base_path_dir
|
|
@@ -486,12 +505,21 @@ impl JacAPIServer.render_base_route_callback(
|
|
|
486
505
|
|
|
487
506
|
impl JacAPIServer.register_functions_endpoints -> None {
|
|
488
507
|
for func_name in self.get_functions() {
|
|
508
|
+
func_obj = self.get_functions()[func_name];
|
|
509
|
+
restspec = func_obj.restspec if func_obj?.restspec else None;
|
|
510
|
+
spec_method = restspec.method if restspec else HTTPMethod.POST;
|
|
511
|
+
spec_path = restspec.path if restspec else None;
|
|
512
|
+
|
|
513
|
+
final_path = spec_path or f"/function/{func_name}";
|
|
514
|
+
|
|
489
515
|
self.server.add_endpoint(
|
|
490
516
|
JEndPoint(
|
|
491
|
-
method=
|
|
492
|
-
path=
|
|
517
|
+
method=spec_method,
|
|
518
|
+
path=final_path,
|
|
493
519
|
callback=self.create_function_callback(func_name),
|
|
494
|
-
parameters=self.create_function_parameters(
|
|
520
|
+
parameters=self.create_function_parameters(
|
|
521
|
+
func_name, method=spec_method
|
|
522
|
+
),
|
|
495
523
|
response_model=None,
|
|
496
524
|
tags=['Functions'],
|
|
497
525
|
summary='This is a summary',
|
|
@@ -501,7 +529,9 @@ impl JacAPIServer.register_functions_endpoints -> None {
|
|
|
501
529
|
}
|
|
502
530
|
}
|
|
503
531
|
|
|
504
|
-
impl JacAPIServer.create_function_parameters(
|
|
532
|
+
impl JacAPIServer.create_function_parameters(
|
|
533
|
+
func_name: str, method: HTTPMethod = HTTPMethod.POST
|
|
534
|
+
) -> list[APIParameter] {
|
|
505
535
|
parameters: list[APIParameter] = [];
|
|
506
536
|
if self.introspector.is_auth_required_for_function(func_name) {
|
|
507
537
|
parameters.append(
|
|
@@ -520,8 +550,13 @@ impl JacAPIServer.create_function_parameters(func_name: str) -> list[APIParamete
|
|
|
520
550
|
)['parameters'];
|
|
521
551
|
for field_name in func_fields {
|
|
522
552
|
field_type = func_fields[field_name]['type'];
|
|
523
|
-
# Determine parameter type based on field type
|
|
524
|
-
if (
|
|
553
|
+
# Determine parameter type based on field type and method
|
|
554
|
+
if (
|
|
555
|
+
method == HTTPMethod.GET
|
|
556
|
+
and not ('UploadFile' in field_type or 'uploadfile' in field_type.lower())
|
|
557
|
+
) {
|
|
558
|
+
param_type = ParameterType.QUERY;
|
|
559
|
+
} elif ('UploadFile' in field_type or 'uploadfile' in field_type.lower()) {
|
|
525
560
|
# Support UploadFile type for file uploads
|
|
526
561
|
param_type = ParameterType.FILE;
|
|
527
562
|
} else {
|
|
@@ -546,7 +581,7 @@ impl JacAPIServer.create_function_callback(
|
|
|
546
581
|
) -> Callable[..., TransportResponse] {
|
|
547
582
|
import from jaclang.runtimelib.transport { TransportResponse, Meta }
|
|
548
583
|
requires_auth = self.introspector.is_auth_required_for_function(func_name);
|
|
549
|
-
def callback(**kwargs: JsonValue) -> TransportResponse {
|
|
584
|
+
async def callback(**kwargs: JsonValue) -> TransportResponse {
|
|
550
585
|
username: (str | None) = None;
|
|
551
586
|
if requires_auth {
|
|
552
587
|
authorization = kwargs.pop('Authorization', None);
|
|
@@ -558,7 +593,7 @@ impl JacAPIServer.create_function_callback(
|
|
|
558
593
|
) {
|
|
559
594
|
token = authorization[7:];
|
|
560
595
|
}
|
|
561
|
-
username = self.validate_jwt_token(token) if token else None;
|
|
596
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
562
597
|
if not username {
|
|
563
598
|
return TransportResponse.fail(
|
|
564
599
|
code='UNAUTHORIZED',
|
|
@@ -568,9 +603,21 @@ impl JacAPIServer.create_function_callback(
|
|
|
568
603
|
}
|
|
569
604
|
}
|
|
570
605
|
print(f"Executing function '{func_name}' with params: {kwargs}");
|
|
571
|
-
result = self.execution_manager.execute_function(
|
|
606
|
+
result = await self.execution_manager.execute_function(
|
|
572
607
|
self.get_functions()[func_name], kwargs, (username or '__guest__')
|
|
573
608
|
);
|
|
609
|
+
# Handle streaming responses (generators/async generators)
|
|
610
|
+
if (isgenerator(result) or isinstance(result, AsyncGenerator)) {
|
|
611
|
+
return StreamingResponse(
|
|
612
|
+
result,
|
|
613
|
+
media_type='text/event-stream',
|
|
614
|
+
headers={
|
|
615
|
+
'Cache-Control': 'no-cache',
|
|
616
|
+
'Connection': 'close',
|
|
617
|
+
'X-Accel-Buffering': 'no'
|
|
618
|
+
}
|
|
619
|
+
);
|
|
620
|
+
}
|
|
574
621
|
if 'error' in result {
|
|
575
622
|
return TransportResponse.fail(
|
|
576
623
|
code='EXECUTION_ERROR',
|
|
@@ -588,13 +635,23 @@ impl JacAPIServer.create_function_callback(
|
|
|
588
635
|
|
|
589
636
|
impl JacAPIServer.register_walkers_endpoints -> None {
|
|
590
637
|
for walker_name in self.get_walkers() {
|
|
638
|
+
# Skip walkers configured for webhook transport - they use /webhook/{walker_name} instead
|
|
639
|
+
transport_type = self.get_transport_type_for_walker(walker_name);
|
|
640
|
+
if transport_type == TransportType.WEBHOOK {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
walker_cls = self.get_walkers()[walker_name];
|
|
644
|
+
restspec = walker_cls.restspec if walker_cls?.restspec else None;
|
|
645
|
+
spec_method = restspec.method if restspec?.method else HTTPMethod.POST;
|
|
646
|
+
spec_path = restspec.path if restspec?.path else f"/walker/{walker_name}";
|
|
647
|
+
|
|
591
648
|
self.server.add_endpoint(
|
|
592
649
|
JEndPoint(
|
|
593
|
-
method=
|
|
594
|
-
path=f"
|
|
650
|
+
method=spec_method,
|
|
651
|
+
path=f"{spec_path}/{{node}}",
|
|
595
652
|
callback=self.create_walker_callback(walker_name, has_node_param=True),
|
|
596
653
|
parameters=self.create_walker_parameters(
|
|
597
|
-
walker_name, invoke_on_root=False
|
|
654
|
+
walker_name, invoke_on_root=False, method=spec_method
|
|
598
655
|
),
|
|
599
656
|
response_model=None,
|
|
600
657
|
tags=['Walkers'],
|
|
@@ -604,11 +661,11 @@ impl JacAPIServer.register_walkers_endpoints -> None {
|
|
|
604
661
|
);
|
|
605
662
|
self.server.add_endpoint(
|
|
606
663
|
JEndPoint(
|
|
607
|
-
method=
|
|
608
|
-
path=
|
|
664
|
+
method=spec_method,
|
|
665
|
+
path=spec_path,
|
|
609
666
|
callback=self.create_walker_callback(walker_name, has_node_param=False),
|
|
610
667
|
parameters=self.create_walker_parameters(
|
|
611
|
-
walker_name, invoke_on_root=True
|
|
668
|
+
walker_name, invoke_on_root=True, method=spec_method
|
|
612
669
|
),
|
|
613
670
|
response_model=None,
|
|
614
671
|
tags=['Walkers'],
|
|
@@ -620,7 +677,7 @@ impl JacAPIServer.register_walkers_endpoints -> None {
|
|
|
620
677
|
}
|
|
621
678
|
|
|
622
679
|
impl JacAPIServer.create_walker_parameters(
|
|
623
|
-
walker_name: str, invoke_on_root: bool
|
|
680
|
+
walker_name: str, invoke_on_root: bool, method: HTTPMethod = HTTPMethod.POST
|
|
624
681
|
) -> list[APIParameter] {
|
|
625
682
|
parameters: list[APIParameter] = [];
|
|
626
683
|
if self.introspector.is_auth_required_for_walker(walker_name) {
|
|
@@ -643,9 +700,14 @@ impl JacAPIServer.create_walker_parameters(
|
|
|
643
700
|
continue;
|
|
644
701
|
}
|
|
645
702
|
field_type = walker_fields[field_name]['type'];
|
|
646
|
-
# Determine parameter type based on field type
|
|
703
|
+
# Determine parameter type based on field type and method
|
|
647
704
|
if (field_name == '_jac_spawn_node') {
|
|
648
705
|
param_type = ParameterType.PATH;
|
|
706
|
+
} elif (
|
|
707
|
+
method == HTTPMethod.GET
|
|
708
|
+
and not ('UploadFile' in field_type or 'uploadfile' in field_type.lower())
|
|
709
|
+
) {
|
|
710
|
+
param_type = ParameterType.QUERY;
|
|
649
711
|
} elif ('UploadFile' in field_type or 'uploadfile' in field_type.lower()) {
|
|
650
712
|
# Support UploadFile type for file uploads
|
|
651
713
|
param_type = ParameterType.FILE;
|
|
@@ -685,7 +747,7 @@ impl JacAPIServer.create_walker_callback(
|
|
|
685
747
|
) {
|
|
686
748
|
token = authorization[7:];
|
|
687
749
|
}
|
|
688
|
-
username = self.validate_jwt_token(token) if token else None;
|
|
750
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
689
751
|
if not username {
|
|
690
752
|
return TransportResponse.fail(
|
|
691
753
|
code='UNAUTHORIZED',
|
|
@@ -700,6 +762,17 @@ impl JacAPIServer.create_walker_callback(
|
|
|
700
762
|
result = await self.execution_manager.spawn_walker(
|
|
701
763
|
self.get_walkers()[walker_name], kwargs, (username or '__guest__')
|
|
702
764
|
);
|
|
765
|
+
if (isgenerator(result) or isinstance(result, AsyncGenerator)) {
|
|
766
|
+
return StreamingResponse(
|
|
767
|
+
result,
|
|
768
|
+
media_type='text/event-stream',
|
|
769
|
+
headers={
|
|
770
|
+
'Cache-Control': 'no-cache',
|
|
771
|
+
'Connection': 'close',
|
|
772
|
+
'X-Accel-Buffering': 'no'
|
|
773
|
+
}
|
|
774
|
+
);
|
|
775
|
+
}
|
|
703
776
|
if 'error' in result {
|
|
704
777
|
return TransportResponse.fail(
|
|
705
778
|
code='EXECUTION_ERROR',
|
|
@@ -783,7 +856,7 @@ impl JacAPIServer.refresh_token(token: (str | None) = None) -> TransportResponse
|
|
|
783
856
|
if token.startswith('Bearer ') {
|
|
784
857
|
token = token[7:];
|
|
785
858
|
}
|
|
786
|
-
new_token = self.refresh_jwt_token(token);
|
|
859
|
+
new_token = self.user_manager.refresh_jwt_token(token);
|
|
787
860
|
if (not new_token) {
|
|
788
861
|
return TransportResponse.fail(
|
|
789
862
|
code='UNAUTHORIZED',
|
|
@@ -809,7 +882,7 @@ impl JacAPIServer.create_user(username: str, password: str) -> TransportResponse
|
|
|
809
882
|
meta=Meta(extra={'http_status': 400})
|
|
810
883
|
);
|
|
811
884
|
}
|
|
812
|
-
res['token'] = self.create_jwt_token(username);
|
|
885
|
+
res['token'] = self.user_manager.create_jwt_token(username);
|
|
813
886
|
return TransportResponse.success(
|
|
814
887
|
data=res, meta=Meta(extra={'http_status': 201})
|
|
815
888
|
);
|
|
@@ -837,7 +910,7 @@ impl JacAPIServer.update_username(
|
|
|
837
910
|
) {
|
|
838
911
|
token = Authorization[7:];
|
|
839
912
|
}
|
|
840
|
-
token_username = self.validate_jwt_token(token) if token else None;
|
|
913
|
+
token_username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
841
914
|
if not token_username {
|
|
842
915
|
return TransportResponse.fail(
|
|
843
916
|
code='UNAUTHORIZED',
|
|
@@ -869,7 +942,7 @@ impl JacAPIServer.update_username(
|
|
|
869
942
|
);
|
|
870
943
|
}
|
|
871
944
|
# Generate new JWT token with updated username
|
|
872
|
-
result['token'] = self.create_jwt_token(new_username);
|
|
945
|
+
result['token'] = self.user_manager.create_jwt_token(new_username);
|
|
873
946
|
return TransportResponse.success(
|
|
874
947
|
data=result, meta=Meta(extra={'http_status': 200})
|
|
875
948
|
);
|
|
@@ -891,7 +964,7 @@ impl JacAPIServer.update_password(
|
|
|
891
964
|
) {
|
|
892
965
|
token = Authorization[7:];
|
|
893
966
|
}
|
|
894
|
-
token_username = self.validate_jwt_token(token) if token else None;
|
|
967
|
+
token_username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
895
968
|
if not token_username {
|
|
896
969
|
return TransportResponse.fail(
|
|
897
970
|
code='UNAUTHORIZED',
|
|
@@ -1066,7 +1139,7 @@ impl JacAPIServer.login(username: str, password: str) -> TransportResponse {
|
|
|
1066
1139
|
meta=Meta(extra={'http_status': 401})
|
|
1067
1140
|
);
|
|
1068
1141
|
}
|
|
1069
|
-
result['token'] = self.create_jwt_token(username);
|
|
1142
|
+
result['token'] = self.user_manager.create_jwt_token(username);
|
|
1070
1143
|
return TransportResponse.success(
|
|
1071
1144
|
data=dict[(str, JsonValue)](result), meta=Meta(extra={'http_status': 200})
|
|
1072
1145
|
);
|
|
@@ -1074,6 +1147,7 @@ impl JacAPIServer.login(username: str, password: str) -> TransportResponse {
|
|
|
1074
1147
|
|
|
1075
1148
|
impl JacAPIServer.postinit -> None {
|
|
1076
1149
|
super.postinit();
|
|
1150
|
+
self._api_key_manager = ApiKeyManager();
|
|
1077
1151
|
self.server.app.add_middleware(
|
|
1078
1152
|
CORSMiddleware,
|
|
1079
1153
|
allow_origins=['*'],
|
|
@@ -1109,219 +1183,6 @@ impl JacAPIServer.postinit -> None {
|
|
|
1109
1183
|
}
|
|
1110
1184
|
self.server.app.add_middleware(CustomHeadersMiddleware);
|
|
1111
1185
|
}
|
|
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
1186
|
}
|
|
1326
1187
|
|
|
1327
1188
|
impl JacAPIServer.register_sso_endpoints -> None {
|
|
@@ -1329,7 +1190,7 @@ impl JacAPIServer.register_sso_endpoints -> None {
|
|
|
1329
1190
|
JEndPoint(
|
|
1330
1191
|
method=HTTPMethod.GET,
|
|
1331
1192
|
path='/sso/{platform}/{operation}',
|
|
1332
|
-
callback=self.sso_initiate,
|
|
1193
|
+
callback=self.user_manager.sso_initiate,
|
|
1333
1194
|
parameters=[
|
|
1334
1195
|
APIParameter(
|
|
1335
1196
|
name='platform',
|
|
@@ -1358,7 +1219,7 @@ impl JacAPIServer.register_sso_endpoints -> None {
|
|
|
1358
1219
|
JEndPoint(
|
|
1359
1220
|
method=HTTPMethod.GET,
|
|
1360
1221
|
path='/sso/{platform}/{operation}/callback',
|
|
1361
|
-
callback=self.sso_callback,
|
|
1222
|
+
callback=self.user_manager.sso_callback,
|
|
1362
1223
|
parameters=[
|
|
1363
1224
|
APIParameter(
|
|
1364
1225
|
name='platform',
|
|
@@ -1417,13 +1278,17 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1417
1278
|
node: str | None = None,
|
|
1418
1279
|
Authorization: str | None = None
|
|
1419
1280
|
) -> TransportResponse {
|
|
1420
|
-
# Parse request body to get walker fields
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1281
|
+
# Parse request body or query params to get walker fields
|
|
1282
|
+
if request.method == 'GET' {
|
|
1283
|
+
kwargs: dict[str, Any] = dict(request.query_params);
|
|
1284
|
+
} else {
|
|
1285
|
+
try {
|
|
1286
|
+
body = await request.json();
|
|
1287
|
+
} except Exception {
|
|
1288
|
+
body = {};
|
|
1289
|
+
}
|
|
1290
|
+
kwargs = dict(body) if body else {};
|
|
1425
1291
|
}
|
|
1426
|
-
kwargs: dict[str, Any] = dict(body) if body else {};
|
|
1427
1292
|
|
|
1428
1293
|
# Reload introspector if files changed (HMR)
|
|
1429
1294
|
if self._hmr_pending {
|
|
@@ -1443,6 +1308,16 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1443
1308
|
);
|
|
1444
1309
|
}
|
|
1445
1310
|
|
|
1311
|
+
# Reject walkers configured for webhook transport - they use /webhook/{walker_name} instead
|
|
1312
|
+
transport_type = self.get_transport_type_for_walker(walker_name);
|
|
1313
|
+
if transport_type == TransportType.WEBHOOK {
|
|
1314
|
+
return TransportResponse.fail(
|
|
1315
|
+
code='BAD_REQUEST',
|
|
1316
|
+
message=f"Walker '{walker_name}' is configured as a webhook. Use /webhook/{walker_name} instead.",
|
|
1317
|
+
meta=Meta(extra={'http_status': 400})
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1446
1321
|
# Handle authentication
|
|
1447
1322
|
username: str | None = None;
|
|
1448
1323
|
authorization = kwargs.pop('Authorization', None);
|
|
@@ -1455,7 +1330,7 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1455
1330
|
) {
|
|
1456
1331
|
token = Authorization[7:];
|
|
1457
1332
|
}
|
|
1458
|
-
username = self.validate_jwt_token(token) if token else None;
|
|
1333
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1459
1334
|
if not username {
|
|
1460
1335
|
return TransportResponse.fail(
|
|
1461
1336
|
code='UNAUTHORIZED',
|
|
@@ -1473,6 +1348,20 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1473
1348
|
result = await self.execution_manager.spawn_walker(
|
|
1474
1349
|
walkers[walker_name], kwargs, (username or '__guest__')
|
|
1475
1350
|
);
|
|
1351
|
+
|
|
1352
|
+
# Handle streaming responses (generators/async generators)
|
|
1353
|
+
if (isgenerator(result) or isinstance(result, AsyncGenerator)) {
|
|
1354
|
+
return StreamingResponse(
|
|
1355
|
+
result,
|
|
1356
|
+
media_type='text/event-stream',
|
|
1357
|
+
headers={
|
|
1358
|
+
'Cache-Control': 'no-cache',
|
|
1359
|
+
'Connection': 'close',
|
|
1360
|
+
'X-Accel-Buffering': 'no'
|
|
1361
|
+
}
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1476
1365
|
if 'error' in result {
|
|
1477
1366
|
return TransportResponse.fail(
|
|
1478
1367
|
code='EXECUTION_ERROR',
|
|
@@ -1485,7 +1374,7 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1485
1374
|
data=result, meta=Meta(extra={'http_status': 200})
|
|
1486
1375
|
);
|
|
1487
1376
|
}
|
|
1488
|
-
# Register catch-all route for walkers with node parameter
|
|
1377
|
+
# Register catch-all route for walkers with node parameter (POST)
|
|
1489
1378
|
self.server.add_endpoint(
|
|
1490
1379
|
JEndPoint(
|
|
1491
1380
|
method=HTTPMethod.POST,
|
|
@@ -1523,7 +1412,45 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1523
1412
|
description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
|
|
1524
1413
|
)
|
|
1525
1414
|
);
|
|
1526
|
-
# Register catch-all route for walkers
|
|
1415
|
+
# Register catch-all route for walkers with node parameter (GET)
|
|
1416
|
+
self.server.add_endpoint(
|
|
1417
|
+
JEndPoint(
|
|
1418
|
+
method=HTTPMethod.GET,
|
|
1419
|
+
path='/walker/{walker_name}/{node}',
|
|
1420
|
+
callback=dynamic_walker_handler,
|
|
1421
|
+
parameters=[
|
|
1422
|
+
APIParameter(
|
|
1423
|
+
name='walker_name',
|
|
1424
|
+
data_type='string',
|
|
1425
|
+
required=True,
|
|
1426
|
+
default=None,
|
|
1427
|
+
description='Name of the walker to execute',
|
|
1428
|
+
type=ParameterType.PATH
|
|
1429
|
+
),
|
|
1430
|
+
APIParameter(
|
|
1431
|
+
name='node',
|
|
1432
|
+
data_type='string',
|
|
1433
|
+
required=True,
|
|
1434
|
+
default=None,
|
|
1435
|
+
description='Node ID to spawn walker on',
|
|
1436
|
+
type=ParameterType.PATH
|
|
1437
|
+
),
|
|
1438
|
+
APIParameter(
|
|
1439
|
+
name='Authorization',
|
|
1440
|
+
data_type='string',
|
|
1441
|
+
required=False,
|
|
1442
|
+
default=None,
|
|
1443
|
+
description='Bearer token for authentication',
|
|
1444
|
+
type=ParameterType.HEADER
|
|
1445
|
+
)
|
|
1446
|
+
],
|
|
1447
|
+
response_model=None,
|
|
1448
|
+
tags=['Walkers (Dynamic)'],
|
|
1449
|
+
summary='Execute walker on node (dynamic HMR)',
|
|
1450
|
+
description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
|
|
1451
|
+
)
|
|
1452
|
+
);
|
|
1453
|
+
# Register catch-all route for walkers without node parameter (root) (POST)
|
|
1527
1454
|
self.server.add_endpoint(
|
|
1528
1455
|
JEndPoint(
|
|
1529
1456
|
method=HTTPMethod.POST,
|
|
@@ -1553,6 +1480,36 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1553
1480
|
description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
|
|
1554
1481
|
)
|
|
1555
1482
|
);
|
|
1483
|
+
# Register catch-all route for walkers without node parameter (root) (GET)
|
|
1484
|
+
self.server.add_endpoint(
|
|
1485
|
+
JEndPoint(
|
|
1486
|
+
method=HTTPMethod.GET,
|
|
1487
|
+
path='/walker/{walker_name}',
|
|
1488
|
+
callback=dynamic_walker_handler,
|
|
1489
|
+
parameters=[
|
|
1490
|
+
APIParameter(
|
|
1491
|
+
name='walker_name',
|
|
1492
|
+
data_type='string',
|
|
1493
|
+
required=True,
|
|
1494
|
+
default=None,
|
|
1495
|
+
description='Name of the walker to execute',
|
|
1496
|
+
type=ParameterType.PATH
|
|
1497
|
+
),
|
|
1498
|
+
APIParameter(
|
|
1499
|
+
name='Authorization',
|
|
1500
|
+
data_type='string',
|
|
1501
|
+
required=False,
|
|
1502
|
+
default=None,
|
|
1503
|
+
description='Bearer token for authentication',
|
|
1504
|
+
type=ParameterType.HEADER
|
|
1505
|
+
)
|
|
1506
|
+
],
|
|
1507
|
+
response_model=None,
|
|
1508
|
+
tags=['Walkers (Dynamic)'],
|
|
1509
|
+
summary='Execute walker on root (dynamic HMR)',
|
|
1510
|
+
description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
|
|
1511
|
+
)
|
|
1512
|
+
);
|
|
1556
1513
|
}
|
|
1557
1514
|
|
|
1558
1515
|
"""Register a single dynamic endpoint for all functions.
|
|
@@ -1566,13 +1523,17 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1566
1523
|
async def dynamic_function_handler(
|
|
1567
1524
|
request: Request, function_name: str, Authorization: str | None = None
|
|
1568
1525
|
) -> TransportResponse {
|
|
1569
|
-
# Parse request body to get function arguments
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1526
|
+
# Parse request body or query params to get function arguments
|
|
1527
|
+
if request.method == 'GET' {
|
|
1528
|
+
kwargs: dict[str, Any] = dict(request.query_params);
|
|
1529
|
+
} else {
|
|
1530
|
+
try {
|
|
1531
|
+
body = await request.json();
|
|
1532
|
+
} except Exception {
|
|
1533
|
+
body = {};
|
|
1534
|
+
}
|
|
1535
|
+
kwargs = dict(body) if body else {};
|
|
1574
1536
|
}
|
|
1575
|
-
kwargs: dict[str, Any] = dict(body) if body else {};
|
|
1576
1537
|
|
|
1577
1538
|
# Reload introspector if files changed (HMR)
|
|
1578
1539
|
if self._hmr_pending {
|
|
@@ -1604,7 +1565,7 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1604
1565
|
) {
|
|
1605
1566
|
token = Authorization[7:];
|
|
1606
1567
|
}
|
|
1607
|
-
username = self.validate_jwt_token(token) if token else None;
|
|
1568
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1608
1569
|
if not username {
|
|
1609
1570
|
return TransportResponse.fail(
|
|
1610
1571
|
code='UNAUTHORIZED',
|
|
@@ -1614,9 +1575,21 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1614
1575
|
}
|
|
1615
1576
|
}
|
|
1616
1577
|
|
|
1617
|
-
result = self.execution_manager.execute_function(
|
|
1578
|
+
result = await self.execution_manager.execute_function(
|
|
1618
1579
|
functions[function_name], kwargs, (username or '__guest__')
|
|
1619
1580
|
);
|
|
1581
|
+
# Handle streaming responses (generators/async generators)
|
|
1582
|
+
if (isgenerator(result) or isinstance(result, AsyncGenerator)) {
|
|
1583
|
+
return StreamingResponse(
|
|
1584
|
+
result,
|
|
1585
|
+
media_type='text/event-stream',
|
|
1586
|
+
headers={
|
|
1587
|
+
'Cache-Control': 'no-cache',
|
|
1588
|
+
'Connection': 'close',
|
|
1589
|
+
'X-Accel-Buffering': 'no'
|
|
1590
|
+
}
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1620
1593
|
if 'error' in result {
|
|
1621
1594
|
return TransportResponse.fail(
|
|
1622
1595
|
code='EXECUTION_ERROR',
|
|
@@ -1629,6 +1602,7 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1629
1602
|
data=result, meta=Meta(extra={'http_status': 200})
|
|
1630
1603
|
);
|
|
1631
1604
|
}
|
|
1605
|
+
# Register POST endpoint
|
|
1632
1606
|
self.server.add_endpoint(
|
|
1633
1607
|
JEndPoint(
|
|
1634
1608
|
method=HTTPMethod.POST,
|
|
@@ -1658,6 +1632,36 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1658
1632
|
description='Dynamically routes to any registered function. Supports HMR - function changes are reflected immediately.'
|
|
1659
1633
|
)
|
|
1660
1634
|
);
|
|
1635
|
+
# Register GET endpoint
|
|
1636
|
+
self.server.add_endpoint(
|
|
1637
|
+
JEndPoint(
|
|
1638
|
+
method=HTTPMethod.GET,
|
|
1639
|
+
path='/function/{function_name}',
|
|
1640
|
+
callback=dynamic_function_handler,
|
|
1641
|
+
parameters=[
|
|
1642
|
+
APIParameter(
|
|
1643
|
+
name='function_name',
|
|
1644
|
+
data_type='string',
|
|
1645
|
+
required=True,
|
|
1646
|
+
default=None,
|
|
1647
|
+
description='Name of the function to call',
|
|
1648
|
+
type=ParameterType.PATH
|
|
1649
|
+
),
|
|
1650
|
+
APIParameter(
|
|
1651
|
+
name='Authorization',
|
|
1652
|
+
data_type='string',
|
|
1653
|
+
required=False,
|
|
1654
|
+
default=None,
|
|
1655
|
+
description='Bearer token for authentication',
|
|
1656
|
+
type=ParameterType.HEADER
|
|
1657
|
+
)
|
|
1658
|
+
],
|
|
1659
|
+
response_model=None,
|
|
1660
|
+
tags=['Functions (Dynamic)'],
|
|
1661
|
+
summary='Call function (dynamic HMR)',
|
|
1662
|
+
description='Dynamically routes to any registered function. Supports HMR - function changes are reflected immediately.'
|
|
1663
|
+
)
|
|
1664
|
+
);
|
|
1661
1665
|
}
|
|
1662
1666
|
|
|
1663
1667
|
"""Register endpoints for runtime introspection of available walkers/functions.
|
|
@@ -1783,3 +1787,482 @@ impl JacAPIServer.register_dynamic_introspection_endpoints -> None {
|
|
|
1783
1787
|
)
|
|
1784
1788
|
);
|
|
1785
1789
|
}
|
|
1790
|
+
|
|
1791
|
+
"""Get or create the API key manager instance."""
|
|
1792
|
+
impl JacAPIServer.get_api_key_manager -> ApiKeyManager {
|
|
1793
|
+
if self._api_key_manager is None {
|
|
1794
|
+
self._api_key_manager = ApiKeyManager();
|
|
1795
|
+
}
|
|
1796
|
+
return self._api_key_manager;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
"""Create a new API key for the authenticated user."""
|
|
1800
|
+
impl JacAPIServer.create_api_key(
|
|
1801
|
+
name: str, expiry_days: int | None = None, Authorization: str | None = None
|
|
1802
|
+
) -> TransportResponse {
|
|
1803
|
+
# Validate JWT token to get username
|
|
1804
|
+
token: str | None = None;
|
|
1805
|
+
if Authorization and Authorization.startswith('Bearer ') {
|
|
1806
|
+
token = Authorization[7:];
|
|
1807
|
+
}
|
|
1808
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1809
|
+
if not username {
|
|
1810
|
+
return TransportResponse.fail(
|
|
1811
|
+
code='UNAUTHORIZED',
|
|
1812
|
+
message='Valid authentication required to create API keys',
|
|
1813
|
+
meta=Meta(extra={'http_status': 401})
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
return self.get_api_key_manager().create_api_key(
|
|
1817
|
+
username=username, name=name, expiry_days=expiry_days
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
"""List all API keys for the authenticated user."""
|
|
1822
|
+
impl JacAPIServer.list_api_keys(Authorization: str | None = None) -> TransportResponse {
|
|
1823
|
+
# Validate JWT token to get username
|
|
1824
|
+
token: str | None = None;
|
|
1825
|
+
if Authorization and Authorization.startswith('Bearer ') {
|
|
1826
|
+
token = Authorization[7:];
|
|
1827
|
+
}
|
|
1828
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1829
|
+
if not username {
|
|
1830
|
+
return TransportResponse.fail(
|
|
1831
|
+
code='UNAUTHORIZED',
|
|
1832
|
+
message='Valid authentication required to list API keys',
|
|
1833
|
+
meta=Meta(extra={'http_status': 401})
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
return self.get_api_key_manager().list_api_keys(username);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
"""Revoke an API key for the authenticated user."""
|
|
1840
|
+
impl JacAPIServer.revoke_api_key(
|
|
1841
|
+
api_key_id: str, Authorization: str | None = None
|
|
1842
|
+
) -> TransportResponse {
|
|
1843
|
+
# Validate JWT token to get username
|
|
1844
|
+
token: str | None = None;
|
|
1845
|
+
if Authorization and Authorization.startswith('Bearer ') {
|
|
1846
|
+
token = Authorization[7:];
|
|
1847
|
+
}
|
|
1848
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1849
|
+
if not username {
|
|
1850
|
+
return TransportResponse.fail(
|
|
1851
|
+
code='UNAUTHORIZED',
|
|
1852
|
+
message='Valid authentication required to revoke API keys',
|
|
1853
|
+
meta=Meta(extra={'http_status': 401})
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
return self.get_api_key_manager().revoke_api_key(username, api_key_id);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
"""Register API key management endpoints."""
|
|
1860
|
+
impl JacAPIServer.register_api_key_endpoints -> None {
|
|
1861
|
+
# Create API key
|
|
1862
|
+
self.server.add_endpoint(
|
|
1863
|
+
JEndPoint(
|
|
1864
|
+
method=HTTPMethod.POST,
|
|
1865
|
+
path='/api-key/create',
|
|
1866
|
+
callback=self.create_api_key,
|
|
1867
|
+
parameters=[
|
|
1868
|
+
APIParameter(
|
|
1869
|
+
name='name',
|
|
1870
|
+
data_type='string',
|
|
1871
|
+
required=True,
|
|
1872
|
+
default=None,
|
|
1873
|
+
description='A friendly name for the API key',
|
|
1874
|
+
type=ParameterType.BODY
|
|
1875
|
+
),
|
|
1876
|
+
APIParameter(
|
|
1877
|
+
name='expiry_days',
|
|
1878
|
+
data_type='integer',
|
|
1879
|
+
required=False,
|
|
1880
|
+
default=None,
|
|
1881
|
+
description='Number of days until expiry (default from config)',
|
|
1882
|
+
type=ParameterType.BODY
|
|
1883
|
+
),
|
|
1884
|
+
APIParameter(
|
|
1885
|
+
name='Authorization',
|
|
1886
|
+
data_type='string',
|
|
1887
|
+
required=True,
|
|
1888
|
+
default=None,
|
|
1889
|
+
description='Bearer token for authentication',
|
|
1890
|
+
type=ParameterType.HEADER
|
|
1891
|
+
)
|
|
1892
|
+
],
|
|
1893
|
+
response_model=None,
|
|
1894
|
+
tags=['API Keys'],
|
|
1895
|
+
summary='Create a new API key',
|
|
1896
|
+
description='Creates a new API key for webhook authentication. The API key is wrapped in a JWT and can be used with HMAC-SHA256 signature verification.'
|
|
1897
|
+
)
|
|
1898
|
+
);
|
|
1899
|
+
# List API keys
|
|
1900
|
+
self.server.add_endpoint(
|
|
1901
|
+
JEndPoint(
|
|
1902
|
+
method=HTTPMethod.GET,
|
|
1903
|
+
path='/api-key/list',
|
|
1904
|
+
callback=self.list_api_keys,
|
|
1905
|
+
parameters=[
|
|
1906
|
+
APIParameter(
|
|
1907
|
+
name='Authorization',
|
|
1908
|
+
data_type='string',
|
|
1909
|
+
required=True,
|
|
1910
|
+
default=None,
|
|
1911
|
+
description='Bearer token for authentication',
|
|
1912
|
+
type=ParameterType.HEADER
|
|
1913
|
+
)
|
|
1914
|
+
],
|
|
1915
|
+
response_model=None,
|
|
1916
|
+
tags=['API Keys'],
|
|
1917
|
+
summary='List all API keys',
|
|
1918
|
+
description='Lists all API keys for the authenticated user (metadata only, not the actual keys).'
|
|
1919
|
+
)
|
|
1920
|
+
);
|
|
1921
|
+
# Revoke API key
|
|
1922
|
+
self.server.add_endpoint(
|
|
1923
|
+
JEndPoint(
|
|
1924
|
+
method=HTTPMethod.DELETE,
|
|
1925
|
+
path='/api-key/{api_key_id}',
|
|
1926
|
+
callback=self.revoke_api_key,
|
|
1927
|
+
parameters=[
|
|
1928
|
+
APIParameter(
|
|
1929
|
+
name='api_key_id',
|
|
1930
|
+
data_type='string',
|
|
1931
|
+
required=True,
|
|
1932
|
+
default=None,
|
|
1933
|
+
description='The ID of the API key to revoke',
|
|
1934
|
+
type=ParameterType.PATH
|
|
1935
|
+
),
|
|
1936
|
+
APIParameter(
|
|
1937
|
+
name='Authorization',
|
|
1938
|
+
data_type='string',
|
|
1939
|
+
required=True,
|
|
1940
|
+
default=None,
|
|
1941
|
+
description='Bearer token for authentication',
|
|
1942
|
+
type=ParameterType.HEADER
|
|
1943
|
+
)
|
|
1944
|
+
],
|
|
1945
|
+
response_model=None,
|
|
1946
|
+
tags=['API Keys'],
|
|
1947
|
+
summary='Revoke an API key',
|
|
1948
|
+
description='Revokes an API key, making it invalid for future webhook requests.'
|
|
1949
|
+
)
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
"""Get the transport type for a walker by checking its restspec."""
|
|
1954
|
+
impl JacAPIServer.get_transport_type_for_walker(walker_name: str) -> str {
|
|
1955
|
+
walkers = self.get_walkers();
|
|
1956
|
+
if walker_name not in walkers {
|
|
1957
|
+
return TransportType.HTTP;
|
|
1958
|
+
}
|
|
1959
|
+
walker_cls = walkers[walker_name];
|
|
1960
|
+
restspec = walker_cls.restspec if walker_cls?.restspec else None;
|
|
1961
|
+
if restspec?.webhook {
|
|
1962
|
+
return TransportType.WEBHOOK;
|
|
1963
|
+
}
|
|
1964
|
+
return TransportType.HTTP;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
"""Create webhook callback for a walker with HMAC-SHA256 signature verification."""
|
|
1968
|
+
impl JacAPIServer.create_webhook_callback(
|
|
1969
|
+
walker_name: str
|
|
1970
|
+
) -> Callable[..., TransportResponse] {
|
|
1971
|
+
async def callback(request: Request, **kwargs: JsonValue) -> TransportResponse {
|
|
1972
|
+
webhook_config = get_scale_config().get_webhook_config();
|
|
1973
|
+
signature_header = webhook_config.get(
|
|
1974
|
+
'signature_header', 'X-Webhook-Signature'
|
|
1975
|
+
);
|
|
1976
|
+
verify_signature = webhook_config.get('verify_signature', True);
|
|
1977
|
+
|
|
1978
|
+
# Get API key from header
|
|
1979
|
+
api_key = request.headers.get('X-API-Key');
|
|
1980
|
+
if not api_key {
|
|
1981
|
+
return TransportResponse.fail(
|
|
1982
|
+
code='UNAUTHORIZED',
|
|
1983
|
+
message='Missing X-API-Key header',
|
|
1984
|
+
meta=Meta(extra={'http_status': 401})
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
# Validate API key and get username
|
|
1989
|
+
username = self.get_api_key_manager().validate_api_key(api_key);
|
|
1990
|
+
if not username {
|
|
1991
|
+
return TransportResponse.fail(
|
|
1992
|
+
code='UNAUTHORIZED',
|
|
1993
|
+
message='Invalid or expired API key',
|
|
1994
|
+
meta=Meta(extra={'http_status': 401})
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
# Verify HMAC-SHA256 signature if enabled
|
|
1999
|
+
if verify_signature {
|
|
2000
|
+
signature = request.headers.get(signature_header);
|
|
2001
|
+
if not signature {
|
|
2002
|
+
return TransportResponse.fail(
|
|
2003
|
+
code='UNAUTHORIZED',
|
|
2004
|
+
message=f"Missing {signature_header} header",
|
|
2005
|
+
meta=Meta(extra={'http_status': 401})
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
# Get raw body for signature verification
|
|
2009
|
+
body = await request.body();
|
|
2010
|
+
extracted_signature = WebhookUtils.extract_signature(signature);
|
|
2011
|
+
# Use the API key as the secret for HMAC verification
|
|
2012
|
+
if not WebhookUtils.verify_signature(body, extracted_signature, api_key) {
|
|
2013
|
+
return TransportResponse.fail(
|
|
2014
|
+
code='UNAUTHORIZED',
|
|
2015
|
+
message='Invalid webhook signature',
|
|
2016
|
+
meta=Meta(extra={'http_status': 401})
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
# Parse body for walker fields
|
|
2022
|
+
try {
|
|
2023
|
+
body_bytes = await request.body();
|
|
2024
|
+
body_data = json.loads(body_bytes) if body_bytes else {};
|
|
2025
|
+
} except Exception {
|
|
2026
|
+
body_data = {};
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
walker_kwargs: dict[str, Any] = dict(body_data) if body_data else {};
|
|
2030
|
+
|
|
2031
|
+
# Execute the walker
|
|
2032
|
+
result = await self.execution_manager.spawn_walker(
|
|
2033
|
+
self.get_walkers()[walker_name], walker_kwargs, username
|
|
2034
|
+
);
|
|
2035
|
+
|
|
2036
|
+
if 'error' in result {
|
|
2037
|
+
return TransportResponse.fail(
|
|
2038
|
+
code='EXECUTION_ERROR',
|
|
2039
|
+
message=result.get('error', 'Walker execution failed'),
|
|
2040
|
+
details=result.get('traceback') if 'traceback' in result else None,
|
|
2041
|
+
meta=Meta(extra={'http_status': 500})
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
return TransportResponse.success(
|
|
2046
|
+
data=result, meta=Meta(extra={'http_status': 200})
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
return callback;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
"""Create parameters for webhook endpoint."""
|
|
2053
|
+
impl JacAPIServer.create_webhook_parameters(walker_name: str) -> list[APIParameter] {
|
|
2054
|
+
parameters: list[APIParameter] = [];
|
|
2055
|
+
# API key header (required for webhooks)
|
|
2056
|
+
# Use underscore naming for valid Python identifiers - FastAPI auto-converts to hyphenated headers
|
|
2057
|
+
parameters.append(
|
|
2058
|
+
APIParameter(
|
|
2059
|
+
name='x_api_key',
|
|
2060
|
+
data_type='string',
|
|
2061
|
+
required=True,
|
|
2062
|
+
default=None,
|
|
2063
|
+
description='API key for webhook authentication (X-API-Key header)',
|
|
2064
|
+
type=ParameterType.HEADER
|
|
2065
|
+
)
|
|
2066
|
+
);
|
|
2067
|
+
# Signature header (for HMAC verification)
|
|
2068
|
+
# Use underscore naming for valid Python identifiers
|
|
2069
|
+
parameters.append(
|
|
2070
|
+
APIParameter(
|
|
2071
|
+
name='x_webhook_signature',
|
|
2072
|
+
data_type='string',
|
|
2073
|
+
required=False,
|
|
2074
|
+
default=None,
|
|
2075
|
+
description='HMAC-SHA256 signature of the request body (X-Webhook-Signature header)',
|
|
2076
|
+
type=ParameterType.HEADER
|
|
2077
|
+
)
|
|
2078
|
+
);
|
|
2079
|
+
# Add walker fields as body parameters (excluding transport_type)
|
|
2080
|
+
walker_fields = self.introspector.introspect_walker(
|
|
2081
|
+
self.get_walkers()[walker_name]
|
|
2082
|
+
)['fields'];
|
|
2083
|
+
for field_name in walker_fields {
|
|
2084
|
+
if field_name in ('_jac_spawn_node', ) {
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
field_type = walker_fields[field_name]['type'];
|
|
2088
|
+
parameters.append(
|
|
2089
|
+
APIParameter(
|
|
2090
|
+
name=field_name,
|
|
2091
|
+
data_type=field_type,
|
|
2092
|
+
required=walker_fields[field_name]['required'],
|
|
2093
|
+
default=walker_fields[field_name]['default'],
|
|
2094
|
+
description=f"Field {field_name} for webhook walker {walker_name}",
|
|
2095
|
+
type=ParameterType.BODY
|
|
2096
|
+
)
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
return parameters;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
"""Register webhook endpoints for walkers with transport_type=WEBHOOK."""
|
|
2103
|
+
impl JacAPIServer.register_webhook_endpoints -> None {
|
|
2104
|
+
for walker_name in self.get_walkers() {
|
|
2105
|
+
transport_type = self.get_transport_type_for_walker(walker_name);
|
|
2106
|
+
|
|
2107
|
+
if transport_type == TransportType.WEBHOOK {
|
|
2108
|
+
self.server.add_endpoint(
|
|
2109
|
+
JEndPoint(
|
|
2110
|
+
method=HTTPMethod.POST,
|
|
2111
|
+
path=f"/webhook/{walker_name}",
|
|
2112
|
+
callback=self.create_webhook_callback(walker_name),
|
|
2113
|
+
parameters=self.create_webhook_parameters(walker_name),
|
|
2114
|
+
response_model=None,
|
|
2115
|
+
tags=['Webhooks'],
|
|
2116
|
+
summary=f'Webhook endpoint for {walker_name}',
|
|
2117
|
+
description=f'Webhook endpoint for {walker_name}. Requires API key authentication and HMAC-SHA256 signature verification.'
|
|
2118
|
+
)
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
"""Register dynamic webhook endpoint for HMR support."""
|
|
2125
|
+
impl JacAPIServer.register_dynamic_webhook_endpoint -> None {
|
|
2126
|
+
async def dynamic_webhook_handler(
|
|
2127
|
+
request: Request, walker_name: str
|
|
2128
|
+
) -> TransportResponse {
|
|
2129
|
+
# Reload introspector if files changed (HMR)
|
|
2130
|
+
if self._hmr_pending {
|
|
2131
|
+
self.introspector.load(force_reload=True);
|
|
2132
|
+
self._hmr_pending = False;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
walkers = self.get_walkers();
|
|
2136
|
+
|
|
2137
|
+
if walker_name not in walkers {
|
|
2138
|
+
return TransportResponse.fail(
|
|
2139
|
+
code='NOT_FOUND',
|
|
2140
|
+
message=f"Webhook walker '{walker_name}' not found",
|
|
2141
|
+
meta=Meta(extra={'http_status': 404})
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
# Verify this walker is configured for webhook transport
|
|
2146
|
+
transport_type = self.get_transport_type_for_walker(walker_name);
|
|
2147
|
+
if transport_type != TransportType.WEBHOOK {
|
|
2148
|
+
return TransportResponse.fail(
|
|
2149
|
+
code='BAD_REQUEST',
|
|
2150
|
+
message=f"Walker '{walker_name}' is not configured as a webhook. Use /walker/{walker_name} instead.",
|
|
2151
|
+
meta=Meta(extra={'http_status': 400})
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
webhook_config = get_scale_config().get_webhook_config();
|
|
2156
|
+
signature_header = webhook_config.get(
|
|
2157
|
+
'signature_header', 'X-Webhook-Signature'
|
|
2158
|
+
);
|
|
2159
|
+
verify_signature = webhook_config.get('verify_signature', True);
|
|
2160
|
+
|
|
2161
|
+
# Get API key from header
|
|
2162
|
+
api_key = request.headers.get('X-API-Key');
|
|
2163
|
+
if not api_key {
|
|
2164
|
+
return TransportResponse.fail(
|
|
2165
|
+
code='UNAUTHORIZED',
|
|
2166
|
+
message='Missing X-API-Key header',
|
|
2167
|
+
meta=Meta(extra={'http_status': 401})
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
# Validate API key and get username
|
|
2172
|
+
username = self.get_api_key_manager().validate_api_key(api_key);
|
|
2173
|
+
if not username {
|
|
2174
|
+
return TransportResponse.fail(
|
|
2175
|
+
code='UNAUTHORIZED',
|
|
2176
|
+
message='Invalid or expired API key',
|
|
2177
|
+
meta=Meta(extra={'http_status': 401})
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
# Verify HMAC-SHA256 signature if enabled
|
|
2182
|
+
if verify_signature {
|
|
2183
|
+
signature = request.headers.get(signature_header);
|
|
2184
|
+
if not signature {
|
|
2185
|
+
return TransportResponse.fail(
|
|
2186
|
+
code='UNAUTHORIZED',
|
|
2187
|
+
message=f"Missing {signature_header} header",
|
|
2188
|
+
meta=Meta(extra={'http_status': 401})
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
body = await request.body();
|
|
2192
|
+
extracted_signature = WebhookUtils.extract_signature(signature);
|
|
2193
|
+
if not WebhookUtils.verify_signature(body, extracted_signature, api_key) {
|
|
2194
|
+
return TransportResponse.fail(
|
|
2195
|
+
code='UNAUTHORIZED',
|
|
2196
|
+
message='Invalid webhook signature',
|
|
2197
|
+
meta=Meta(extra={'http_status': 401})
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
# Parse body for walker fields
|
|
2203
|
+
try {
|
|
2204
|
+
body_bytes = await request.body();
|
|
2205
|
+
body_data = json.loads(body_bytes) if body_bytes else {};
|
|
2206
|
+
} except Exception {
|
|
2207
|
+
body_data = {};
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
walker_kwargs: dict[str, Any] = dict(body_data) if body_data else {};
|
|
2211
|
+
|
|
2212
|
+
# Execute the walker
|
|
2213
|
+
result = await self.execution_manager.spawn_walker(
|
|
2214
|
+
walkers[walker_name], walker_kwargs, username
|
|
2215
|
+
);
|
|
2216
|
+
|
|
2217
|
+
if 'error' in result {
|
|
2218
|
+
return TransportResponse.fail(
|
|
2219
|
+
code='EXECUTION_ERROR',
|
|
2220
|
+
message=result.get('error', 'Walker execution failed'),
|
|
2221
|
+
details=result.get('traceback') if 'traceback' in result else None,
|
|
2222
|
+
meta=Meta(extra={'http_status': 500})
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
return TransportResponse.success(
|
|
2227
|
+
data=result, meta=Meta(extra={'http_status': 200})
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
# Register dynamic webhook route
|
|
2231
|
+
self.server.add_endpoint(
|
|
2232
|
+
JEndPoint(
|
|
2233
|
+
method=HTTPMethod.POST,
|
|
2234
|
+
path='/webhook/{walker_name}',
|
|
2235
|
+
callback=dynamic_webhook_handler,
|
|
2236
|
+
parameters=[
|
|
2237
|
+
APIParameter(
|
|
2238
|
+
name='walker_name',
|
|
2239
|
+
data_type='string',
|
|
2240
|
+
required=True,
|
|
2241
|
+
default=None,
|
|
2242
|
+
description='Name of the webhook walker to execute',
|
|
2243
|
+
type=ParameterType.PATH
|
|
2244
|
+
),
|
|
2245
|
+
APIParameter(
|
|
2246
|
+
name='x_api_key',
|
|
2247
|
+
data_type='string',
|
|
2248
|
+
required=True,
|
|
2249
|
+
default=None,
|
|
2250
|
+
description='API key for webhook authentication (X-API-Key header)',
|
|
2251
|
+
type=ParameterType.HEADER
|
|
2252
|
+
),
|
|
2253
|
+
APIParameter(
|
|
2254
|
+
name='x_webhook_signature',
|
|
2255
|
+
data_type='string',
|
|
2256
|
+
required=False,
|
|
2257
|
+
default=None,
|
|
2258
|
+
description='HMAC-SHA256 signature of the request body (X-Webhook-Signature header)',
|
|
2259
|
+
type=ParameterType.HEADER
|
|
2260
|
+
)
|
|
2261
|
+
],
|
|
2262
|
+
response_model=None,
|
|
2263
|
+
tags=['Webhooks (Dynamic)'],
|
|
2264
|
+
summary='Execute webhook walker (dynamic HMR)',
|
|
2265
|
+
description='Dynamically routes to webhook walkers. Supports HMR - walker changes are reflected immediately.'
|
|
2266
|
+
)
|
|
2267
|
+
);
|
|
2268
|
+
}
|