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.
- jac_scale/__init__.py +0 -0
- jac_scale/abstractions/config/app_config.jac +30 -0
- jac_scale/abstractions/config/base_config.jac +26 -0
- jac_scale/abstractions/database_provider.jac +51 -0
- jac_scale/abstractions/deployment_target.jac +64 -0
- jac_scale/abstractions/image_registry.jac +54 -0
- jac_scale/abstractions/logger.jac +20 -0
- jac_scale/abstractions/models/deployment_result.jac +27 -0
- jac_scale/abstractions/models/resource_status.jac +38 -0
- jac_scale/config_loader.jac +31 -0
- jac_scale/context.jac +14 -0
- jac_scale/factories/database_factory.jac +43 -0
- jac_scale/factories/deployment_factory.jac +43 -0
- jac_scale/factories/registry_factory.jac +32 -0
- jac_scale/factories/utility_factory.jac +34 -0
- jac_scale/impl/config_loader.impl.jac +131 -0
- jac_scale/impl/context.impl.jac +24 -0
- jac_scale/impl/memory_hierarchy.main.impl.jac +63 -0
- jac_scale/impl/memory_hierarchy.mongo.impl.jac +239 -0
- jac_scale/impl/memory_hierarchy.redis.impl.jac +186 -0
- jac_scale/impl/serve.impl.jac +1785 -0
- jac_scale/jserver/__init__.py +0 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +731 -0
- jac_scale/jserver/impl/jserver.impl.jac +79 -0
- jac_scale/jserver/jfast_api.jac +162 -0
- jac_scale/jserver/jserver.jac +101 -0
- jac_scale/memory_hierarchy.jac +138 -0
- jac_scale/plugin.jac +218 -0
- jac_scale/plugin_config.jac +175 -0
- jac_scale/providers/database/kubernetes_mongo.jac +137 -0
- jac_scale/providers/database/kubernetes_redis.jac +110 -0
- jac_scale/providers/registry/dockerhub.jac +64 -0
- jac_scale/serve.jac +118 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +215 -0
- jac_scale/targets/kubernetes/kubernetes_target.jac +841 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac +519 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.jac +85 -0
- jac_scale/tests/__init__.py +0 -0
- jac_scale/tests/conftest.py +29 -0
- jac_scale/tests/fixtures/test_api.jac +159 -0
- jac_scale/tests/fixtures/todo_app.jac +68 -0
- jac_scale/tests/test_abstractions.py +88 -0
- jac_scale/tests/test_deploy_k8s.py +265 -0
- jac_scale/tests/test_examples.py +484 -0
- jac_scale/tests/test_factories.py +149 -0
- jac_scale/tests/test_file_upload.py +444 -0
- jac_scale/tests/test_k8s_utils.py +156 -0
- jac_scale/tests/test_memory_hierarchy.py +247 -0
- jac_scale/tests/test_serve.py +1835 -0
- jac_scale/tests/test_sso.py +711 -0
- jac_scale/utilities/loggers/standard_logger.jac +40 -0
- jac_scale/utils.jac +16 -0
- jac_scale-0.1.1.dist-info/METADATA +658 -0
- jac_scale-0.1.1.dist-info/RECORD +57 -0
- jac_scale-0.1.1.dist-info/WHEEL +5 -0
- jac_scale-0.1.1.dist-info/entry_points.txt +3 -0
- 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
|
+
}
|