jac-scale 0.1.1__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.
Files changed (57) hide show
  1. jac_scale/__init__.py +0 -0
  2. jac_scale/abstractions/config/app_config.jac +30 -0
  3. jac_scale/abstractions/config/base_config.jac +26 -0
  4. jac_scale/abstractions/database_provider.jac +51 -0
  5. jac_scale/abstractions/deployment_target.jac +64 -0
  6. jac_scale/abstractions/image_registry.jac +54 -0
  7. jac_scale/abstractions/logger.jac +20 -0
  8. jac_scale/abstractions/models/deployment_result.jac +27 -0
  9. jac_scale/abstractions/models/resource_status.jac +38 -0
  10. jac_scale/config_loader.jac +31 -0
  11. jac_scale/context.jac +14 -0
  12. jac_scale/factories/database_factory.jac +43 -0
  13. jac_scale/factories/deployment_factory.jac +43 -0
  14. jac_scale/factories/registry_factory.jac +32 -0
  15. jac_scale/factories/utility_factory.jac +34 -0
  16. jac_scale/impl/config_loader.impl.jac +131 -0
  17. jac_scale/impl/context.impl.jac +24 -0
  18. jac_scale/impl/memory_hierarchy.main.impl.jac +63 -0
  19. jac_scale/impl/memory_hierarchy.mongo.impl.jac +239 -0
  20. jac_scale/impl/memory_hierarchy.redis.impl.jac +186 -0
  21. jac_scale/impl/serve.impl.jac +1785 -0
  22. jac_scale/jserver/__init__.py +0 -0
  23. jac_scale/jserver/impl/jfast_api.impl.jac +731 -0
  24. jac_scale/jserver/impl/jserver.impl.jac +79 -0
  25. jac_scale/jserver/jfast_api.jac +162 -0
  26. jac_scale/jserver/jserver.jac +101 -0
  27. jac_scale/memory_hierarchy.jac +138 -0
  28. jac_scale/plugin.jac +218 -0
  29. jac_scale/plugin_config.jac +175 -0
  30. jac_scale/providers/database/kubernetes_mongo.jac +137 -0
  31. jac_scale/providers/database/kubernetes_redis.jac +110 -0
  32. jac_scale/providers/registry/dockerhub.jac +64 -0
  33. jac_scale/serve.jac +118 -0
  34. jac_scale/targets/kubernetes/kubernetes_config.jac +215 -0
  35. jac_scale/targets/kubernetes/kubernetes_target.jac +841 -0
  36. jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac +519 -0
  37. jac_scale/targets/kubernetes/utils/kubernetes_utils.jac +85 -0
  38. jac_scale/tests/__init__.py +0 -0
  39. jac_scale/tests/conftest.py +29 -0
  40. jac_scale/tests/fixtures/test_api.jac +159 -0
  41. jac_scale/tests/fixtures/todo_app.jac +68 -0
  42. jac_scale/tests/test_abstractions.py +88 -0
  43. jac_scale/tests/test_deploy_k8s.py +265 -0
  44. jac_scale/tests/test_examples.py +484 -0
  45. jac_scale/tests/test_factories.py +149 -0
  46. jac_scale/tests/test_file_upload.py +444 -0
  47. jac_scale/tests/test_k8s_utils.py +156 -0
  48. jac_scale/tests/test_memory_hierarchy.py +247 -0
  49. jac_scale/tests/test_serve.py +1835 -0
  50. jac_scale/tests/test_sso.py +711 -0
  51. jac_scale/utilities/loggers/standard_logger.jac +40 -0
  52. jac_scale/utils.jac +16 -0
  53. jac_scale-0.1.1.dist-info/METADATA +658 -0
  54. jac_scale-0.1.1.dist-info/RECORD +57 -0
  55. jac_scale-0.1.1.dist-info/WHEEL +5 -0
  56. jac_scale-0.1.1.dist-info/entry_points.txt +3 -0
  57. jac_scale-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1785 @@
