jac-scale 0.1.3__py3-none-any.whl → 0.1.5__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/config_loader.jac +2 -1
- jac_scale/impl/config_loader.impl.jac +28 -3
- jac_scale/impl/context.impl.jac +1 -3
- jac_scale/impl/serve.impl.jac +679 -37
- jac_scale/impl/webhook.impl.jac +212 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +4 -0
- jac_scale/plugin_config.jac +1 -1
- jac_scale/serve.jac +30 -4
- jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac +8 -7
- jac_scale/tests/fixtures/scale-feats/components/Button.cl.jac +1 -1
- jac_scale/tests/fixtures/test_api.jac +60 -0
- jac_scale/tests/fixtures/test_restspec.jac +51 -0
- jac_scale/tests/test_restspec.py +97 -0
- jac_scale/tests/test_serve.py +357 -4
- jac_scale/webhook.jac +93 -0
- {jac_scale-0.1.3.dist-info → jac_scale-0.1.5.dist-info}/METADATA +4 -4
- {jac_scale-0.1.3.dist-info → jac_scale-0.1.5.dist-info}/RECORD +20 -18
- {jac_scale-0.1.3.dist-info → jac_scale-0.1.5.dist-info}/WHEEL +0 -0
- {jac_scale-0.1.3.dist-info → jac_scale-0.1.5.dist-info}/entry_points.txt +0 -0
- {jac_scale-0.1.3.dist-info → jac_scale-0.1.5.dist-info}/top_level.txt +0 -0
jac_scale/impl/serve.impl.jac
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import from jaclang.cli.console { console }
|
|
2
|
+
import from jaclang.cli.banners { JAC_DISCORD_URL }
|
|
3
|
+
|
|
1
4
|
"""Helper function to convert TransportResponse to dict for JSONResponse."""
|
|
2
5
|
def _transport_response_to_dict(
|
|
3
6
|
transport_response: TransportResponse
|
|
@@ -57,15 +60,14 @@ def _transport_response_to_json_response(
|
|
|
57
60
|
);
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
impl JacAPIServer.start(dev: bool = False) -> None {
|
|
63
|
+
impl JacAPIServer.start(dev: bool = False, no_client: bool = False) -> None {
|
|
61
64
|
self.introspector.load();
|
|
62
|
-
# Eagerly build client bundle if there are client exports (skip in dev mode)
|
|
63
|
-
if not dev {
|
|
65
|
+
# Eagerly build client bundle if there are client exports (skip in dev or no_client mode)
|
|
66
|
+
if not dev and not no_client {
|
|
64
67
|
client_exports = self.introspector._client_manifest.get('exports', []);
|
|
65
68
|
if client_exports {
|
|
66
69
|
import time;
|
|
67
70
|
import sys;
|
|
68
|
-
import from jaclang.cli.console { console }
|
|
69
71
|
start_time = time.time();
|
|
70
72
|
try {
|
|
71
73
|
with console.status(
|
|
@@ -84,6 +86,11 @@ impl JacAPIServer.start(dev: bool = False) -> None {
|
|
|
84
86
|
style="muted",
|
|
85
87
|
file=sys.stderr
|
|
86
88
|
);
|
|
89
|
+
console.info('Try again after running: jac clean --all', emoji=True);
|
|
90
|
+
console.info(
|
|
91
|
+
f'If it still doesn\'t work, ask for help at {JAC_DISCORD_URL}',
|
|
92
|
+
emoji=True
|
|
93
|
+
);
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
}
|
|
@@ -96,14 +103,17 @@ impl JacAPIServer.start(dev: bool = False) -> None {
|
|
|
96
103
|
self.register_static_file_endpoint();
|
|
97
104
|
self.register_update_username_endpoint();
|
|
98
105
|
self.register_update_password_endpoint();
|
|
106
|
+
self.register_api_key_endpoints();
|
|
99
107
|
# Use dynamic routing for HMR support, static routing for production
|
|
100
108
|
if dev {
|
|
101
109
|
self.register_dynamic_walker_endpoint();
|
|
102
110
|
self.register_dynamic_function_endpoint();
|
|
103
111
|
self.register_dynamic_introspection_endpoints();
|
|
112
|
+
self.register_dynamic_webhook_endpoint();
|
|
104
113
|
} else {
|
|
105
114
|
self.register_walkers_endpoints();
|
|
106
115
|
self.register_functions_endpoints();
|
|
116
|
+
self.register_webhook_endpoints();
|
|
107
117
|
}
|
|
108
118
|
self.register_root_asset_endpoint();
|
|
109
119
|
self._configure_openapi_security();
|
|
@@ -177,19 +187,35 @@ impl JacAPIServer._configure_openapi_security -> None {
|
|
|
177
187
|
self.server.app.openapi = custom_openapi;
|
|
178
188
|
}
|
|
179
189
|
|
|
180
|
-
"""Serve root-level assets like /img.png, /icons/logo.svg, etc.
|
|
190
|
+
"""Serve root-level assets like /img.png, /icons/logo.svg, etc.
|
|
191
|
+
Falls back to SPA HTML for extensionless paths when base_route_app is configured."""
|
|
181
192
|
impl JacAPIServer.serve_root_asset(file_path: str) -> Response {
|
|
182
193
|
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
194
|
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
195
|
import from jaclang.project.config { get_config }
|
|
188
196
|
config = get_config();
|
|
189
197
|
cl_route_prefix = config.serve.cl_route_prefix if config else "cl";
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
api_prefixes = (f'{cl_route_prefix}/', 'walker/', 'function/', 'user/', 'static/');
|
|
199
|
+
if (not file_ext or (file_ext not in allowed_extensions)) {
|
|
200
|
+
# SPA catch-all: serve base_route_app for extensionless paths
|
|
201
|
+
base_route_app = config.serve.base_route_app if config else "";
|
|
202
|
+
if (not file_ext and base_route_app and not file_path.startswith(api_prefixes)) {
|
|
203
|
+
try {
|
|
204
|
+
render_payload = self.introspector.render_page(
|
|
205
|
+
base_route_app, {}, '__guest__'
|
|
206
|
+
);
|
|
207
|
+
return HTMLResponse(content=render_payload['html']);
|
|
208
|
+
} except ValueError {
|
|
209
|
+
return HTMLResponse(content="<h1>404 Not Found</h1>", status_code=404);
|
|
210
|
+
} except RuntimeError {
|
|
211
|
+
return HTMLResponse(
|
|
212
|
+
content="<h1>503 Service Unavailable</h1>", status_code=503
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return Response(status_code=404, content='Not found', media_type='text/plain');
|
|
217
|
+
}
|
|
218
|
+
if file_path.startswith(api_prefixes) {
|
|
193
219
|
return Response(status_code=404, content='Not found', media_type='text/plain');
|
|
194
220
|
}
|
|
195
221
|
# Find project root (where jac.toml is) instead of using base_path_dir
|
|
@@ -452,7 +478,7 @@ impl JacAPIServer.render_page_callback -> Callable[..., HTMLResponse] {
|
|
|
452
478
|
} except ValueError as exc {
|
|
453
479
|
return HTMLResponse(content=f"<h1>404 Not Found</h1>", status_code=404);
|
|
454
480
|
} except RuntimeError as exc {
|
|
455
|
-
print(f"Error rendering page '{page_name}': {exc}");
|
|
481
|
+
console.print(f"Error rendering page '{page_name}': {exc}");
|
|
456
482
|
return HTMLResponse(
|
|
457
483
|
content=f"<h1>503 Service Unavailable</h1>", status_code=503
|
|
458
484
|
);
|
|
@@ -475,7 +501,7 @@ impl JacAPIServer.render_base_route_callback(
|
|
|
475
501
|
} except ValueError as exc {
|
|
476
502
|
return HTMLResponse(content=f"<h1>404 Not Found</h1>", status_code=404);
|
|
477
503
|
} except RuntimeError as exc {
|
|
478
|
-
print(f"Error rendering base route app '{app_name}': {exc}");
|
|
504
|
+
console.print(f"Error rendering base route app '{app_name}': {exc}");
|
|
479
505
|
return HTMLResponse(
|
|
480
506
|
content=f"<h1>503 Service Unavailable</h1>", status_code=503
|
|
481
507
|
);
|
|
@@ -498,7 +524,9 @@ impl JacAPIServer.register_functions_endpoints -> None {
|
|
|
498
524
|
method=spec_method,
|
|
499
525
|
path=final_path,
|
|
500
526
|
callback=self.create_function_callback(func_name),
|
|
501
|
-
parameters=self.create_function_parameters(
|
|
527
|
+
parameters=self.create_function_parameters(
|
|
528
|
+
func_name, method=spec_method
|
|
529
|
+
),
|
|
502
530
|
response_model=None,
|
|
503
531
|
tags=['Functions'],
|
|
504
532
|
summary='This is a summary',
|
|
@@ -508,7 +536,9 @@ impl JacAPIServer.register_functions_endpoints -> None {
|
|
|
508
536
|
}
|
|
509
537
|
}
|
|
510
538
|
|
|
511
|
-
impl JacAPIServer.create_function_parameters(
|
|
539
|
+
impl JacAPIServer.create_function_parameters(
|
|
540
|
+
func_name: str, method: HTTPMethod = HTTPMethod.POST
|
|
541
|
+
) -> list[APIParameter] {
|
|
512
542
|
parameters: list[APIParameter] = [];
|
|
513
543
|
if self.introspector.is_auth_required_for_function(func_name) {
|
|
514
544
|
parameters.append(
|
|
@@ -527,8 +557,13 @@ impl JacAPIServer.create_function_parameters(func_name: str) -> list[APIParamete
|
|
|
527
557
|
)['parameters'];
|
|
528
558
|
for field_name in func_fields {
|
|
529
559
|
field_type = func_fields[field_name]['type'];
|
|
530
|
-
# Determine parameter type based on field type
|
|
531
|
-
if (
|
|
560
|
+
# Determine parameter type based on field type and method
|
|
561
|
+
if (
|
|
562
|
+
method == HTTPMethod.GET
|
|
563
|
+
and not ('UploadFile' in field_type or 'uploadfile' in field_type.lower())
|
|
564
|
+
) {
|
|
565
|
+
param_type = ParameterType.QUERY;
|
|
566
|
+
} elif ('UploadFile' in field_type or 'uploadfile' in field_type.lower()) {
|
|
532
567
|
# Support UploadFile type for file uploads
|
|
533
568
|
param_type = ParameterType.FILE;
|
|
534
569
|
} else {
|
|
@@ -574,7 +609,7 @@ impl JacAPIServer.create_function_callback(
|
|
|
574
609
|
);
|
|
575
610
|
}
|
|
576
611
|
}
|
|
577
|
-
print(f"Executing function '{func_name}' with params: {kwargs}");
|
|
612
|
+
console.print(f"Executing function '{func_name}' with params: {kwargs}");
|
|
578
613
|
result = await self.execution_manager.execute_function(
|
|
579
614
|
self.get_functions()[func_name], kwargs, (username or '__guest__')
|
|
580
615
|
);
|
|
@@ -607,6 +642,11 @@ impl JacAPIServer.create_function_callback(
|
|
|
607
642
|
|
|
608
643
|
impl JacAPIServer.register_walkers_endpoints -> None {
|
|
609
644
|
for walker_name in self.get_walkers() {
|
|
645
|
+
# Skip walkers configured for webhook transport - they use /webhook/{walker_name} instead
|
|
646
|
+
transport_type = self.get_transport_type_for_walker(walker_name);
|
|
647
|
+
if transport_type == TransportType.WEBHOOK {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
610
650
|
walker_cls = self.get_walkers()[walker_name];
|
|
611
651
|
restspec = walker_cls.restspec if walker_cls?.restspec else None;
|
|
612
652
|
spec_method = restspec.method if restspec?.method else HTTPMethod.POST;
|
|
@@ -618,7 +658,7 @@ impl JacAPIServer.register_walkers_endpoints -> None {
|
|
|
618
658
|
path=f"{spec_path}/{{node}}",
|
|
619
659
|
callback=self.create_walker_callback(walker_name, has_node_param=True),
|
|
620
660
|
parameters=self.create_walker_parameters(
|
|
621
|
-
walker_name, invoke_on_root=False
|
|
661
|
+
walker_name, invoke_on_root=False, method=spec_method
|
|
622
662
|
),
|
|
623
663
|
response_model=None,
|
|
624
664
|
tags=['Walkers'],
|
|
@@ -632,7 +672,7 @@ impl JacAPIServer.register_walkers_endpoints -> None {
|
|
|
632
672
|
path=spec_path,
|
|
633
673
|
callback=self.create_walker_callback(walker_name, has_node_param=False),
|
|
634
674
|
parameters=self.create_walker_parameters(
|
|
635
|
-
walker_name, invoke_on_root=True
|
|
675
|
+
walker_name, invoke_on_root=True, method=spec_method
|
|
636
676
|
),
|
|
637
677
|
response_model=None,
|
|
638
678
|
tags=['Walkers'],
|
|
@@ -644,7 +684,7 @@ impl JacAPIServer.register_walkers_endpoints -> None {
|
|
|
644
684
|
}
|
|
645
685
|
|
|
646
686
|
impl JacAPIServer.create_walker_parameters(
|
|
647
|
-
walker_name: str, invoke_on_root: bool
|
|
687
|
+
walker_name: str, invoke_on_root: bool, method: HTTPMethod = HTTPMethod.POST
|
|
648
688
|
) -> list[APIParameter] {
|
|
649
689
|
parameters: list[APIParameter] = [];
|
|
650
690
|
if self.introspector.is_auth_required_for_walker(walker_name) {
|
|
@@ -667,9 +707,14 @@ impl JacAPIServer.create_walker_parameters(
|
|
|
667
707
|
continue;
|
|
668
708
|
}
|
|
669
709
|
field_type = walker_fields[field_name]['type'];
|
|
670
|
-
# Determine parameter type based on field type
|
|
710
|
+
# Determine parameter type based on field type and method
|
|
671
711
|
if (field_name == '_jac_spawn_node') {
|
|
672
712
|
param_type = ParameterType.PATH;
|
|
713
|
+
} elif (
|
|
714
|
+
method == HTTPMethod.GET
|
|
715
|
+
and not ('UploadFile' in field_type or 'uploadfile' in field_type.lower())
|
|
716
|
+
) {
|
|
717
|
+
param_type = ParameterType.QUERY;
|
|
673
718
|
} elif ('UploadFile' in field_type or 'uploadfile' in field_type.lower()) {
|
|
674
719
|
# Support UploadFile type for file uploads
|
|
675
720
|
param_type = ParameterType.FILE;
|
|
@@ -850,7 +895,7 @@ impl JacAPIServer.create_user(username: str, password: str) -> TransportResponse
|
|
|
850
895
|
);
|
|
851
896
|
} except Exception as e {
|
|
852
897
|
error_trace = traceback.format_exc();
|
|
853
|
-
print(f"Error in create_user: {e}\n{error_trace}");
|
|
898
|
+
console.print(f"Error in create_user: {e}\n{error_trace}");
|
|
854
899
|
return TransportResponse.fail(
|
|
855
900
|
code='INTERNAL_ERROR',
|
|
856
901
|
message=f"Registration failed: {e}",
|
|
@@ -1109,6 +1154,7 @@ impl JacAPIServer.login(username: str, password: str) -> TransportResponse {
|
|
|
1109
1154
|
|
|
1110
1155
|
impl JacAPIServer.postinit -> None {
|
|
1111
1156
|
super.postinit();
|
|
1157
|
+
self._api_key_manager = ApiKeyManager();
|
|
1112
1158
|
self.server.app.add_middleware(
|
|
1113
1159
|
CORSMiddleware,
|
|
1114
1160
|
allow_origins=['*'],
|
|
@@ -1239,13 +1285,17 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1239
1285
|
node: str | None = None,
|
|
1240
1286
|
Authorization: str | None = None
|
|
1241
1287
|
) -> TransportResponse {
|
|
1242
|
-
# Parse request body to get walker fields
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1288
|
+
# Parse request body or query params to get walker fields
|
|
1289
|
+
if request.method == 'GET' {
|
|
1290
|
+
kwargs: dict[str, Any] = dict(request.query_params);
|
|
1291
|
+
} else {
|
|
1292
|
+
try {
|
|
1293
|
+
body = await request.json();
|
|
1294
|
+
} except Exception {
|
|
1295
|
+
body = {};
|
|
1296
|
+
}
|
|
1297
|
+
kwargs = dict(body) if body else {};
|
|
1247
1298
|
}
|
|
1248
|
-
kwargs: dict[str, Any] = dict(body) if body else {};
|
|
1249
1299
|
|
|
1250
1300
|
# Reload introspector if files changed (HMR)
|
|
1251
1301
|
if self._hmr_pending {
|
|
@@ -1265,6 +1315,16 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1265
1315
|
);
|
|
1266
1316
|
}
|
|
1267
1317
|
|
|
1318
|
+
# Reject walkers configured for webhook transport - they use /webhook/{walker_name} instead
|
|
1319
|
+
transport_type = self.get_transport_type_for_walker(walker_name);
|
|
1320
|
+
if transport_type == TransportType.WEBHOOK {
|
|
1321
|
+
return TransportResponse.fail(
|
|
1322
|
+
code='BAD_REQUEST',
|
|
1323
|
+
message=f"Walker '{walker_name}' is configured as a webhook. Use /webhook/{walker_name} instead.",
|
|
1324
|
+
meta=Meta(extra={'http_status': 400})
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1268
1328
|
# Handle authentication
|
|
1269
1329
|
username: str | None = None;
|
|
1270
1330
|
authorization = kwargs.pop('Authorization', None);
|
|
@@ -1321,7 +1381,7 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1321
1381
|
data=result, meta=Meta(extra={'http_status': 200})
|
|
1322
1382
|
);
|
|
1323
1383
|
}
|
|
1324
|
-
# Register catch-all route for walkers with node parameter
|
|
1384
|
+
# Register catch-all route for walkers with node parameter (POST)
|
|
1325
1385
|
self.server.add_endpoint(
|
|
1326
1386
|
JEndPoint(
|
|
1327
1387
|
method=HTTPMethod.POST,
|
|
@@ -1359,7 +1419,45 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1359
1419
|
description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
|
|
1360
1420
|
)
|
|
1361
1421
|
);
|
|
1362
|
-
# Register catch-all route for walkers
|
|
1422
|
+
# Register catch-all route for walkers with node parameter (GET)
|
|
1423
|
+
self.server.add_endpoint(
|
|
1424
|
+
JEndPoint(
|
|
1425
|
+
method=HTTPMethod.GET,
|
|
1426
|
+
path='/walker/{walker_name}/{node}',
|
|
1427
|
+
callback=dynamic_walker_handler,
|
|
1428
|
+
parameters=[
|
|
1429
|
+
APIParameter(
|
|
1430
|
+
name='walker_name',
|
|
1431
|
+
data_type='string',
|
|
1432
|
+
required=True,
|
|
1433
|
+
default=None,
|
|
1434
|
+
description='Name of the walker to execute',
|
|
1435
|
+
type=ParameterType.PATH
|
|
1436
|
+
),
|
|
1437
|
+
APIParameter(
|
|
1438
|
+
name='node',
|
|
1439
|
+
data_type='string',
|
|
1440
|
+
required=True,
|
|
1441
|
+
default=None,
|
|
1442
|
+
description='Node ID to spawn walker on',
|
|
1443
|
+
type=ParameterType.PATH
|
|
1444
|
+
),
|
|
1445
|
+
APIParameter(
|
|
1446
|
+
name='Authorization',
|
|
1447
|
+
data_type='string',
|
|
1448
|
+
required=False,
|
|
1449
|
+
default=None,
|
|
1450
|
+
description='Bearer token for authentication',
|
|
1451
|
+
type=ParameterType.HEADER
|
|
1452
|
+
)
|
|
1453
|
+
],
|
|
1454
|
+
response_model=None,
|
|
1455
|
+
tags=['Walkers (Dynamic)'],
|
|
1456
|
+
summary='Execute walker on node (dynamic HMR)',
|
|
1457
|
+
description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
|
|
1458
|
+
)
|
|
1459
|
+
);
|
|
1460
|
+
# Register catch-all route for walkers without node parameter (root) (POST)
|
|
1363
1461
|
self.server.add_endpoint(
|
|
1364
1462
|
JEndPoint(
|
|
1365
1463
|
method=HTTPMethod.POST,
|
|
@@ -1389,6 +1487,36 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
|
|
|
1389
1487
|
description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
|
|
1390
1488
|
)
|
|
1391
1489
|
);
|
|
1490
|
+
# Register catch-all route for walkers without node parameter (root) (GET)
|
|
1491
|
+
self.server.add_endpoint(
|
|
1492
|
+
JEndPoint(
|
|
1493
|
+
method=HTTPMethod.GET,
|
|
1494
|
+
path='/walker/{walker_name}',
|
|
1495
|
+
callback=dynamic_walker_handler,
|
|
1496
|
+
parameters=[
|
|
1497
|
+
APIParameter(
|
|
1498
|
+
name='walker_name',
|
|
1499
|
+
data_type='string',
|
|
1500
|
+
required=True,
|
|
1501
|
+
default=None,
|
|
1502
|
+
description='Name of the walker to execute',
|
|
1503
|
+
type=ParameterType.PATH
|
|
1504
|
+
),
|
|
1505
|
+
APIParameter(
|
|
1506
|
+
name='Authorization',
|
|
1507
|
+
data_type='string',
|
|
1508
|
+
required=False,
|
|
1509
|
+
default=None,
|
|
1510
|
+
description='Bearer token for authentication',
|
|
1511
|
+
type=ParameterType.HEADER
|
|
1512
|
+
)
|
|
1513
|
+
],
|
|
1514
|
+
response_model=None,
|
|
1515
|
+
tags=['Walkers (Dynamic)'],
|
|
1516
|
+
summary='Execute walker on root (dynamic HMR)',
|
|
1517
|
+
description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
|
|
1518
|
+
)
|
|
1519
|
+
);
|
|
1392
1520
|
}
|
|
1393
1521
|
|
|
1394
1522
|
"""Register a single dynamic endpoint for all functions.
|
|
@@ -1402,13 +1530,17 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1402
1530
|
async def dynamic_function_handler(
|
|
1403
1531
|
request: Request, function_name: str, Authorization: str | None = None
|
|
1404
1532
|
) -> TransportResponse {
|
|
1405
|
-
# Parse request body to get function arguments
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1533
|
+
# Parse request body or query params to get function arguments
|
|
1534
|
+
if request.method == 'GET' {
|
|
1535
|
+
kwargs: dict[str, Any] = dict(request.query_params);
|
|
1536
|
+
} else {
|
|
1537
|
+
try {
|
|
1538
|
+
body = await request.json();
|
|
1539
|
+
} except Exception {
|
|
1540
|
+
body = {};
|
|
1541
|
+
}
|
|
1542
|
+
kwargs = dict(body) if body else {};
|
|
1410
1543
|
}
|
|
1411
|
-
kwargs: dict[str, Any] = dict(body) if body else {};
|
|
1412
1544
|
|
|
1413
1545
|
# Reload introspector if files changed (HMR)
|
|
1414
1546
|
if self._hmr_pending {
|
|
@@ -1477,6 +1609,7 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1477
1609
|
data=result, meta=Meta(extra={'http_status': 200})
|
|
1478
1610
|
);
|
|
1479
1611
|
}
|
|
1612
|
+
# Register POST endpoint
|
|
1480
1613
|
self.server.add_endpoint(
|
|
1481
1614
|
JEndPoint(
|
|
1482
1615
|
method=HTTPMethod.POST,
|
|
@@ -1506,6 +1639,36 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
|
|
|
1506
1639
|
description='Dynamically routes to any registered function. Supports HMR - function changes are reflected immediately.'
|
|
1507
1640
|
)
|
|
1508
1641
|
);
|
|
1642
|
+
# Register GET endpoint
|
|
1643
|
+
self.server.add_endpoint(
|
|
1644
|
+
JEndPoint(
|
|
1645
|
+
method=HTTPMethod.GET,
|
|
1646
|
+
path='/function/{function_name}',
|
|
1647
|
+
callback=dynamic_function_handler,
|
|
1648
|
+
parameters=[
|
|
1649
|
+
APIParameter(
|
|
1650
|
+
name='function_name',
|
|
1651
|
+
data_type='string',
|
|
1652
|
+
required=True,
|
|
1653
|
+
default=None,
|
|
1654
|
+
description='Name of the function to call',
|
|
1655
|
+
type=ParameterType.PATH
|
|
1656
|
+
),
|
|
1657
|
+
APIParameter(
|
|
1658
|
+
name='Authorization',
|
|
1659
|
+
data_type='string',
|
|
1660
|
+
required=False,
|
|
1661
|
+
default=None,
|
|
1662
|
+
description='Bearer token for authentication',
|
|
1663
|
+
type=ParameterType.HEADER
|
|
1664
|
+
)
|
|
1665
|
+
],
|
|
1666
|
+
response_model=None,
|
|
1667
|
+
tags=['Functions (Dynamic)'],
|
|
1668
|
+
summary='Call function (dynamic HMR)',
|
|
1669
|
+
description='Dynamically routes to any registered function. Supports HMR - function changes are reflected immediately.'
|
|
1670
|
+
)
|
|
1671
|
+
);
|
|
1509
1672
|
}
|
|
1510
1673
|
|
|
1511
1674
|
"""Register endpoints for runtime introspection of available walkers/functions.
|
|
@@ -1631,3 +1794,482 @@ impl JacAPIServer.register_dynamic_introspection_endpoints -> None {
|
|
|
1631
1794
|
)
|
|
1632
1795
|
);
|
|
1633
1796
|
}
|
|
1797
|
+
|
|
1798
|
+
"""Get or create the API key manager instance."""
|
|
1799
|
+
impl JacAPIServer.get_api_key_manager -> ApiKeyManager {
|
|
1800
|
+
if self._api_key_manager is None {
|
|
1801
|
+
self._api_key_manager = ApiKeyManager();
|
|
1802
|
+
}
|
|
1803
|
+
return self._api_key_manager;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
"""Create a new API key for the authenticated user."""
|
|
1807
|
+
impl JacAPIServer.create_api_key(
|
|
1808
|
+
name: str, expiry_days: int | None = None, Authorization: str | None = None
|
|
1809
|
+
) -> TransportResponse {
|
|
1810
|
+
# Validate JWT token to get username
|
|
1811
|
+
token: str | None = None;
|
|
1812
|
+
if Authorization and Authorization.startswith('Bearer ') {
|
|
1813
|
+
token = Authorization[7:];
|
|
1814
|
+
}
|
|
1815
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1816
|
+
if not username {
|
|
1817
|
+
return TransportResponse.fail(
|
|
1818
|
+
code='UNAUTHORIZED',
|
|
1819
|
+
message='Valid authentication required to create API keys',
|
|
1820
|
+
meta=Meta(extra={'http_status': 401})
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
return self.get_api_key_manager().create_api_key(
|
|
1824
|
+
username=username, name=name, expiry_days=expiry_days
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
"""List all API keys for the authenticated user."""
|
|
1829
|
+
impl JacAPIServer.list_api_keys(Authorization: str | None = None) -> TransportResponse {
|
|
1830
|
+
# Validate JWT token to get username
|
|
1831
|
+
token: str | None = None;
|
|
1832
|
+
if Authorization and Authorization.startswith('Bearer ') {
|
|
1833
|
+
token = Authorization[7:];
|
|
1834
|
+
}
|
|
1835
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1836
|
+
if not username {
|
|
1837
|
+
return TransportResponse.fail(
|
|
1838
|
+
code='UNAUTHORIZED',
|
|
1839
|
+
message='Valid authentication required to list API keys',
|
|
1840
|
+
meta=Meta(extra={'http_status': 401})
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
return self.get_api_key_manager().list_api_keys(username);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
"""Revoke an API key for the authenticated user."""
|
|
1847
|
+
impl JacAPIServer.revoke_api_key(
|
|
1848
|
+
api_key_id: str, Authorization: str | None = None
|
|
1849
|
+
) -> TransportResponse {
|
|
1850
|
+
# Validate JWT token to get username
|
|
1851
|
+
token: str | None = None;
|
|
1852
|
+
if Authorization and Authorization.startswith('Bearer ') {
|
|
1853
|
+
token = Authorization[7:];
|
|
1854
|
+
}
|
|
1855
|
+
username = self.user_manager.validate_jwt_token(token) if token else None;
|
|
1856
|
+
if not username {
|
|
1857
|
+
return TransportResponse.fail(
|
|
1858
|
+
code='UNAUTHORIZED',
|
|
1859
|
+
message='Valid authentication required to revoke API keys',
|
|
1860
|
+
meta=Meta(extra={'http_status': 401})
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
return self.get_api_key_manager().revoke_api_key(username, api_key_id);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
"""Register API key management endpoints."""
|
|
1867
|
+
impl JacAPIServer.register_api_key_endpoints -> None {
|
|
1868
|
+
# Create API key
|
|
1869
|
+
self.server.add_endpoint(
|
|
1870
|
+
JEndPoint(
|
|
1871
|
+
method=HTTPMethod.POST,
|
|
1872
|
+
path='/api-key/create',
|
|
1873
|
+
callback=self.create_api_key,
|
|
1874
|
+
parameters=[
|
|
1875
|
+
APIParameter(
|
|
1876
|
+
name='name',
|
|
1877
|
+
data_type='string',
|
|
1878
|
+
required=True,
|
|
1879
|
+
default=None,
|
|
1880
|
+
description='A friendly name for the API key',
|
|
1881
|
+
type=ParameterType.BODY
|
|
1882
|
+
),
|
|
1883
|
+
APIParameter(
|
|
1884
|
+
name='expiry_days',
|
|
1885
|
+
data_type='integer',
|
|
1886
|
+
required=False,
|
|
1887
|
+
default=None,
|
|
1888
|
+
description='Number of days until expiry (default from config)',
|
|
1889
|
+
type=ParameterType.BODY
|
|
1890
|
+
),
|
|
1891
|
+
APIParameter(
|
|
1892
|
+
name='Authorization',
|
|
1893
|
+
data_type='string',
|
|
1894
|
+
required=True,
|
|
1895
|
+
default=None,
|
|
1896
|
+
description='Bearer token for authentication',
|
|
1897
|
+
type=ParameterType.HEADER
|
|
1898
|
+
)
|
|
1899
|
+
],
|
|
1900
|
+
response_model=None,
|
|
1901
|
+
tags=['API Keys'],
|
|
1902
|
+
summary='Create a new API key',
|
|
1903
|
+
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.'
|
|
1904
|
+
)
|
|
1905
|
+
);
|
|
1906
|
+
# List API keys
|
|
1907
|
+
self.server.add_endpoint(
|
|
1908
|
+
JEndPoint(
|
|
1909
|
+
method=HTTPMethod.GET,
|
|
1910
|
+
path='/api-key/list',
|
|
1911
|
+
callback=self.list_api_keys,
|
|
1912
|
+
parameters=[
|
|
1913
|
+
APIParameter(
|
|
1914
|
+
name='Authorization',
|
|
1915
|
+
data_type='string',
|
|
1916
|
+
required=True,
|
|
1917
|
+
default=None,
|
|
1918
|
+
description='Bearer token for authentication',
|
|
1919
|
+
type=ParameterType.HEADER
|
|
1920
|
+
)
|
|
1921
|
+
],
|
|
1922
|
+
response_model=None,
|
|
1923
|
+
tags=['API Keys'],
|
|
1924
|
+
summary='List all API keys',
|
|
1925
|
+
description='Lists all API keys for the authenticated user (metadata only, not the actual keys).'
|
|
1926
|
+
)
|
|
1927
|
+
);
|
|
1928
|
+
# Revoke API key
|
|
1929
|
+
self.server.add_endpoint(
|
|
1930
|
+
JEndPoint(
|
|
1931
|
+
method=HTTPMethod.DELETE,
|
|
1932
|
+
path='/api-key/{api_key_id}',
|
|
1933
|
+
callback=self.revoke_api_key,
|
|
1934
|
+
parameters=[
|
|
1935
|
+
APIParameter(
|
|
1936
|
+
name='api_key_id',
|
|
1937
|
+
data_type='string',
|
|
1938
|
+
required=True,
|
|
1939
|
+
default=None,
|
|
1940
|
+
description='The ID of the API key to revoke',
|
|
1941
|
+
type=ParameterType.PATH
|
|
1942
|
+
),
|
|
1943
|
+
APIParameter(
|
|
1944
|
+
name='Authorization',
|
|
1945
|
+
data_type='string',
|
|
1946
|
+
required=True,
|
|
1947
|
+
default=None,
|
|
1948
|
+
description='Bearer token for authentication',
|
|
1949
|
+
type=ParameterType.HEADER
|
|
1950
|
+
)
|
|
1951
|
+
],
|
|
1952
|
+
response_model=None,
|
|
1953
|
+
tags=['API Keys'],
|
|
1954
|
+
summary='Revoke an API key',
|
|
1955
|
+
description='Revokes an API key, making it invalid for future webhook requests.'
|
|
1956
|
+
)
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
"""Get the transport type for a walker by checking its restspec."""
|
|
1961
|
+
impl JacAPIServer.get_transport_type_for_walker(walker_name: str) -> str {
|
|
1962
|
+
walkers = self.get_walkers();
|
|
1963
|
+
if walker_name not in walkers {
|
|
1964
|
+
return TransportType.HTTP;
|
|
1965
|
+
}
|
|
1966
|
+
walker_cls = walkers[walker_name];
|
|
1967
|
+
restspec = walker_cls.restspec if walker_cls?.restspec else None;
|
|
1968
|
+
if restspec?.webhook {
|
|
1969
|
+
return TransportType.WEBHOOK;
|
|
1970
|
+
}
|
|
1971
|
+
return TransportType.HTTP;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
"""Create webhook callback for a walker with HMAC-SHA256 signature verification."""
|
|
1975
|
+
impl JacAPIServer.create_webhook_callback(
|
|
1976
|
+
walker_name: str
|
|
1977
|
+
) -> Callable[..., TransportResponse] {
|
|
1978
|
+
async def callback(request: Request, **kwargs: JsonValue) -> TransportResponse {
|
|
1979
|
+
webhook_config = get_scale_config().get_webhook_config();
|
|
1980
|
+
signature_header = webhook_config.get(
|
|
1981
|
+
'signature_header', 'X-Webhook-Signature'
|
|
1982
|
+
);
|
|
1983
|
+
verify_signature = webhook_config.get('verify_signature', True);
|
|
1984
|
+
|
|
1985
|
+
# Get API key from header
|
|
1986
|
+
api_key = request.headers.get('X-API-Key');
|
|
1987
|
+
if not api_key {
|
|
1988
|
+
return TransportResponse.fail(
|
|
1989
|
+
code='UNAUTHORIZED',
|
|
1990
|
+
message='Missing X-API-Key header',
|
|
1991
|
+
meta=Meta(extra={'http_status': 401})
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
# Validate API key and get username
|
|
1996
|
+
username = self.get_api_key_manager().validate_api_key(api_key);
|
|
1997
|
+
if not username {
|
|
1998
|
+
return TransportResponse.fail(
|
|
1999
|
+
code='UNAUTHORIZED',
|
|
2000
|
+
message='Invalid or expired API key',
|
|
2001
|
+
meta=Meta(extra={'http_status': 401})
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
# Verify HMAC-SHA256 signature if enabled
|
|
2006
|
+
if verify_signature {
|
|
2007
|
+
signature = request.headers.get(signature_header);
|
|
2008
|
+
if not signature {
|
|
2009
|
+
return TransportResponse.fail(
|
|
2010
|
+
code='UNAUTHORIZED',
|
|
2011
|
+
message=f"Missing {signature_header} header",
|
|
2012
|
+
meta=Meta(extra={'http_status': 401})
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
# Get raw body for signature verification
|
|
2016
|
+
body = await request.body();
|
|
2017
|
+
extracted_signature = WebhookUtils.extract_signature(signature);
|
|
2018
|
+
# Use the API key as the secret for HMAC verification
|
|
2019
|
+
if not WebhookUtils.verify_signature(body, extracted_signature, api_key) {
|
|
2020
|
+
return TransportResponse.fail(
|
|
2021
|
+
code='UNAUTHORIZED',
|
|
2022
|
+
message='Invalid webhook signature',
|
|
2023
|
+
meta=Meta(extra={'http_status': 401})
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
# Parse body for walker fields
|
|
2029
|
+
try {
|
|
2030
|
+
body_bytes = await request.body();
|
|
2031
|
+
body_data = json.loads(body_bytes) if body_bytes else {};
|
|
2032
|
+
} except Exception {
|
|
2033
|
+
body_data = {};
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
walker_kwargs: dict[str, Any] = dict(body_data) if body_data else {};
|
|
2037
|
+
|
|
2038
|
+
# Execute the walker
|
|
2039
|
+
result = await self.execution_manager.spawn_walker(
|
|
2040
|
+
self.get_walkers()[walker_name], walker_kwargs, username
|
|
2041
|
+
);
|
|
2042
|
+
|
|
2043
|
+
if 'error' in result {
|
|
2044
|
+
return TransportResponse.fail(
|
|
2045
|
+
code='EXECUTION_ERROR',
|
|
2046
|
+
message=result.get('error', 'Walker execution failed'),
|
|
2047
|
+
details=result.get('traceback') if 'traceback' in result else None,
|
|
2048
|
+
meta=Meta(extra={'http_status': 500})
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
return TransportResponse.success(
|
|
2053
|
+
data=result, meta=Meta(extra={'http_status': 200})
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
return callback;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
"""Create parameters for webhook endpoint."""
|
|
2060
|
+
impl JacAPIServer.create_webhook_parameters(walker_name: str) -> list[APIParameter] {
|
|
2061
|
+
parameters: list[APIParameter] = [];
|
|
2062
|
+
# API key header (required for webhooks)
|
|
2063
|
+
# Use underscore naming for valid Python identifiers - FastAPI auto-converts to hyphenated headers
|
|
2064
|
+
parameters.append(
|
|
2065
|
+
APIParameter(
|
|
2066
|
+
name='x_api_key',
|
|
2067
|
+
data_type='string',
|
|
2068
|
+
required=True,
|
|
2069
|
+
default=None,
|
|
2070
|
+
description='API key for webhook authentication (X-API-Key header)',
|
|
2071
|
+
type=ParameterType.HEADER
|
|
2072
|
+
)
|
|
2073
|
+
);
|
|
2074
|
+
# Signature header (for HMAC verification)
|
|
2075
|
+
# Use underscore naming for valid Python identifiers
|
|
2076
|
+
parameters.append(
|
|
2077
|
+
APIParameter(
|
|
2078
|
+
name='x_webhook_signature',
|
|
2079
|
+
data_type='string',
|
|
2080
|
+
required=False,
|
|
2081
|
+
default=None,
|
|
2082
|
+
description='HMAC-SHA256 signature of the request body (X-Webhook-Signature header)',
|
|
2083
|
+
type=ParameterType.HEADER
|
|
2084
|
+
)
|
|
2085
|
+
);
|
|
2086
|
+
# Add walker fields as body parameters (excluding transport_type)
|
|
2087
|
+
walker_fields = self.introspector.introspect_walker(
|
|
2088
|
+
self.get_walkers()[walker_name]
|
|
2089
|
+
)['fields'];
|
|
2090
|
+
for field_name in walker_fields {
|
|
2091
|
+
if field_name in ('_jac_spawn_node', ) {
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
field_type = walker_fields[field_name]['type'];
|
|
2095
|
+
parameters.append(
|
|
2096
|
+
APIParameter(
|
|
2097
|
+
name=field_name,
|
|
2098
|
+
data_type=field_type,
|
|
2099
|
+
required=walker_fields[field_name]['required'],
|
|
2100
|
+
default=walker_fields[field_name]['default'],
|
|
2101
|
+
description=f"Field {field_name} for webhook walker {walker_name}",
|
|
2102
|
+
type=ParameterType.BODY
|
|
2103
|
+
)
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
return parameters;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
"""Register webhook endpoints for walkers with transport_type=WEBHOOK."""
|
|
2110
|
+
impl JacAPIServer.register_webhook_endpoints -> None {
|
|
2111
|
+
for walker_name in self.get_walkers() {
|
|
2112
|
+
transport_type = self.get_transport_type_for_walker(walker_name);
|
|
2113
|
+
|
|
2114
|
+
if transport_type == TransportType.WEBHOOK {
|
|
2115
|
+
self.server.add_endpoint(
|
|
2116
|
+
JEndPoint(
|
|
2117
|
+
method=HTTPMethod.POST,
|
|
2118
|
+
path=f"/webhook/{walker_name}",
|
|
2119
|
+
callback=self.create_webhook_callback(walker_name),
|
|
2120
|
+
parameters=self.create_webhook_parameters(walker_name),
|
|
2121
|
+
response_model=None,
|
|
2122
|
+
tags=['Webhooks'],
|
|
2123
|
+
summary=f'Webhook endpoint for {walker_name}',
|
|
2124
|
+
description=f'Webhook endpoint for {walker_name}. Requires API key authentication and HMAC-SHA256 signature verification.'
|
|
2125
|
+
)
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
"""Register dynamic webhook endpoint for HMR support."""
|
|
2132
|
+
impl JacAPIServer.register_dynamic_webhook_endpoint -> None {
|
|
2133
|
+
async def dynamic_webhook_handler(
|
|
2134
|
+
request: Request, walker_name: str
|
|
2135
|
+
) -> TransportResponse {
|
|
2136
|
+
# Reload introspector if files changed (HMR)
|
|
2137
|
+
if self._hmr_pending {
|
|
2138
|
+
self.introspector.load(force_reload=True);
|
|
2139
|
+
self._hmr_pending = False;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
walkers = self.get_walkers();
|
|
2143
|
+
|
|
2144
|
+
if walker_name not in walkers {
|
|
2145
|
+
return TransportResponse.fail(
|
|
2146
|
+
code='NOT_FOUND',
|
|
2147
|
+
message=f"Webhook walker '{walker_name}' not found",
|
|
2148
|
+
meta=Meta(extra={'http_status': 404})
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
# Verify this walker is configured for webhook transport
|
|
2153
|
+
transport_type = self.get_transport_type_for_walker(walker_name);
|
|
2154
|
+
if transport_type != TransportType.WEBHOOK {
|
|
2155
|
+
return TransportResponse.fail(
|
|
2156
|
+
code='BAD_REQUEST',
|
|
2157
|
+
message=f"Walker '{walker_name}' is not configured as a webhook. Use /walker/{walker_name} instead.",
|
|
2158
|
+
meta=Meta(extra={'http_status': 400})
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
webhook_config = get_scale_config().get_webhook_config();
|
|
2163
|
+
signature_header = webhook_config.get(
|
|
2164
|
+
'signature_header', 'X-Webhook-Signature'
|
|
2165
|
+
);
|
|
2166
|
+
verify_signature = webhook_config.get('verify_signature', True);
|
|
2167
|
+
|
|
2168
|
+
# Get API key from header
|
|
2169
|
+
api_key = request.headers.get('X-API-Key');
|
|
2170
|
+
if not api_key {
|
|
2171
|
+
return TransportResponse.fail(
|
|
2172
|
+
code='UNAUTHORIZED',
|
|
2173
|
+
message='Missing X-API-Key header',
|
|
2174
|
+
meta=Meta(extra={'http_status': 401})
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
# Validate API key and get username
|
|
2179
|
+
username = self.get_api_key_manager().validate_api_key(api_key);
|
|
2180
|
+
if not username {
|
|
2181
|
+
return TransportResponse.fail(
|
|
2182
|
+
code='UNAUTHORIZED',
|
|
2183
|
+
message='Invalid or expired API key',
|
|
2184
|
+
meta=Meta(extra={'http_status': 401})
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
# Verify HMAC-SHA256 signature if enabled
|
|
2189
|
+
if verify_signature {
|
|
2190
|
+
signature = request.headers.get(signature_header);
|
|
2191
|
+
if not signature {
|
|
2192
|
+
return TransportResponse.fail(
|
|
2193
|
+
code='UNAUTHORIZED',
|
|
2194
|
+
message=f"Missing {signature_header} header",
|
|
2195
|
+
meta=Meta(extra={'http_status': 401})
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
body = await request.body();
|
|
2199
|
+
extracted_signature = WebhookUtils.extract_signature(signature);
|
|
2200
|
+
if not WebhookUtils.verify_signature(body, extracted_signature, api_key) {
|
|
2201
|
+
return TransportResponse.fail(
|
|
2202
|
+
code='UNAUTHORIZED',
|
|
2203
|
+
message='Invalid webhook signature',
|
|
2204
|
+
meta=Meta(extra={'http_status': 401})
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
# Parse body for walker fields
|
|
2210
|
+
try {
|
|
2211
|
+
body_bytes = await request.body();
|
|
2212
|
+
body_data = json.loads(body_bytes) if body_bytes else {};
|
|
2213
|
+
} except Exception {
|
|
2214
|
+
body_data = {};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
walker_kwargs: dict[str, Any] = dict(body_data) if body_data else {};
|
|
2218
|
+
|
|
2219
|
+
# Execute the walker
|
|
2220
|
+
result = await self.execution_manager.spawn_walker(
|
|
2221
|
+
walkers[walker_name], walker_kwargs, username
|
|
2222
|
+
);
|
|
2223
|
+
|
|
2224
|
+
if 'error' in result {
|
|
2225
|
+
return TransportResponse.fail(
|
|
2226
|
+
code='EXECUTION_ERROR',
|
|
2227
|
+
message=result.get('error', 'Walker execution failed'),
|
|
2228
|
+
details=result.get('traceback') if 'traceback' in result else None,
|
|
2229
|
+
meta=Meta(extra={'http_status': 500})
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
return TransportResponse.success(
|
|
2234
|
+
data=result, meta=Meta(extra={'http_status': 200})
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
# Register dynamic webhook route
|
|
2238
|
+
self.server.add_endpoint(
|
|
2239
|
+
JEndPoint(
|
|
2240
|
+
method=HTTPMethod.POST,
|
|
2241
|
+
path='/webhook/{walker_name}',
|
|
2242
|
+
callback=dynamic_webhook_handler,
|
|
2243
|
+
parameters=[
|
|
2244
|
+
APIParameter(
|
|
2245
|
+
name='walker_name',
|
|
2246
|
+
data_type='string',
|
|
2247
|
+
required=True,
|
|
2248
|
+
default=None,
|
|
2249
|
+
description='Name of the webhook walker to execute',
|
|
2250
|
+
type=ParameterType.PATH
|
|
2251
|
+
),
|
|
2252
|
+
APIParameter(
|
|
2253
|
+
name='x_api_key',
|
|
2254
|
+
data_type='string',
|
|
2255
|
+
required=True,
|
|
2256
|
+
default=None,
|
|
2257
|
+
description='API key for webhook authentication (X-API-Key header)',
|
|
2258
|
+
type=ParameterType.HEADER
|
|
2259
|
+
),
|
|
2260
|
+
APIParameter(
|
|
2261
|
+
name='x_webhook_signature',
|
|
2262
|
+
data_type='string',
|
|
2263
|
+
required=False,
|
|
2264
|
+
default=None,
|
|
2265
|
+
description='HMAC-SHA256 signature of the request body (X-Webhook-Signature header)',
|
|
2266
|
+
type=ParameterType.HEADER
|
|
2267
|
+
)
|
|
2268
|
+
],
|
|
2269
|
+
response_model=None,
|
|
2270
|
+
tags=['Webhooks (Dynamic)'],
|
|
2271
|
+
summary='Execute webhook walker (dynamic HMR)',
|
|
2272
|
+
description='Dynamically routes to webhook walkers. Supports HMR - walker changes are reflected immediately.'
|
|
2273
|
+
)
|
|
2274
|
+
);
|
|
2275
|
+
}
|