jac-scale 0.1.3__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.
@@ -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
- if file_path.startswith(
191
- (f'{cl_route_prefix}/', 'walker/', 'function/', 'user/', 'static/')
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
@@ -498,7 +517,9 @@ impl JacAPIServer.register_functions_endpoints -> None {
498
517
  method=spec_method,
499
518
  path=final_path,
500
519
  callback=self.create_function_callback(func_name),
501
- parameters=self.create_function_parameters(func_name),
520
+ parameters=self.create_function_parameters(
521
+ func_name, method=spec_method
522
+ ),
502
523
  response_model=None,
503
524
  tags=['Functions'],
504
525
  summary='This is a summary',
@@ -508,7 +529,9 @@ impl JacAPIServer.register_functions_endpoints -> None {
508
529
  }
509
530
  }
510
531
 
511
- impl JacAPIServer.create_function_parameters(func_name: str) -> list[APIParameter] {
532
+ impl JacAPIServer.create_function_parameters(
533
+ func_name: str, method: HTTPMethod = HTTPMethod.POST
534
+ ) -> list[APIParameter] {
512
535
  parameters: list[APIParameter] = [];
513
536
  if self.introspector.is_auth_required_for_function(func_name) {
514
537
  parameters.append(
@@ -527,8 +550,13 @@ impl JacAPIServer.create_function_parameters(func_name: str) -> list[APIParamete
527
550
  )['parameters'];
528
551
  for field_name in func_fields {
529
552
  field_type = func_fields[field_name]['type'];
530
- # Determine parameter type based on field type
531
- if ('UploadFile' in field_type or 'uploadfile' in field_type.lower()) {
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()) {
532
560
  # Support UploadFile type for file uploads
533
561
  param_type = ParameterType.FILE;
534
562
  } else {
@@ -607,6 +635,11 @@ impl JacAPIServer.create_function_callback(
607
635
 
608
636
  impl JacAPIServer.register_walkers_endpoints -> None {
609
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
+ }
610
643
  walker_cls = self.get_walkers()[walker_name];
611
644
  restspec = walker_cls.restspec if walker_cls?.restspec else None;
612
645
  spec_method = restspec.method if restspec?.method else HTTPMethod.POST;
@@ -618,7 +651,7 @@ impl JacAPIServer.register_walkers_endpoints -> None {
618
651
  path=f"{spec_path}/{{node}}",
619
652
  callback=self.create_walker_callback(walker_name, has_node_param=True),
620
653
  parameters=self.create_walker_parameters(
621
- walker_name, invoke_on_root=False
654
+ walker_name, invoke_on_root=False, method=spec_method
622
655
  ),
623
656
  response_model=None,
624
657
  tags=['Walkers'],
@@ -632,7 +665,7 @@ impl JacAPIServer.register_walkers_endpoints -> None {
632
665
  path=spec_path,
633
666
  callback=self.create_walker_callback(walker_name, has_node_param=False),
634
667
  parameters=self.create_walker_parameters(
635
- walker_name, invoke_on_root=True
668
+ walker_name, invoke_on_root=True, method=spec_method
636
669
  ),
637
670
  response_model=None,
638
671
  tags=['Walkers'],
@@ -644,7 +677,7 @@ impl JacAPIServer.register_walkers_endpoints -> None {
644
677
  }
645
678
 
646
679
  impl JacAPIServer.create_walker_parameters(
647
- walker_name: str, invoke_on_root: bool
680
+ walker_name: str, invoke_on_root: bool, method: HTTPMethod = HTTPMethod.POST
648
681
  ) -> list[APIParameter] {
649
682
  parameters: list[APIParameter] = [];
650
683
  if self.introspector.is_auth_required_for_walker(walker_name) {
@@ -667,9 +700,14 @@ impl JacAPIServer.create_walker_parameters(
667
700
  continue;
668
701
  }
669
702
  field_type = walker_fields[field_name]['type'];
670
- # Determine parameter type based on field type
703
+ # Determine parameter type based on field type and method
671
704
  if (field_name == '_jac_spawn_node') {
672
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;
673
711
  } elif ('UploadFile' in field_type or 'uploadfile' in field_type.lower()) {
674
712
  # Support UploadFile type for file uploads
675
713
  param_type = ParameterType.FILE;
@@ -1109,6 +1147,7 @@ impl JacAPIServer.login(username: str, password: str) -> TransportResponse {
1109
1147
 
1110
1148
  impl JacAPIServer.postinit -> None {
1111
1149
  super.postinit();
1150
+ self._api_key_manager = ApiKeyManager();
1112
1151
  self.server.app.add_middleware(
1113
1152
  CORSMiddleware,
1114
1153
  allow_origins=['*'],
@@ -1239,13 +1278,17 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
1239
1278
  node: str | None = None,
1240
1279
  Authorization: str | None = None
1241
1280
  ) -> TransportResponse {
1242
- # Parse request body to get walker fields
1243
- try {
1244
- body = await request.json();
1245
- } except Exception {
1246
- body = {};
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 {};
1247
1291
  }
1248
- kwargs: dict[str, Any] = dict(body) if body else {};
1249
1292
 
1250
1293
  # Reload introspector if files changed (HMR)
1251
1294
  if self._hmr_pending {
@@ -1265,6 +1308,16 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
1265
1308
  );
1266
1309
  }
1267
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
+
1268
1321
  # Handle authentication
1269
1322
  username: str | None = None;
1270
1323
  authorization = kwargs.pop('Authorization', None);
@@ -1321,7 +1374,7 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
1321
1374
  data=result, meta=Meta(extra={'http_status': 200})
1322
1375
  );
1323
1376
  }
1324
- # Register catch-all route for walkers with node parameter
1377
+ # Register catch-all route for walkers with node parameter (POST)
1325
1378
  self.server.add_endpoint(
1326
1379
  JEndPoint(
1327
1380
  method=HTTPMethod.POST,
@@ -1359,7 +1412,45 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
1359
1412
  description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
1360
1413
  )
1361
1414
  );
1362
- # Register catch-all route for walkers without node parameter (root)
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)
1363
1454
  self.server.add_endpoint(
1364
1455
  JEndPoint(
1365
1456
  method=HTTPMethod.POST,
@@ -1389,6 +1480,36 @@ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
1389
1480
  description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
1390
1481
  )
1391
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
+ );
1392
1513
  }
1393
1514
 
1394
1515
  """Register a single dynamic endpoint for all functions.
@@ -1402,13 +1523,17 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
1402
1523
  async def dynamic_function_handler(
1403
1524
  request: Request, function_name: str, Authorization: str | None = None
1404
1525
  ) -> TransportResponse {
1405
- # Parse request body to get function arguments
1406
- try {
1407
- body = await request.json();
1408
- } except Exception {
1409
- body = {};
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 {};
1410
1536
  }
1411
- kwargs: dict[str, Any] = dict(body) if body else {};
1412
1537
 
1413
1538
  # Reload introspector if files changed (HMR)
1414
1539
  if self._hmr_pending {
@@ -1477,6 +1602,7 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
1477
1602
  data=result, meta=Meta(extra={'http_status': 200})
1478
1603
  );
1479
1604
  }
1605
+ # Register POST endpoint
1480
1606
  self.server.add_endpoint(
1481
1607
  JEndPoint(
1482
1608
  method=HTTPMethod.POST,
@@ -1506,6 +1632,36 @@ impl JacAPIServer.register_dynamic_function_endpoint -> None {
1506
1632
  description='Dynamically routes to any registered function. Supports HMR - function changes are reflected immediately.'
1507
1633
  )
1508
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
+ );
1509
1665
  }
1510
1666
 
1511
1667
  """Register endpoints for runtime introspection of available walkers/functions.
@@ -1631,3 +1787,482 @@ impl JacAPIServer.register_dynamic_introspection_endpoints -> None {
1631
1787
  )
1632
1788
  );
1633
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
+ }