1
+ """Helper function to convert TransportResponse to dict for JSONResponse."""
2
+ def _transport_response_to_dict(
3
+ transport_response: TransportResponse
4
+ ) -> dict[str, Any] {
5
+ result = {
6
+ 'ok': transport_response.ok,
7
+ 'type': transport_response.type,
8
+ 'data': transport_response.data,
9
+ 'error': None
10
+ };
11
+ if not transport_response.ok and transport_response.error {
12
+ result['error'] = {
13
+ 'code': transport_response.error.code,
14
+ 'message': transport_response.error.message,
15
+ 'details': transport_response.error.details
16
+ };
17
+ }
18
+ if transport_response.meta {
19
+ meta_dict = {};
20
+ if transport_response.meta.request_id {
21
+ meta_dict['request_id'] = transport_response.meta.request_id;
22
+ }
23
+ if transport_response.meta.trace_id {
24
+ meta_dict['trace_id'] = transport_response.meta.trace_id;
25
+ }
26
+ if transport_response.meta.timestamp {
27
+ meta_dict['timestamp'] = transport_response.meta.timestamp;
28
+ }
29
+ if transport_response.meta.extra {
30
+ meta_dict['extra'] = transport_response.meta.extra;
31
+ }
32
+ if meta_dict {
33
+ result['meta'] = meta_dict;
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+
39
+ """Helper function to get HTTP status code from TransportResponse."""
40
+ def _get_http_status(transport_response: TransportResponse) -> int {
41
+ if transport_response.meta and transport_response.meta.extra {
42
+ return transport_response.meta.extra.get(
43
+ 'http_status', 200 if transport_response.ok else 500
44
+ );
45
+ }
46
+ return 200 if transport_response.ok else 500;
47
+ }
48
+
49
+ """Helper function to convert TransportResponse directly to JSONResponse."""
50
+ def _transport_response_to_json_response(
51
+ transport_response: TransportResponse
52
+ ) -> JSONResponse {
53
+ import from fastapi.responses { JSONResponse }
54
+ return JSONResponse(
55
+ status_code=_get_http_status(transport_response),
56
+ content=_transport_response_to_dict(transport_response)
57
+ );
58
+ }
59
+
60
+ impl JacAPIServer.start(dev: bool = False) -> None {
61
+ self.introspector.load();
62
+ # Eagerly build client bundle if there are client exports (skip in dev mode)
63
+ if not dev {
64
+ client_exports = self.introspector._client_manifest.get('exports', []);
65
+ if client_exports {
66
+ import time;
67
+ import sys;
68
+ import from jaclang.cli.console { console }
69
+ start_time = time.time();
70
+ try {
71
+ with console.status(
72
+ "[cyan]Building client bundle...[/cyan]", spinner="dots"
73
+ ) as status {
74
+ self.introspector.ensure_bundle();
75
+ }
76
+ elapsed = time.time() - start_time;
77
+ console.print(
78
+ f" ✔ Client bundle ready ({elapsed:.1f}s)", style="success"
79
+ );
80
+ } except Exception as e {
81
+ console.warning(f"Failed to build client bundle: {e}");
82
+ console.print(
83
+ "\n Client pages will not be available until this is fixed.\n",
84
+ style="muted",
85
+ file=sys.stderr
86
+ );
87
+ }
88
+ }
89
+ }
90
+ self.register_create_user_endpoint();
91
+ self.register_login_endpoint();
92
+ self.register_page_endpoint();
93
+ self.register_refresh_token_endpoint();
94
+ self.register_sso_endpoints();
95
+ self.register_client_js_endpoint();
96
+ self.register_static_file_endpoint();
97
+ self.register_update_username_endpoint();
98
+ self.register_update_password_endpoint();
99
+ # Use dynamic routing for HMR support, static routing for production
100
+ if dev {
101
+ self.register_dynamic_walker_endpoint();
102
+ self.register_dynamic_function_endpoint();
103
+ self.register_dynamic_introspection_endpoints();
104
+ } else {
105
+ self.register_walkers_endpoints();
106
+ self.register_functions_endpoints();
107
+ }
108
+ self.register_root_asset_endpoint();
109
+ self._configure_openapi_security();
110
+ self.user_manager.create_user('__guest__', '__no_password__');
111
+ self.server.run_server(port=self.port);
112
+ }
113
+
114
+ """Configure OpenAPI security scheme to only apply to walker endpoints that require auth."""
115
+ impl JacAPIServer._configure_openapi_security -> None {
116
+ import from fastapi.openapi.utils { get_openapi }
117
+ def custom_openapi -> dict[str, Any] {
118
+ if self.server.app.openapi_schema {
119
+ return self.server.app.openapi_schema;
120
+ }
121
+ openapi_schema = get_openapi(
122
+ title=self.server.app.title,
123
+ version=self.server.app.version,
124
+ routes=self.server.app.routes
125
+ );
126
+ openapi_schema['components'] = openapi_schema.get('components', {});
127
+ openapi_schema['components']['securitySchemes'] = {
128
+ 'BearerAuth': {
129
+ 'type': 'http',
130
+ 'scheme': 'bearer',
131
+ 'bearerFormat': 'JWT',
132
+ 'description': "Enter your JWT token (without 'Bearer ' prefix)"
133
+ }
134
+ };
135
+ for (path, path_item) in openapi_schema.get('paths', {}).items() {
136
+ if path.startswith('/walker/') {
137
+ path_parts = path.split('/');
138
+ if (len(path_parts) >= 3) {
139
+ walker_name = path_parts[2].split('{')[0].rstrip('/');
140
+ if (
141
+ (walker_name in self.get_walkers())
142
+ and self.introspector.is_auth_required_for_walker(walker_name)
143
+ ) {
144
+ for method in path_item {
145
+ if (method in ['get', 'post', 'put', 'patch', 'delete']) {
146
+ path_item[method]['security'] = [{'BearerAuth': []}];
147
+ }
148
+ }
149
+ }
150
+ }
151
+ } elif path.startswith('/function/') {
152
+ path_parts = path.split('/');
153
+ if (len(path_parts) >= 3) {
154
+ func_name = path_parts[2];
155
+ if (
156
+ (func_name in self.get_functions())
157
+ and self.introspector.is_auth_required_for_function(func_name)
158
+ ) {
159
+ for method in path_item {
160
+ if (method in ['get', 'post', 'put', 'patch', 'delete']) {
161
+ path_item[method]['security'] = [{'BearerAuth': []}];
162
+ }
163
+ }
164
+ }
165
+ }
166
+ } elif path in ['/user/username', '/user/password'] {
167
+ for method in path_item {
168
+ if (method in ['put', 'patch']) {
169
+ path_item[method]['security'] = [{'BearerAuth': []}];
170
+ }
171
+ }
172
+ }
173
+ }
174
+ self.server.app.openapi_schema = openapi_schema;
175
+ return openapi_schema;
176
+ }
177
+ self.server.app.openapi = custom_openapi;
178
+ }
179
+
180
+ """Serve root-level assets like /img.png, /icons/logo.svg, etc."""
181
+ impl JacAPIServer.serve_root_asset(file_path: str) -> Response {
182
+ 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
+ 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
+ import from jaclang.project.config { get_config }
188
+ config = get_config();
189
+ 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
+ ) {
193
+ return Response(status_code=404, content='Not found', media_type='text/plain');
194
+ }
195
+ # Find project root (where jac.toml is) instead of using base_path_dir
196
+ # base_path_dir might be src/ when serving src/app.jac, but we need project root
197
+ import from jaclang.project.config { find_project_root }
198
+ base_path_dir = Path(Jac.base_path_dir) if Jac.base_path_dir else Path.cwd();
199
+ project_root_result = find_project_root(base_path_dir);
200
+ if project_root_result {
201
+ (base_path, _) = project_root_result;
202
+ } else {
203
+ # Fallback to base_path_dir if no project root found
204
+ base_path = base_path_dir;
205
+ }
206
+ file_name = Path(file_path).name;
207
+ candidates = [
208
+ base_path / 'dist' / file_path,
209
+ base_path / 'dist' / file_name,
210
+ base_path / 'assets' / file_path,
211
+ base_path / 'assets' / file_name,
212
+ base_path / 'public' / file_path,
213
+ base_path / 'src' / file_path,
214
+ base_path / 'src' / file_name,
215
+ (base_path / file_path)
216
+ ];
217
+ for candidate_file in candidates {
218
+ if (candidate_file.exists() and candidate_file.is_file()) {
219
+ file_content = candidate_file.read_bytes();
220
+ (content_type, _) = mimetypes.guess_type(str(candidate_file));
221
+ if (content_type is None) {
222
+ content_type = 'application/octet-stream';
223
+ }
224
+ headers = {'Cache-Control': 'public, max-age=31536000'};
225
+ return Response(
226
+ content=file_content, media_type=content_type, headers=headers
227
+ );
228
+ }
229
+ }
230
+ return Response(
231
+ status_code=404,
232
+ content=f"Asset not found: {file_path}",
233
+ media_type='text/plain'
234
+ );
235
+ }
236
+
237
+ """Register root-level asset serving endpoint for files like /img.png, /logo.svg"""
238
+ impl JacAPIServer.register_root_asset_endpoint -> None {
239
+ self.server.add_endpoint(
240
+ JEndPoint(
241
+ method=HTTPMethod.GET,
242
+ path='/{file_path:path}',
243
+ callback=self.serve_root_asset,
244
+ parameters=[
245
+ APIParameter(
246
+ name='file_path',
247
+ data_type='string',
248
+ required=True,
249
+ default=None,
250
+ description='Path to asset file (e.g., img.png, icons/logo.svg)',
251
+ type=ParameterType.PATH
252
+ )
253
+ ],
254
+ response_model=None,
255
+ tags=['Static Files'],
256
+ summary='Serve root-level assets',
257
+ description='Endpoint to serve assets from root path with common extensions (.png, .jpg, .svg, etc.)'
258
+ )
259
+ );
260
+ }
261
+
262
+ """Serve a static file given its path."""
263
+ impl JacAPIServer.serve_static_file(file_path: str) -> Response {
264
+ try {
265
+ # Find project root (where jac.toml is) instead of using base_path_dir
266
+ # base_path_dir might be src/ when serving src/app.jac, but we need project root
267
+ import from jaclang.project.config { find_project_root }
268
+ base_path_dir = Path(Jac.base_path_dir) if Jac.base_path_dir else Path.cwd();
269
+ project_root_result = find_project_root(base_path_dir);
270
+ if project_root_result {
271
+ (base_path, _) = project_root_result;
272
+ } else {
273
+ # Fallback to base_path_dir if no project root found
274
+ base_path = base_path_dir;
275
+ }
276
+ file_name = Path(file_path).name;
277
+ # Check multiple locations for files
278
+ # 1. .jac/client/dist/ (Jac-client build output)
279
+ client_build_dist_file = base_path / '.jac' / 'client' / 'dist' / file_path;
280
+ client_build_dist_file_simple = base_path / '.jac' / 'client' / 'dist' / file_name;
281
+ # 2. dist/ (jac core build output)
282
+ dist_file = base_path / 'dist' / file_path;
283
+ dist_file_simple = base_path / 'dist' / file_name;
284
+ # 3. assets/ (static assets)
285
+ assets_file = base_path / 'assets' / file_path;
286
+ assets_file_simple = base_path / 'assets' / file_name;
287
+ if file_name.endswith('.css') {
288
+ # Check .jac/client/dist/ first (jac-client)
289
+ if client_build_dist_file.exists() {
290
+ css_content = client_build_dist_file.read_text(encoding='utf-8');
291
+ return Response(content=css_content, media_type='text/css');
292
+ } elif client_build_dist_file_simple.exists() {
293
+ css_content = client_build_dist_file_simple.read_text(encoding='utf-8');
294
+ return Response(content=css_content, media_type='text/css');
295
+ } elif dist_file.exists() {
296
+ css_content = dist_file.read_text(encoding='utf-8');
297
+ return Response(content=css_content, media_type='text/css');
298
+ } elif dist_file_simple.exists() {
299
+ css_content = dist_file_simple.read_text(encoding='utf-8');
300
+ return Response(content=css_content, media_type='text/css');
301
+ } elif assets_file.exists() {
302
+ css_content = assets_file.read_text(encoding='utf-8');
303
+ return Response(content=css_content, media_type='text/css');
304
+ } elif assets_file_simple.exists() {
305
+ css_content = assets_file_simple.read_text(encoding='utf-8');
306
+ return Response(content=css_content, media_type='text/css');
307
+ } else {
308
+ return Response(
309
+ status_code=404,
310
+ content='CSS file not found',
311
+ media_type='text/plain'
312
+ );
313
+ }
314
+ }
315
+ for candidate_file in [
316
+ client_build_dist_file,
317
+ client_build_dist_file_simple,
318
+ dist_file,
319
+ dist_file_simple,
320
+ assets_file,
321
+ assets_file_simple
322
+ ] {
323
+ if (candidate_file.exists() and candidate_file.is_file()) {
324
+ file_content = candidate_file.read_bytes();
325
+ (content_type, _) = mimetypes.guess_type(str(candidate_file));
326
+ if (content_type is None) {
327
+ content_type = 'application/octet-stream';
328
+ }
329
+ return Response(content=file_content, media_type=content_type);
330
+ }
331
+ }
332
+ return Response(
333
+ status_code=404, content='Static file not found', media_type='text/plain'
334
+ );
335
+ } except Exception as exc {
336
+ return Response(status_code=500);
337
+ }
338
+ }
339
+
340
+ """Register the static file serving endpoint using JEndPoint."""
341
+ impl JacAPIServer.register_static_file_endpoint -> None {
342
+ self.server.add_endpoint(
343
+ JEndPoint(
344
+ method=HTTPMethod.GET,
345
+ path='/static/{file_path:path}',
346
+ callback=self.serve_static_file,
347
+ parameters=[
348
+ APIParameter(
349
+ name='file_path',
350
+ data_type='string',
351
+ required=True,
352
+ default=None,
353
+ description='Path of the static file to serve',
354
+ type=ParameterType.PATH
355
+ )
356
+ ],
357
+ response_model=None,
358
+ tags=['Static Files'],
359
+ summary='Serve static files',
360
+ description='Endpoint to serve static files from the server.'
361
+ )
362
+ );
363
+ }
364
+
365
+ """Register the client.js serving endpoint using JEndPoint."""
366
+ impl JacAPIServer.register_client_js_endpoint -> None {
367
+ self.server.add_endpoint(
368
+ JEndPoint(
369
+ method=HTTPMethod.GET,
370
+ path='/static/client.js',
371
+ callback=self.serve_client_js_callback(),
372
+ parameters=[],
373
+ response_model=None,
374
+ tags=['Static Files'],
375
+ summary='Serve client.js',
376
+ description='Endpoint to serve the client-side JavaScript file.'
377
+ )
378
+ );
379
+ }
380
+
381
+ """Create callback to serve the client.js file."""
382
+ impl JacAPIServer.serve_client_js_callback -> Callable[..., Response] {
383
+ def callback -> Response {
384
+ try {
385
+ self.introspector.load();
386
+ self.introspector.ensure_bundle();
387
+ return Response(
388
+ content=self.introspector._bundle.code,
389
+ media_type='application/javascript'
390
+ );
391
+ } except RuntimeError as exc {
392
+ return Response(content=str(exc), status_code=503, media_type='text/plain');
393
+ }
394
+ }
395
+ return callback;
396
+ }
397
+
398
+ """Register the page rendering endpoint using JEndPoint."""
399
+ impl JacAPIServer.register_page_endpoint -> None {
400
+ import from jaclang.project.config { get_config }
401
+ config = get_config();
402
+ cl_route_prefix = config.serve.cl_route_prefix if config else "cl";
403
+ base_route_app = config.serve.base_route_app if config else "";
404
+ # Register the configurable cl route (e.g., /cl/{page_name})
405
+ self.server.add_endpoint(
406
+ JEndPoint(
407
+ method=HTTPMethod.GET,
408
+ path=f'/{cl_route_prefix}/{{page_name}}',
409
+ callback=self.render_page_callback(),
410
+ parameters=[
411
+ APIParameter(
412
+ name='page_name',
413
+ data_type='string',
414
+ required=True,
415
+ default=None,
416
+ description='Name of the page to render',
417
+ type=ParameterType.PATH
418
+ )
419
+ ],
420
+ response_model=None,
421
+ tags=['Pages'],
422
+ summary='Render a page',
423
+ description=f'Endpoint to render and retrieve a specific page by name at /{cl_route_prefix}/{{name}}.'
424
+ )
425
+ );
426
+ # If base_route_app is configured, register it at /
427
+ if base_route_app {
428
+ self.server.add_endpoint(
429
+ JEndPoint(
430
+ method=HTTPMethod.GET,
431
+ path='/',
432
+ callback=self.render_base_route_callback(base_route_app),
433
+ parameters=[],
434
+ response_model=None,
435
+ tags=['Pages'],
436
+ summary='Base route app',
437
+ description=f'Serves the {base_route_app} client app at the root path.'
438
+ )
439
+ );
440
+ }
441
+ }
442
+
443
+ """Create callback that extracts all query parameters from FastAPI Request."""
444
+ impl JacAPIServer.render_page_callback -> Callable[..., HTMLResponse] {
445
+ """Render a page by name with all query parameters.""";
446
+ def callback(page_name: str, **kwargs: JsonValue) -> HTMLResponse {
447
+ try {
448
+ render_payload = self.introspector.render_page(
449
+ page_name, kwargs, '__guest__'
450
+ );
451
+ return HTMLResponse(content=render_payload['html']);
452
+ } except ValueError as exc {
453
+ return HTMLResponse(content=f"<h1>404 Not Found</h1>", status_code=404);
454
+ } except RuntimeError as exc {
455
+ print(f"Error rendering page '{page_name}': {exc}");
456
+ return HTMLResponse(
457
+ content=f"<h1>503 Service Unavailable</h1>", status_code=503
458
+ );
459
+ }
460
+ }
461
+ return callback;
462
+ }
463
+
464
+ """Create callback for base route app rendering."""
465
+ impl JacAPIServer.render_base_route_callback(
466
+ app_name: str
467
+ ) -> Callable[..., HTMLResponse] {
468
+ """Render the base route app.""";
469
+ def callback(**kwargs: JsonValue) -> HTMLResponse {
470
+ try {
471
+ render_payload = self.introspector.render_page(
472
+ app_name, kwargs, '__guest__'
473
+ );
474
+ return HTMLResponse(content=render_payload['html']);
475
+ } except ValueError as exc {
476
+ return HTMLResponse(content=f"<h1>404 Not Found</h1>", status_code=404);
477
+ } except RuntimeError as exc {
478
+ print(f"Error rendering base route app '{app_name}': {exc}");
479
+ return HTMLResponse(
480
+ content=f"<h1>503 Service Unavailable</h1>", status_code=503
481
+ );
482
+ }
483
+ }
484
+ return callback;
485
+ }
486
+
487
+ impl JacAPIServer.register_functions_endpoints -> None {
488
+ for func_name in self.get_functions() {
489
+ self.server.add_endpoint(
490
+ JEndPoint(
491
+ method=HTTPMethod.POST,
492
+ path=f"/function/{func_name}",
493
+ callback=self.create_function_callback(func_name),
494
+ parameters=self.create_function_parameters(func_name),
495
+ response_model=None,
496
+ tags=['Functions'],
497
+ summary='This is a summary',
498
+ description='This is a description'
499
+ )
500
+ );
501
+ }
502
+ }
503
+
504
+ impl JacAPIServer.create_function_parameters(func_name: str) -> list[APIParameter] {
505
+ parameters: list[APIParameter] = [];
506
+ if self.introspector.is_auth_required_for_function(func_name) {
507
+ parameters.append(
508
+ APIParameter(
509
+ name='Authorization',
510
+ data_type='string',
511
+ required=False,
512
+ default=None,
513
+ description='Bearer token for authentication',
514
+ type=ParameterType.HEADER
515
+ )
516
+ );
517
+ }
518
+ func_fields = self.introspector.introspect_callable(
519
+ self.get_functions()[func_name]
520
+ )['parameters'];
521
+ for field_name in func_fields {
522
+ field_type = func_fields[field_name]['type'];
523
+ # Determine parameter type based on field type
524
+ if ('UploadFile' in field_type or 'uploadfile' in field_type.lower()) {
525
+ # Support UploadFile type for file uploads
526
+ param_type = ParameterType.FILE;
527
+ } else {
528
+ param_type = ParameterType.BODY;
529
+ }
530
+ parameters.append(
531
+ APIParameter(
532
+ name=field_name,
533
+ data_type=field_type,
534
+ required=func_fields[field_name]['required'],
535
+ default=func_fields[field_name]['default'],
536
+ description=f"Field {field_name} for function {func_name}",
537
+ type=param_type
538
+ )
539
+ );
540
+ }
541
+ return parameters;
542
+ }
543
+
544
+ impl JacAPIServer.create_function_callback(
545
+ func_name: str
546
+ ) -> Callable[..., TransportResponse] {
547
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
548
+ requires_auth = self.introspector.is_auth_required_for_function(func_name);
549
+ def callback(**kwargs: JsonValue) -> TransportResponse {
550
+ username: (str | None) = None;
551
+ if requires_auth {
552
+ authorization = kwargs.pop('Authorization', None);
553
+ token: (str | None) = None;
554
+ if (
555
+ authorization
556
+ and isinstance(authorization, str)
557
+ and authorization.startswith('Bearer ')
558
+ ) {
559
+ token = authorization[7:];
560
+ }
561
+ username = self.validate_jwt_token(token) if token else None;
562
+ if not username {
563
+ return TransportResponse.fail(
564
+ code='UNAUTHORIZED',
565
+ message='Unauthorized',
566
+ meta=Meta(extra={'http_status': 401})
567
+ );
568
+ }
569
+ }
570
+ print(f"Executing function '{func_name}' with params: {kwargs}");
571
+ result = self.execution_manager.execute_function(
572
+ self.get_functions()[func_name], kwargs, (username or '__guest__')
573
+ );
574
+ if 'error' in result {
575
+ return TransportResponse.fail(
576
+ code='EXECUTION_ERROR',
577
+ message=result.get('error', 'Function execution failed'),
578
+ details=result.get('traceback') if 'traceback' in result else None,
579
+ meta=Meta(extra={'http_status': 500})
580
+ );
581
+ }
582
+ return TransportResponse.success(
583
+ data=result, meta=Meta(extra={'http_status': 200})
584
+ );
585
+ }
586
+ return callback;
587
+ }
588
+
589
+ impl JacAPIServer.register_walkers_endpoints -> None {
590
+ for walker_name in self.get_walkers() {
591
+ self.server.add_endpoint(
592
+ JEndPoint(
593
+ method=HTTPMethod.POST,
594
+ path=f"/walker/{walker_name}/{{node}}",
595
+ callback=self.create_walker_callback(walker_name, has_node_param=True),
596
+ parameters=self.create_walker_parameters(
597
+ walker_name, invoke_on_root=False
598
+ ),
599
+ response_model=None,
600
+ tags=['Walkers'],
601
+ summary='API Entry',
602
+ description='API Entry'
603
+ )
604
+ );
605
+ self.server.add_endpoint(
606
+ JEndPoint(
607
+ method=HTTPMethod.POST,
608
+ path=f"/walker/{walker_name}",
609
+ callback=self.create_walker_callback(walker_name, has_node_param=False),
610
+ parameters=self.create_walker_parameters(
611
+ walker_name, invoke_on_root=True
612
+ ),
613
+ response_model=None,
614
+ tags=['Walkers'],
615
+ summary='API Root',
616
+ description='API Root'
617
+ )
618
+ );
619
+ }
620
+ }
621
+
622
+ impl JacAPIServer.create_walker_parameters(
623
+ walker_name: str, invoke_on_root: bool
624
+ ) -> list[APIParameter] {
625
+ parameters: list[APIParameter] = [];
626
+ if self.introspector.is_auth_required_for_walker(walker_name) {
627
+ parameters.append(
628
+ APIParameter(
629
+ name='Authorization',
630
+ data_type='string',
631
+ required=False,
632
+ default=None,
633
+ description='Bearer token for authentication',
634
+ type=ParameterType.HEADER
635
+ )
636
+ );
637
+ }
638
+ walker_fields = self.introspector.introspect_walker(
639
+ self.get_walkers()[walker_name]
640
+ )['fields'];
641
+ for field_name in walker_fields {
642
+ if ((field_name == '_jac_spawn_node') and invoke_on_root) {
643
+ continue;
644
+ }
645
+ field_type = walker_fields[field_name]['type'];
646
+ # Determine parameter type based on field type
647
+ if (field_name == '_jac_spawn_node') {
648
+ param_type = ParameterType.PATH;
649
+ } elif ('UploadFile' in field_type or 'uploadfile' in field_type.lower()) {
650
+ # Support UploadFile type for file uploads
651
+ param_type = ParameterType.FILE;
652
+ } else {
653
+ param_type = ParameterType.BODY;
654
+ }
655
+ parameters.append(
656
+ APIParameter(
657
+ name='node' if (field_name == '_jac_spawn_node') else field_name,
658
+ data_type=field_type,
659
+ required=walker_fields[field_name]['required'],
660
+ default=walker_fields[field_name]['default'],
661
+ description=f"Field {field_name} for walker {walker_name}",
662
+ type=param_type
663
+ )
664
+ );
665
+ }
666
+ return parameters;
667
+ }
668
+
669
+ impl JacAPIServer.create_walker_callback(
670
+ walker_name: str, has_node_param: bool = False
671
+ ) -> Callable[..., TransportResponse] {
672
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
673
+ requires_auth = self.introspector.is_auth_required_for_walker(walker_name);
674
+ async def callback(
675
+ node: (str | None) = None, **kwargs: JsonValue
676
+ ) -> TransportResponse {
677
+ username: (str | None) = None;
678
+ if requires_auth {
679
+ authorization = kwargs.pop('Authorization', None);
680
+ token: (str | None) = None;
681
+ if (
682
+ authorization
683
+ and isinstance(authorization, str)
684
+ and authorization.startswith('Bearer ')
685
+ ) {
686
+ token = authorization[7:];
687
+ }
688
+ username = self.validate_jwt_token(token) if token else None;
689
+ if not username {
690
+ return TransportResponse.fail(
691
+ code='UNAUTHORIZED',
692
+ message='Unauthorized',
693
+ meta=Meta(extra={'http_status': 401})
694
+ );
695
+ }
696
+ }
697
+ if node {
698
+ kwargs['_jac_spawn_node'] = node;
699
+ }
700
+ result = await self.execution_manager.spawn_walker(
701
+ self.get_walkers()[walker_name], kwargs, (username or '__guest__')
702
+ );
703
+ if 'error' in result {
704
+ return TransportResponse.fail(
705
+ code='EXECUTION_ERROR',
706
+ message=result.get('error', 'Walker execution failed'),
707
+ details=result.get('traceback') if 'traceback' in result else None,
708
+ meta=Meta(extra={'http_status': 500})
709
+ );
710
+ }
711
+ return TransportResponse.success(
712
+ data=result, meta=Meta(extra={'http_status': 200})
713
+ );
714
+ }
715
+ return callback;
716
+ }
717
+
718
+ impl JacAPIServer.register_refresh_token_endpoint -> None {
719
+ self.server.add_endpoint(
720
+ JEndPoint(
721
+ method=HTTPMethod.POST,
722
+ path='/user/refresh-token',
723
+ callback=self.refresh_token,
724
+ parameters=[
725
+ APIParameter(
726
+ name='token',
727
+ data_type='string',
728
+ required=True,
729
+ default=None,
730
+ description='JWT token to refresh (with or without Bearer prefix)',
731
+ type=ParameterType.BODY
732
+ )
733
+ ],
734
+ response_model=None,
735
+ tags=['User APIs'],
736
+ summary='Refresh JWT token',
737
+ description='Endpoint for refreshing an existing JWT token. Token must be within the refresh window.'
738
+ )
739
+ );
740
+ }
741
+
742
+ impl JacAPIServer.register_create_user_endpoint -> None {
743
+ self.server.add_endpoint(
744
+ JEndPoint(
745
+ method=HTTPMethod.POST,
746
+ path='/user/register',
747
+ callback=self.create_user,
748
+ parameters=[
749
+ APIParameter(
750
+ name='username',
751
+ data_type='string',
752
+ required=True,
753
+ default=None,
754
+ description='Username for new user',
755
+ type=ParameterType.BODY
756
+ ),
757
+ APIParameter(
758
+ name='password',
759
+ data_type='string',
760
+ required=True,
761
+ default=None,
762
+ description='Password for new user',
763
+ type=ParameterType.BODY
764
+ )
765
+ ],
766
+ response_model=None,
767
+ tags=['User APIs'],
768
+ summary='Register user API.',
769
+ description='Endpoint for creating a new user account'
770
+ )
771
+ );
772
+ }
773
+
774
+ impl JacAPIServer.refresh_token(token: (str | None) = None) -> TransportResponse {
775
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
776
+ if (not token) {
777
+ return TransportResponse.fail(
778
+ code='VALIDATION_ERROR',
779
+ message='Token is required',
780
+ meta=Meta(extra={'http_status': 400})
781
+ );
782
+ }
783
+ if token.startswith('Bearer ') {
784
+ token = token[7:];
785
+ }
786
+ new_token = self.refresh_jwt_token(token);
787
+ if (not new_token) {
788
+ return TransportResponse.fail(
789
+ code='UNAUTHORIZED',
790
+ message='Invalid or expired token',
791
+ meta=Meta(extra={'http_status': 401})
792
+ );
793
+ }
794
+ return TransportResponse.success(
795
+ data={'token': new_token, 'message': 'Token refreshed successfully'},
796
+ meta=Meta(extra={'http_status': 200})
797
+ );
798
+ }
799
+
800
+ impl JacAPIServer.create_user(username: str, password: str) -> TransportResponse {
801
+ import traceback;
802
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
803
+ try {
804
+ res = self.user_manager.create_user(username, password);
805
+ if ('error' in res) {
806
+ return TransportResponse.fail(
807
+ code='USER_EXISTS',
808
+ message=res.get('error', 'User creation failed'),
809
+ meta=Meta(extra={'http_status': 400})
810
+ );
811
+ }
812
+ res['token'] = self.create_jwt_token(username);
813
+ return TransportResponse.success(
814
+ data=res, meta=Meta(extra={'http_status': 201})
815
+ );
816
+ } except Exception as e {
817
+ error_trace = traceback.format_exc();
818
+ print(f"Error in create_user: {e}\n{error_trace}");
819
+ return TransportResponse.fail(
820
+ code='INTERNAL_ERROR',
821
+ message=f"Registration failed: {e}",
822
+ meta=Meta(extra={'http_status': 500})
823
+ );
824
+ }
825
+ }
826
+
827
+ impl JacAPIServer.update_username(
828
+ current_username: str, new_username: str, Authorization: (str | None) = None
829
+ ) -> TransportResponse {
830
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
831
+ # Validate token and extract username
832
+ token: (str | None) = None;
833
+ if (
834
+ Authorization
835
+ and isinstance(Authorization, str)
836
+ and Authorization.startswith('Bearer ')
837
+ ) {
838
+ token = Authorization[7:];
839
+ }
840
+ token_username = self.validate_jwt_token(token) if token else None;
841
+ if not token_username {
842
+ return TransportResponse.fail(
843
+ code='UNAUTHORIZED',
844
+ message='Invalid or expired token',
845
+ meta=Meta(extra={'http_status': 401})
846
+ );
847
+ }
848
+ # Ensure user is updating their own username
849
+ if (token_username != current_username) {
850
+ return TransportResponse.fail(
851
+ code='FORBIDDEN',
852
+ message='Cannot update another user\'s username',
853
+ meta=Meta(extra={'http_status': 403})
854
+ );
855
+ }
856
+ if (not new_username) {
857
+ return TransportResponse.fail(
858
+ code='VALIDATION_ERROR',
859
+ message='New username is required',
860
+ meta=Meta(extra={'http_status': 400})
861
+ );
862
+ }
863
+ result = self.user_manager.update_username(current_username, new_username);
864
+ if ('error' in result) {
865
+ return TransportResponse.fail(
866
+ code='UPDATE_FAILED',
867
+ message=result.get('error', 'Username update failed'),
868
+ meta=Meta(extra={'http_status': 400})
869
+ );
870
+ }
871
+ # Generate new JWT token with updated username
872
+ result['token'] = self.create_jwt_token(new_username);
873
+ return TransportResponse.success(
874
+ data=result, meta=Meta(extra={'http_status': 200})
875
+ );
876
+ }
877
+
878
+ impl JacAPIServer.update_password(
879
+ username: str,
880
+ current_password: str,
881
+ new_password: str,
882
+ Authorization: (str | None) = None
883
+ ) -> TransportResponse {
884
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
885
+ # Validate token and extract username
886
+ token: (str | None) = None;
887
+ if (
888
+ Authorization
889
+ and isinstance(Authorization, str)
890
+ and Authorization.startswith('Bearer ')
891
+ ) {
892
+ token = Authorization[7:];
893
+ }
894
+ token_username = self.validate_jwt_token(token) if token else None;
895
+ if not token_username {
896
+ return TransportResponse.fail(
897
+ code='UNAUTHORIZED',
898
+ message='Invalid or expired token',
899
+ meta=Meta(extra={'http_status': 401})
900
+ );
901
+ }
902
+ # Ensure user is updating their own password
903
+ if (token_username != username) {
904
+ return TransportResponse.fail(
905
+ code='FORBIDDEN',
906
+ message='Cannot update another user\'s password',
907
+ meta=Meta(extra={'http_status': 403})
908
+ );
909
+ }
910
+ if (not current_password or not new_password) {
911
+ return TransportResponse.fail(
912
+ code='VALIDATION_ERROR',
913
+ message='Current password and new password are required',
914
+ meta=Meta(extra={'http_status': 400})
915
+ );
916
+ }
917
+ result = self.user_manager.update_password(
918
+ username, current_password, new_password
919
+ );
920
+ if ('error' in result) {
921
+ return TransportResponse.fail(
922
+ code='UPDATE_FAILED',
923
+ message=result.get('error', 'Password update failed'),
924
+ meta=Meta(extra={'http_status': 400})
925
+ );
926
+ }
927
+ return TransportResponse.success(
928
+ data=result, meta=Meta(extra={'http_status': 200})
929
+ );
930
+ }
931
+
932
+ impl JacAPIServer.register_update_username_endpoint -> None {
933
+ import from fastapi { Request }
934
+ async def update_username_handler(
935
+ request: Request, current_username: str, new_username: str
936
+ ) -> TransportResponse {
937
+ authorization = request.headers.get('Authorization');
938
+ return self.update_username(current_username, new_username, authorization);
939
+ }
940
+ self.server.add_endpoint(
941
+ JEndPoint(
942
+ method=HTTPMethod.PUT,
943
+ path='/user/username',
944
+ callback=update_username_handler,
945
+ parameters=[
946
+ APIParameter(
947
+ name='current_username',
948
+ data_type='string',
949
+ required=True,
950
+ default=None,
951
+ description='Current username',
952
+ type=ParameterType.BODY
953
+ ),
954
+ APIParameter(
955
+ name='new_username',
956
+ data_type='string',
957
+ required=True,
958
+ default=None,
959
+ description='New username',
960
+ type=ParameterType.BODY
961
+ )
962
+ ],
963
+ response_model=None,
964
+ tags=['User APIs'],
965
+ summary='Update username',
966
+ description='Endpoint for updating user\'s username. Requires authentication.'
967
+ )
968
+ );
969
+ }
970
+
971
+ impl JacAPIServer.register_update_password_endpoint -> None {
972
+ import from fastapi { Request }
973
+ async def update_password_handler(
974
+ request: Request, username: str, current_password: str, new_password: str
975
+ ) -> TransportResponse {
976
+ authorization = request.headers.get('Authorization');
977
+ return self.update_password(
978
+ username, current_password, new_password, authorization
979
+ );
980
+ }
981
+ self.server.add_endpoint(
982
+ JEndPoint(
983
+ method=HTTPMethod.PUT,
984
+ path='/user/password',
985
+ callback=update_password_handler,
986
+ parameters=[
987
+ APIParameter(
988
+ name='username',
989
+ data_type='string',
990
+ required=True,
991
+ default=None,
992
+ description='Username',
993
+ type=ParameterType.BODY
994
+ ),
995
+ APIParameter(
996
+ name='current_password',
997
+ data_type='string',
998
+ required=True,
999
+ default=None,
1000
+ description='Current password',
1001
+ type=ParameterType.BODY
1002
+ ),
1003
+ APIParameter(
1004
+ name='new_password',
1005
+ data_type='string',
1006
+ required=True,
1007
+ default=None,
1008
+ description='New password',
1009
+ type=ParameterType.BODY
1010
+ )
1011
+ ],
1012
+ response_model=None,
1013
+ tags=['User APIs'],
1014
+ summary='Update password',
1015
+ description='Endpoint for updating user\'s password. Requires authentication.'
1016
+ )
1017
+ );
1018
+ }
1019
+
1020
+ impl JacAPIServer.register_login_endpoint -> None {
1021
+ self.server.add_endpoint(
1022
+ JEndPoint(
1023
+ method=HTTPMethod.POST,
1024
+ path='/user/login',
1025
+ callback=self.login,
1026
+ parameters=[
1027
+ APIParameter(
1028
+ name='username',
1029
+ data_type='string',
1030
+ required=True,
1031
+ default=None,
1032
+ description='username for login',
1033
+ type=ParameterType.BODY
1034
+ ),
1035
+ APIParameter(
1036
+ name='password',
1037
+ data_type='string',
1038
+ required=True,
1039
+ default=None,
1040
+ description='Password for login',
1041
+ type=ParameterType.BODY
1042
+ )
1043
+ ],
1044
+ response_model=None,
1045
+ tags=['User APIs'],
1046
+ summary='User login',
1047
+ description='Endpoint for user authentication and token generation'
1048
+ )
1049
+ );
1050
+ }
1051
+
1052
+ impl JacAPIServer.login(username: str, password: str) -> TransportResponse {
1053
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
1054
+ if (not username or not password) {
1055
+ return TransportResponse.fail(
1056
+ code='VALIDATION_ERROR',
1057
+ message='Username and password required',
1058
+ meta=Meta(extra={'http_status': 400})
1059
+ );
1060
+ }
1061
+ result = self.user_manager.authenticate(username, password);
1062
+ if not result {
1063
+ return TransportResponse.fail(
1064
+ code='UNAUTHORIZED',
1065
+ message='Invalid credentials',
1066
+ meta=Meta(extra={'http_status': 401})
1067
+ );
1068
+ }
1069
+ result['token'] = self.create_jwt_token(username);
1070
+ return TransportResponse.success(
1071
+ data=dict[(str, JsonValue)](result), meta=Meta(extra={'http_status': 200})
1072
+ );
1073
+ }
1074
+
1075
+ impl JacAPIServer.postinit -> None {
1076
+ super.postinit();
1077
+ self.server.app.add_middleware(
1078
+ CORSMiddleware,
1079
+ allow_origins=['*'],
1080
+ allow_credentials=True,
1081
+ allow_methods=['*'],
1082
+ allow_headers=['*']
1083
+ );
1084
+ # Add custom response headers from jac.toml [environments.response.headers]
1085
+ import from jaclang.project.config { JacConfig }
1086
+ import from starlette.middleware.base { BaseHTTPMiddleware }
1087
+ import from pathlib { Path }
1088
+ # Use base_path to find the correct jac.toml for this project
1089
+ start_path = Path(self.base_path) if self.base_path else None;
1090
+ config = JacConfig.discover(start_path);
1091
+ custom_headers: dict = {};
1092
+ if config
1093
+ and config.environments
1094
+ and "response" in config.environments
1095
+ and "headers" in config.environments["response"] {
1096
+ custom_headers = config.environments["response"]["headers"];
1097
+ }
1098
+ if custom_headers {
1099
+ class CustomHeadersMiddleware(BaseHTTPMiddleware) {
1100
+ async def dispatch(
1101
+ self: CustomHeadersMiddleware, request: Any, call_next: Any
1102
+ ) -> Any {
1103
+ response = await call_next(request);
1104
+ for (header_name, header_value) in custom_headers.items() {
1105
+ response.headers[header_name] = header_value;
1106
+ }
1107
+ return response;
1108
+ }
1109
+ }
1110
+ self.server.app.add_middleware(CustomHeadersMiddleware);
1111
+ }
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
+ }
1326
+
1327
+ impl JacAPIServer.register_sso_endpoints -> None {
1328
+ self.server.add_endpoint(
1329
+ JEndPoint(
1330
+ method=HTTPMethod.GET,
1331
+ path='/sso/{platform}/{operation}',
1332
+ callback=self.sso_initiate,
1333
+ parameters=[
1334
+ APIParameter(
1335
+ name='platform',
1336
+ data_type='string',
1337
+ required=True,
1338
+ default=None,
1339
+ description='SSO platform: google',
1340
+ type=ParameterType.PATH
1341
+ ),
1342
+ APIParameter(
1343
+ name='operation',
1344
+ data_type='string',
1345
+ required=True,
1346
+ default=None,
1347
+ description='Operation to perform: "login" or "register"',
1348
+ type=ParameterType.PATH
1349
+ )
1350
+ ],
1351
+ response_model=None,
1352
+ tags=['SSO APIs'],
1353
+ summary='Initiate SSO authentication',
1354
+ description='Redirects to the SSO provider for authentication. Supported platforms: Google. Configure each platform by setting SSO_{PLATFORM}_CLIENT_ID and SSO_{PLATFORM}_CLIENT_SECRET environment variables.'
1355
+ )
1356
+ );
1357
+ self.server.add_endpoint(
1358
+ JEndPoint(
1359
+ method=HTTPMethod.GET,
1360
+ path='/sso/{platform}/{operation}/callback',
1361
+ callback=self.sso_callback,
1362
+ parameters=[
1363
+ APIParameter(
1364
+ name='platform',
1365
+ data_type='string',
1366
+ required=True,
1367
+ default=None,
1368
+ description='SSO platform: google',
1369
+ type=ParameterType.PATH
1370
+ ),
1371
+ APIParameter(
1372
+ name='operation',
1373
+ data_type='string',
1374
+ required=True,
1375
+ default=None,
1376
+ description='Operation to perform: "login" or "register"',
1377
+ type=ParameterType.PATH
1378
+ )
1379
+ ],
1380
+ response_model=None,
1381
+ tags=['SSO APIs'],
1382
+ summary='SSO callback endpoint',
1383
+ description='Handles the callback from SSO provider after authentication'
1384
+ )
1385
+ );
1386
+ }
1387
+
1388
+ # ============================================================================
1389
+ # HMR (Hot Module Replacement) Dynamic Routing Support
1390
+ # ============================================================================
1391
+ """Enable HMR mode - file changes trigger reload on next request."""
1392
+ impl JacAPIServer.enable_hmr(hot_reloader: Any) -> None {
1393
+ self._hot_reloader = hot_reloader;
1394
+ # Callback when file changes - sets pending flag
1395
+ def on_change(event: Any) -> None {
1396
+ self._hmr_pending = True;
1397
+ logger.debug(f"Change detected: {event.path}");
1398
+ }
1399
+ # Register callback with the watcher
1400
+ hot_reloader.watcher.add_callback(on_change);
1401
+ logger.debug("Dynamic routing enabled for jac-scale");
1402
+ }
1403
+
1404
+ """Register a single dynamic endpoint for all walkers.
1405
+
1406
+ Instead of pre-registering /walker/WalkerA, /walker/WalkerB, etc.,
1407
+ we register catch-all routes that look up walkers at request time.
1408
+ This enables HMR since introspector.load() is called per-request.
1409
+ """
1410
+ impl JacAPIServer.register_dynamic_walker_endpoint -> None {
1411
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
1412
+ import from fastapi { Request }
1413
+ # Dynamic handler that looks up walker at request time
1414
+ async def dynamic_walker_handler(
1415
+ request: Request,
1416
+ walker_name: str,
1417
+ node: str | None = None,
1418
+ Authorization: str | None = None
1419
+ ) -> TransportResponse {
1420
+ # Parse request body to get walker fields
1421
+ try {
1422
+ body = await request.json();
1423
+ } except Exception {
1424
+ body = {};
1425
+ }
1426
+ kwargs: dict[str, Any] = dict(body) if body else {};
1427
+
1428
+ # Reload introspector if files changed (HMR)
1429
+ if self._hmr_pending {
1430
+ self.introspector.load(force_reload=True);
1431
+ self._hmr_pending = False;
1432
+ }
1433
+
1434
+ walkers = self.get_walkers();
1435
+
1436
+ if walker_name not in walkers {
1437
+ return TransportResponse.fail(
1438
+ code='NOT_FOUND',
1439
+ message=f"Walker '{walker_name}' not found. Available: {list(
1440
+ walkers.keys()
1441
+ )}",
1442
+ meta=Meta(extra={'http_status': 404})
1443
+ );
1444
+ }
1445
+
1446
+ # Handle authentication
1447
+ username: str | None = None;
1448
+ authorization = kwargs.pop('Authorization', None);
1449
+ if self.introspector.is_auth_required_for_walker(walker_name) {
1450
+ token: str | None = None;
1451
+ if (
1452
+ Authorization
1453
+ and isinstance(Authorization, str)
1454
+ and Authorization.startswith('Bearer ')
1455
+ ) {
1456
+ token = Authorization[7:];
1457
+ }
1458
+ username = self.validate_jwt_token(token) if token else None;
1459
+ if not username {
1460
+ return TransportResponse.fail(
1461
+ code='UNAUTHORIZED',
1462
+ message='Unauthorized',
1463
+ meta=Meta(extra={'http_status': 401})
1464
+ );
1465
+ }
1466
+ }
1467
+
1468
+ # Add node to kwargs if provided
1469
+ if node {
1470
+ kwargs['_jac_spawn_node'] = node;
1471
+ }
1472
+
1473
+ result = await self.execution_manager.spawn_walker(
1474
+ walkers[walker_name], kwargs, (username or '__guest__')
1475
+ );
1476
+ if 'error' in result {
1477
+ return TransportResponse.fail(
1478
+ code='EXECUTION_ERROR',
1479
+ message=result.get('error', 'Walker execution failed'),
1480
+ details=result.get('traceback') if 'traceback' in result else None,
1481
+ meta=Meta(extra={'http_status': 500})
1482
+ );
1483
+ }
1484
+ return TransportResponse.success(
1485
+ data=result, meta=Meta(extra={'http_status': 200})
1486
+ );
1487
+ }
1488
+ # Register catch-all route for walkers with node parameter
1489
+ self.server.add_endpoint(
1490
+ JEndPoint(
1491
+ method=HTTPMethod.POST,
1492
+ path='/walker/{walker_name}/{node}',
1493
+ callback=dynamic_walker_handler,
1494
+ parameters=[
1495
+ APIParameter(
1496
+ name='walker_name',
1497
+ data_type='string',
1498
+ required=True,
1499
+ default=None,
1500
+ description='Name of the walker to execute',
1501
+ type=ParameterType.PATH
1502
+ ),
1503
+ APIParameter(
1504
+ name='node',
1505
+ data_type='string',
1506
+ required=True,
1507
+ default=None,
1508
+ description='Node ID to spawn walker on',
1509
+ type=ParameterType.PATH
1510
+ ),
1511
+ APIParameter(
1512
+ name='Authorization',
1513
+ data_type='string',
1514
+ required=False,
1515
+ default=None,
1516
+ description='Bearer token for authentication',
1517
+ type=ParameterType.HEADER
1518
+ )
1519
+ ],
1520
+ response_model=None,
1521
+ tags=['Walkers (Dynamic)'],
1522
+ summary='Execute walker on node (dynamic HMR)',
1523
+ description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
1524
+ )
1525
+ );
1526
+ # Register catch-all route for walkers without node parameter (root)
1527
+ self.server.add_endpoint(
1528
+ JEndPoint(
1529
+ method=HTTPMethod.POST,
1530
+ path='/walker/{walker_name}',
1531
+ callback=dynamic_walker_handler,
1532
+ parameters=[
1533
+ APIParameter(
1534
+ name='walker_name',
1535
+ data_type='string',
1536
+ required=True,
1537
+ default=None,
1538
+ description='Name of the walker to execute',
1539
+ type=ParameterType.PATH
1540
+ ),
1541
+ APIParameter(
1542
+ name='Authorization',
1543
+ data_type='string',
1544
+ required=False,
1545
+ default=None,
1546
+ description='Bearer token for authentication',
1547
+ type=ParameterType.HEADER
1548
+ )
1549
+ ],
1550
+ response_model=None,
1551
+ tags=['Walkers (Dynamic)'],
1552
+ summary='Execute walker on root (dynamic HMR)',
1553
+ description='Dynamically routes to any registered walker. Supports HMR - walker changes are reflected immediately.'
1554
+ )
1555
+ );
1556
+ }
1557
+
1558
+ """Register a single dynamic endpoint for all functions.
1559
+
1560
+ Similar to dynamic walker routing, this enables HMR for functions.
1561
+ """
1562
+ impl JacAPIServer.register_dynamic_function_endpoint -> None {
1563
+ import from fastapi.responses { JSONResponse }
1564
+ import from fastapi { Request }
1565
+ import from jaclang.runtimelib.transport { TransportResponse, Meta }
1566
+ async def dynamic_function_handler(
1567
+ request: Request, function_name: str, Authorization: str | None = None
1568
+ ) -> TransportResponse {
1569
+ # Parse request body to get function arguments
1570
+ try {
1571
+ body = await request.json();
1572
+ } except Exception {
1573
+ body = {};
1574
+ }
1575
+ kwargs: dict[str, Any] = dict(body) if body else {};
1576
+
1577
+ # Reload introspector if files changed (HMR)
1578
+ if self._hmr_pending {
1579
+ self.introspector.load(force_reload=True);
1580
+ self._hmr_pending = False;
1581
+ }
1582
+
1583
+ functions = self.get_functions();
1584
+
1585
+ if function_name not in functions {
1586
+ return TransportResponse.fail(
1587
+ code='NOT_FOUND',
1588
+ message=f"Function '{function_name}' not found. Available: {list(
1589
+ functions.keys()
1590
+ )}",
1591
+ meta=Meta(extra={'http_status': 404})
1592
+ );
1593
+ }
1594
+
1595
+ # Handle authentication
1596
+ username: str | None = None;
1597
+ authorization = kwargs.pop('Authorization', None);
1598
+ if self.introspector.is_auth_required_for_function(function_name) {
1599
+ token: str | None = None;
1600
+ if (
1601
+ Authorization
1602
+ and isinstance(Authorization, str)
1603
+ and Authorization.startswith('Bearer ')
1604
+ ) {
1605
+ token = Authorization[7:];
1606
+ }
1607
+ username = self.validate_jwt_token(token) if token else None;
1608
+ if not username {
1609
+ return TransportResponse.fail(
1610
+ code='UNAUTHORIZED',
1611
+ message='Unauthorized',
1612
+ meta=Meta(extra={'http_status': 401})
1613
+ );
1614
+ }
1615
+ }
1616
+
1617
+ result = self.execution_manager.execute_function(
1618
+ functions[function_name], kwargs, (username or '__guest__')
1619
+ );
1620
+ if 'error' in result {
1621
+ return TransportResponse.fail(
1622
+ code='EXECUTION_ERROR',
1623
+ message=result.get('error', 'Function execution failed'),
1624
+ details=result.get('traceback') if 'traceback' in result else None,
1625
+ meta=Meta(extra={'http_status': 500})
1626
+ );
1627
+ }
1628
+ return TransportResponse.success(
1629
+ data=result, meta=Meta(extra={'http_status': 200})
1630
+ );
1631
+ }
1632
+ self.server.add_endpoint(
1633
+ JEndPoint(
1634
+ method=HTTPMethod.POST,
1635
+ path='/function/{function_name}',
1636
+ callback=dynamic_function_handler,
1637
+ parameters=[
1638
+ APIParameter(
1639
+ name='function_name',
1640
+ data_type='string',
1641
+ required=True,
1642
+ default=None,
1643
+ description='Name of the function to call',
1644
+ type=ParameterType.PATH
1645
+ ),
1646
+ APIParameter(
1647
+ name='Authorization',
1648
+ data_type='string',
1649
+ required=False,
1650
+ default=None,
1651
+ description='Bearer token for authentication',
1652
+ type=ParameterType.HEADER
1653
+ )
1654
+ ],
1655
+ response_model=None,
1656
+ tags=['Functions (Dynamic)'],
1657
+ summary='Call function (dynamic HMR)',
1658
+ description='Dynamically routes to any registered function. Supports HMR - function changes are reflected immediately.'
1659
+ )
1660
+ );
1661
+ }
1662
+
1663
+ """Register endpoints for runtime introspection of available walkers/functions.
1664
+
1665
+ These endpoints allow clients to discover what walkers and functions are available,
1666
+ which is especially useful in HMR mode where the list can change dynamically.
1667
+ """
1668
+ impl JacAPIServer.register_dynamic_introspection_endpoints -> None {
1669
+ import from fastapi.responses { JSONResponse }
1670
+ def list_walkers -> dict[str, Any] {
1671
+ # Reload introspector if files changed (HMR)
1672
+ if self._hmr_pending {
1673
+ self.introspector.load(force_reload=True);
1674
+ self._hmr_pending = False;
1675
+ }
1676
+
1677
+ walkers = self.get_walkers();
1678
+ walker_info = {};
1679
+ for walker_name in walkers {
1680
+ try {
1681
+ info = self.introspector.introspect_walker(walkers[walker_name]);
1682
+ walker_info[walker_name] = {
1683
+ 'fields': info.get('fields', {}),
1684
+ 'requires_auth': self.introspector.is_auth_required_for_walker(
1685
+ walker_name
1686
+ )
1687
+ };
1688
+ } except Exception as e {
1689
+ walker_info[walker_name] = {'error': str(e)};
1690
+ }
1691
+ }
1692
+ return {'walkers': walker_info};
1693
+ }
1694
+ def list_functions -> dict[str, Any] {
1695
+ # Reload introspector if files changed (HMR)
1696
+ if self._hmr_pending {
1697
+ self.introspector.load(force_reload=True);
1698
+ self._hmr_pending = False;
1699
+ }
1700
+
1701
+ functions = self.get_functions();
1702
+ function_info = {};
1703
+ for func_name in functions {
1704
+ try {
1705
+ info = self.introspector.introspect_callable(functions[func_name]);
1706
+ function_info[func_name] = {
1707
+ 'parameters': info.get('parameters', {}),
1708
+ 'requires_auth': self.introspector.is_auth_required_for_function(
1709
+ func_name
1710
+ )
1711
+ };
1712
+ } except Exception as e {
1713
+ function_info[func_name] = {'error': str(e)};
1714
+ }
1715
+ }
1716
+ return {'functions': function_info};
1717
+ }
1718
+ def get_walker_info(walker_name: str) -> dict[str, Any] {
1719
+ # Reload introspector if files changed (HMR)
1720
+ if self._hmr_pending {
1721
+ self.introspector.load(force_reload=True);
1722
+ self._hmr_pending = False;
1723
+ }
1724
+
1725
+ walkers = self.get_walkers();
1726
+ if walker_name not in walkers {
1727
+ return {'error': f"Walker '{walker_name}' not found", 'status': 404};
1728
+ }
1729
+
1730
+ info = self.introspector.introspect_walker(walkers[walker_name]);
1731
+ return {
1732
+ 'name': walker_name,
1733
+ 'fields': info.get('fields', {}),
1734
+ 'requires_auth': self.introspector.is_auth_required_for_walker(walker_name)
1735
+ };
1736
+ }
1737
+ # List all walkers
1738
+ self.server.add_endpoint(
1739
+ JEndPoint(
1740
+ method=HTTPMethod.GET,
1741
+ path='/introspect/walkers',
1742
+ callback=list_walkers,
1743
+ parameters=[],
1744
+ response_model=None,
1745
+ tags=['Introspection'],
1746
+ summary='List available walkers',
1747
+ description='Returns a list of all available walkers with their field definitions. Supports HMR.'
1748
+ )
1749
+ );
1750
+ # List all functions
1751
+ self.server.add_endpoint(
1752
+ JEndPoint(
1753
+ method=HTTPMethod.GET,
1754
+ path='/introspect/functions',
1755
+ callback=list_functions,
1756
+ parameters=[],
1757
+ response_model=None,
1758
+ tags=['Introspection'],
1759
+ summary='List available functions',
1760
+ description='Returns a list of all available functions with their parameter definitions. Supports HMR.'
1761
+ )
1762
+ );
1763
+ # Get specific walker info
1764
+ self.server.add_endpoint(
1765
+ JEndPoint(
1766
+ method=HTTPMethod.GET,
1767
+ path='/introspect/walker/{walker_name}',
1768
+ callback=get_walker_info,
1769
+ parameters=[
1770
+ APIParameter(
1771
+ name='walker_name',
1772
+ data_type='string',
1773
+ required=True,
1774
+ default=None,
1775
+ description='Name of the walker to inspect',
1776
+ type=ParameterType.PATH
1777
+ )
1778
+ ],
1779
+ response_model=None,
1780
+ tags=['Introspection'],
1781
+ summary='Get walker information',
1782
+ description='Returns detailed information about a specific walker. Supports HMR.'
1783
+ )
1784
+ );
1785
+ }