kdcube-cli 0.0.0__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.
- kdcube_cli/__init__.py +1 -0
- kdcube_cli/banner.py +476 -0
- kdcube_cli/cli.py +5468 -0
- kdcube_cli/export_live_bundles.py +500 -0
- kdcube_cli/frontend_config.py +387 -0
- kdcube_cli/installer.py +4959 -0
- kdcube_cli/tty_keys.py +74 -0
- kdcube_cli-0.0.0.dist-info/METADATA +5 -0
- kdcube_cli-0.0.0.dist-info/RECORD +12 -0
- kdcube_cli-0.0.0.dist-info/WHEEL +5 -0
- kdcube_cli-0.0.0.dist-info/licenses/LICENSE +21 -0
- kdcube_cli-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping, Optional, Union
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def as_text(value: Any) -> str:
|
|
13
|
+
return str(value or "").strip()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"as_text",
|
|
18
|
+
"is_placeholder",
|
|
19
|
+
"normalize_routes_prefix",
|
|
20
|
+
"get_nested",
|
|
21
|
+
"deep_merge",
|
|
22
|
+
"load_yaml_descriptor",
|
|
23
|
+
"load_json_file",
|
|
24
|
+
"build_frontend_config",
|
|
25
|
+
"build_frontend_config_from_assembly",
|
|
26
|
+
"write_frontend_config_file",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_placeholder(value: Optional[str]) -> bool:
|
|
31
|
+
if value is None:
|
|
32
|
+
return True
|
|
33
|
+
stripped = value.strip().strip("'\"")
|
|
34
|
+
if not stripped:
|
|
35
|
+
return True
|
|
36
|
+
if stripped.upper() in {"TENANT_ID", "PROJECT_ID"}:
|
|
37
|
+
return True
|
|
38
|
+
if "<" in stripped and ">" in stripped:
|
|
39
|
+
return True
|
|
40
|
+
lowered = stripped.lower()
|
|
41
|
+
if "/absolute/path" in lowered or "absolute/path" in lowered:
|
|
42
|
+
return True
|
|
43
|
+
if "path/to/" in lowered or lowered.startswith("path/to"):
|
|
44
|
+
return True
|
|
45
|
+
if "relative_path" in lowered:
|
|
46
|
+
return True
|
|
47
|
+
if "platform-repo/" in stripped or "frontend-repo/" in stripped:
|
|
48
|
+
return True
|
|
49
|
+
if "..." in stripped:
|
|
50
|
+
return True
|
|
51
|
+
if "changeme" in lowered:
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def normalize_routes_prefix(value: Any, default: str = "/chatbot") -> str:
|
|
57
|
+
raw = as_text(value) or default
|
|
58
|
+
if raw == "/":
|
|
59
|
+
return ""
|
|
60
|
+
return "/" + raw.strip("/")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_nested(data: Any, *path: str) -> Any:
|
|
64
|
+
cur = data
|
|
65
|
+
for key in path:
|
|
66
|
+
if not isinstance(cur, Mapping):
|
|
67
|
+
return None
|
|
68
|
+
cur = cur.get(key)
|
|
69
|
+
return cur
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def deep_merge(base: Optional[Mapping[str, Any]], overlay: Optional[Mapping[str, Any]]) -> dict[str, Any]:
|
|
73
|
+
result: dict[str, Any] = copy.deepcopy(dict(base or {}))
|
|
74
|
+
for key, value in dict(overlay or {}).items():
|
|
75
|
+
if isinstance(value, Mapping) and isinstance(result.get(key), Mapping):
|
|
76
|
+
result[key] = deep_merge(result.get(key), value)
|
|
77
|
+
else:
|
|
78
|
+
result[key] = copy.deepcopy(value)
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_yaml_descriptor(path: Union[Path, str]) -> dict[str, Any]:
|
|
83
|
+
descriptor_path = Path(path).expanduser()
|
|
84
|
+
data = yaml.safe_load(descriptor_path.read_text()) if descriptor_path.exists() else {}
|
|
85
|
+
return data if isinstance(data, dict) else {}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_json_file(path: Optional[Union[Path, str]]) -> dict[str, Any]:
|
|
89
|
+
if not path:
|
|
90
|
+
return {}
|
|
91
|
+
json_path = Path(path).expanduser()
|
|
92
|
+
if not json_path.exists():
|
|
93
|
+
return {}
|
|
94
|
+
try:
|
|
95
|
+
data = json.loads(json_path.read_text())
|
|
96
|
+
except Exception:
|
|
97
|
+
return {}
|
|
98
|
+
return data if isinstance(data, dict) else {}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _frontend_config_overrides(assembly: Optional[Mapping[str, Any]]) -> dict[str, Any]:
|
|
102
|
+
frontend = get_nested(assembly or {}, "frontend")
|
|
103
|
+
overrides: dict[str, Any] = {}
|
|
104
|
+
if isinstance(frontend, Mapping):
|
|
105
|
+
auth = frontend.get("auth")
|
|
106
|
+
if isinstance(auth, Mapping):
|
|
107
|
+
auth_overrides = copy.deepcopy(dict(auth))
|
|
108
|
+
for source, target in (
|
|
109
|
+
("totp_app_name", "totpAppName"),
|
|
110
|
+
("totp_issuer", "totpIssuer"),
|
|
111
|
+
("api_base", "apiBase"),
|
|
112
|
+
):
|
|
113
|
+
if source in auth_overrides and target not in auth_overrides:
|
|
114
|
+
auth_overrides[target] = auth_overrides[source]
|
|
115
|
+
auth_overrides.pop(source, None)
|
|
116
|
+
overrides["auth"] = auth_overrides
|
|
117
|
+
for key in (
|
|
118
|
+
"routesPrefix",
|
|
119
|
+
"routes_prefix",
|
|
120
|
+
"apiBase",
|
|
121
|
+
"api_base",
|
|
122
|
+
"supportEmail",
|
|
123
|
+
"support_email",
|
|
124
|
+
"userRegistrationBundleId",
|
|
125
|
+
"user_registration_bundle_id",
|
|
126
|
+
"debug",
|
|
127
|
+
):
|
|
128
|
+
if key in frontend:
|
|
129
|
+
target_key = {
|
|
130
|
+
"routes_prefix": "routesPrefix",
|
|
131
|
+
"api_base": "apiBase",
|
|
132
|
+
"support_email": "supportEmail",
|
|
133
|
+
"user_registration_bundle_id": "userRegistrationBundleId",
|
|
134
|
+
}.get(key, key)
|
|
135
|
+
overrides[target_key] = copy.deepcopy(frontend.get(key))
|
|
136
|
+
raw = get_nested(assembly or {}, "frontend", "config")
|
|
137
|
+
if isinstance(raw, dict):
|
|
138
|
+
overrides = deep_merge(overrides, raw)
|
|
139
|
+
return overrides
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _assembly_auth_type(assembly: Optional[Mapping[str, Any]]) -> str:
|
|
143
|
+
auth_type = as_text(get_nested(assembly or {}, "auth", "type")).lower()
|
|
144
|
+
auth_idp = as_text(get_nested(assembly or {}, "auth", "idp")).lower()
|
|
145
|
+
if auth_type == "delegated":
|
|
146
|
+
return "delegated"
|
|
147
|
+
if auth_type in {"bundle", "bundle-session"} or auth_idp in {"session", "bundle", "bundle-session"}:
|
|
148
|
+
return "bundle"
|
|
149
|
+
if auth_type == "simple" or auth_idp == "simple":
|
|
150
|
+
return "simple"
|
|
151
|
+
if auth_type == "cognito" or auth_idp == "cognito":
|
|
152
|
+
return "cognito"
|
|
153
|
+
return "simple"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _assembly_auth_declared(assembly: Optional[Mapping[str, Any]]) -> bool:
|
|
157
|
+
return bool(as_text(get_nested(assembly or {}, "auth", "type")) or as_text(get_nested(assembly or {}, "auth", "idp")))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _normalize_frontend_auth_type(value: Any) -> str:
|
|
161
|
+
auth_type = as_text(value).lower()
|
|
162
|
+
if auth_type == "hardcoded":
|
|
163
|
+
return "simple"
|
|
164
|
+
return auth_type
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _build_oidc_authority(region: str, user_pool_id: str) -> str:
|
|
168
|
+
if region and user_pool_id:
|
|
169
|
+
return f"https://cognito-idp.{region}.amazonaws.com/{user_pool_id}"
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def build_frontend_config(
|
|
174
|
+
*,
|
|
175
|
+
tenant: str,
|
|
176
|
+
project: str,
|
|
177
|
+
assembly: Optional[Mapping[str, Any]] = None,
|
|
178
|
+
template_data: Optional[Mapping[str, Any]] = None,
|
|
179
|
+
existing_data: Optional[Mapping[str, Any]] = None,
|
|
180
|
+
token: str = "test-admin-token-123",
|
|
181
|
+
cognito_region: Optional[str] = None,
|
|
182
|
+
cognito_user_pool_id: Optional[str] = None,
|
|
183
|
+
cognito_app_client_id: Optional[str] = None,
|
|
184
|
+
routes_prefix: Optional[str] = None,
|
|
185
|
+
company_name: Optional[str] = None,
|
|
186
|
+
turnstile_development_token: Optional[str] = None,
|
|
187
|
+
auth_token_cookie_name: Optional[str] = None,
|
|
188
|
+
id_token_cookie_name: Optional[str] = None,
|
|
189
|
+
platform_auth_config: Optional[Mapping[str, Any]] = None,
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
"""
|
|
192
|
+
Build the public frontend runtime config.
|
|
193
|
+
|
|
194
|
+
Merge order is template < existing file < assembly.frontend.config, then the
|
|
195
|
+
installer-owned fields are normalized from assembly/env inputs.
|
|
196
|
+
"""
|
|
197
|
+
frontend_overrides = _frontend_config_overrides(assembly)
|
|
198
|
+
merged = deep_merge(template_data, existing_data)
|
|
199
|
+
merged = deep_merge(merged, frontend_overrides)
|
|
200
|
+
|
|
201
|
+
merged["tenant"] = tenant
|
|
202
|
+
merged["project"] = project
|
|
203
|
+
if "tenant_id" in merged:
|
|
204
|
+
merged["tenant_id"] = tenant
|
|
205
|
+
if "project_id" in merged:
|
|
206
|
+
merged["project_id"] = project
|
|
207
|
+
|
|
208
|
+
override_routes = frontend_overrides.get("routesPrefix") or frontend_overrides.get("routes_prefix")
|
|
209
|
+
if override_routes:
|
|
210
|
+
merged["routesPrefix"] = normalize_routes_prefix(override_routes)
|
|
211
|
+
elif routes_prefix:
|
|
212
|
+
merged["routesPrefix"] = normalize_routes_prefix(routes_prefix)
|
|
213
|
+
else:
|
|
214
|
+
merged["routesPrefix"] = normalize_routes_prefix(merged.get("routesPrefix"))
|
|
215
|
+
|
|
216
|
+
assembly_company = as_text(get_nested(assembly or {}, "company"))
|
|
217
|
+
company = as_text(company_name) or assembly_company or "KDCube"
|
|
218
|
+
auth = copy.deepcopy(merged.get("auth") if isinstance(merged.get("auth"), dict) else {})
|
|
219
|
+
explicit_frontend_auth_type = get_nested(frontend_overrides, "auth", "authType")
|
|
220
|
+
if as_text(explicit_frontend_auth_type):
|
|
221
|
+
auth_type = _normalize_frontend_auth_type(explicit_frontend_auth_type)
|
|
222
|
+
elif _assembly_auth_declared(assembly):
|
|
223
|
+
auth_type = _assembly_auth_type(assembly)
|
|
224
|
+
else:
|
|
225
|
+
auth_type = _normalize_frontend_auth_type(auth.get("authType")) or _assembly_auth_type(assembly)
|
|
226
|
+
auth["authType"] = auth_type
|
|
227
|
+
|
|
228
|
+
provider_auth = dict(platform_auth_config or {})
|
|
229
|
+
id_token_header = (
|
|
230
|
+
as_text(provider_auth.get("id_token_header_name"))
|
|
231
|
+
or as_text(get_nested(assembly or {}, "auth", "id_token_header_name"))
|
|
232
|
+
or "X-ID-Token"
|
|
233
|
+
)
|
|
234
|
+
if auth_type == "simple":
|
|
235
|
+
if auth.get("token") in (None, "", "test-admin-token-123"):
|
|
236
|
+
auth["token"] = token
|
|
237
|
+
elif auth_type in {"cognito", "oauth"}:
|
|
238
|
+
auth.pop("token", None)
|
|
239
|
+
oidc_cfg = copy.deepcopy(auth.get("oidcConfig") if isinstance(auth.get("oidcConfig"), dict) else {})
|
|
240
|
+
region = (
|
|
241
|
+
as_text(cognito_region)
|
|
242
|
+
or as_text(provider_auth.get("region"))
|
|
243
|
+
or as_text(get_nested(assembly or {}, "auth", "cognito", "region"))
|
|
244
|
+
)
|
|
245
|
+
user_pool_id = (
|
|
246
|
+
as_text(cognito_user_pool_id)
|
|
247
|
+
or as_text(provider_auth.get("user_pool_id"))
|
|
248
|
+
or as_text(get_nested(assembly or {}, "auth", "cognito", "user_pool_id"))
|
|
249
|
+
)
|
|
250
|
+
client_id = (
|
|
251
|
+
as_text(cognito_app_client_id)
|
|
252
|
+
or as_text(provider_auth.get("app_client_id"))
|
|
253
|
+
or as_text(get_nested(assembly or {}, "auth", "cognito", "app_client_id"))
|
|
254
|
+
)
|
|
255
|
+
authority = _build_oidc_authority(region, user_pool_id)
|
|
256
|
+
if authority:
|
|
257
|
+
oidc_cfg["authority"] = authority
|
|
258
|
+
if client_id:
|
|
259
|
+
oidc_cfg["client_id"] = client_id
|
|
260
|
+
auth["idTokenHeaderName"] = as_text(auth.get("idTokenHeaderName")) or id_token_header
|
|
261
|
+
auth["oidcConfig"] = oidc_cfg
|
|
262
|
+
elif auth_type == "delegated":
|
|
263
|
+
auth.pop("token", None)
|
|
264
|
+
if auth.get("totpAppName") in (None, "", "COMPANY_NAME", "<COMPANY_NAME>"):
|
|
265
|
+
auth["totpAppName"] = company
|
|
266
|
+
if auth.get("totpIssuer") in (None, "", "COMPANY_NAME", "<COMPANY_NAME>"):
|
|
267
|
+
auth["totpIssuer"] = company
|
|
268
|
+
auth.setdefault("apiBase", "/auth/")
|
|
269
|
+
elif auth_type == "bundle":
|
|
270
|
+
auth.pop("token", None)
|
|
271
|
+
connection_hub_ref = (
|
|
272
|
+
auth.get("connectionHub")
|
|
273
|
+
if isinstance(auth.get("connectionHub"), Mapping)
|
|
274
|
+
else get_nested(assembly or {}, "auth", "connection_hub")
|
|
275
|
+
or get_nested(assembly or {}, "auth", "connectionHub")
|
|
276
|
+
or get_nested(assembly or {}, "auth", "bundle_session", "connection_hub")
|
|
277
|
+
)
|
|
278
|
+
if isinstance(connection_hub_ref, Mapping):
|
|
279
|
+
raw_ref = copy.deepcopy(dict(connection_hub_ref))
|
|
280
|
+
normalized_ref: dict[str, Any] = {}
|
|
281
|
+
for source, target in (
|
|
282
|
+
("bundle_id", "bundleId"),
|
|
283
|
+
("authority_id", "authorityId"),
|
|
284
|
+
("provider_id", "providerId"),
|
|
285
|
+
("provider_type", "providerType"),
|
|
286
|
+
("entrypoint", "entrypoint"),
|
|
287
|
+
):
|
|
288
|
+
value = raw_ref.get(source, raw_ref.get(target))
|
|
289
|
+
if value not in (None, ""):
|
|
290
|
+
normalized_ref[target] = value
|
|
291
|
+
if normalized_ref:
|
|
292
|
+
auth["connectionHub"] = normalized_ref
|
|
293
|
+
login_url = (
|
|
294
|
+
as_text(auth.get("loginUrl"))
|
|
295
|
+
or as_text(get_nested(assembly or {}, "auth", "login_url"))
|
|
296
|
+
or as_text(get_nested(assembly or {}, "auth", "login", "url"))
|
|
297
|
+
or as_text(get_nested(assembly or {}, "auth", "bundle_session", "login_url"))
|
|
298
|
+
)
|
|
299
|
+
if login_url:
|
|
300
|
+
auth["loginUrl"] = login_url
|
|
301
|
+
|
|
302
|
+
turnstile_token = (
|
|
303
|
+
as_text(turnstile_development_token)
|
|
304
|
+
or as_text(get_nested(assembly or {}, "auth", "turnstile_development_token"))
|
|
305
|
+
)
|
|
306
|
+
if turnstile_token and not is_placeholder(turnstile_token):
|
|
307
|
+
auth["turnstileDevelopmentToken"] = turnstile_token
|
|
308
|
+
elif is_placeholder(as_text(auth.get("turnstileDevelopmentToken"))):
|
|
309
|
+
auth.pop("turnstileDevelopmentToken", None)
|
|
310
|
+
|
|
311
|
+
# Non-masquerade auth cookie names, so a parent page (e.g. a co-located
|
|
312
|
+
# landing site) can set exactly the cookies the proxy reads for the
|
|
313
|
+
# embedded same-origin widgets. Source: assembly auth.*_cookie_name with the
|
|
314
|
+
# platform defaults.
|
|
315
|
+
auth["authTokenCookieName"] = (
|
|
316
|
+
as_text(auth_token_cookie_name)
|
|
317
|
+
or as_text(provider_auth.get("auth_token_cookie_name"))
|
|
318
|
+
or as_text(get_nested(assembly or {}, "auth", "auth_token_cookie_name"))
|
|
319
|
+
or "__Secure-LATC"
|
|
320
|
+
)
|
|
321
|
+
auth["idTokenCookieName"] = (
|
|
322
|
+
as_text(id_token_cookie_name)
|
|
323
|
+
or as_text(provider_auth.get("id_token_cookie_name"))
|
|
324
|
+
or as_text(get_nested(assembly or {}, "auth", "id_token_cookie_name"))
|
|
325
|
+
or "__Secure-LITC"
|
|
326
|
+
)
|
|
327
|
+
auth["profileUrl"] = as_text(auth.get("profileUrl")) or "/profile"
|
|
328
|
+
auth["logoutUrl"] = as_text(auth.get("logoutUrl")) or "/api/platform/logout"
|
|
329
|
+
|
|
330
|
+
merged["auth"] = auth
|
|
331
|
+
|
|
332
|
+
platform_ref = as_text(get_nested(assembly or {}, "platform", "ref"))
|
|
333
|
+
if platform_ref:
|
|
334
|
+
versions = copy.deepcopy(merged.get("versions") if isinstance(merged.get("versions"), dict) else {})
|
|
335
|
+
versions["platform"] = platform_ref
|
|
336
|
+
merged["versions"] = versions
|
|
337
|
+
|
|
338
|
+
return merged
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def build_frontend_config_from_assembly(
|
|
342
|
+
assembly: Mapping[str, Any],
|
|
343
|
+
*,
|
|
344
|
+
template_data: Optional[Mapping[str, Any]] = None,
|
|
345
|
+
existing_data: Optional[Mapping[str, Any]] = None,
|
|
346
|
+
token: str = "test-admin-token-123",
|
|
347
|
+
cognito_region: Optional[str] = None,
|
|
348
|
+
cognito_user_pool_id: Optional[str] = None,
|
|
349
|
+
cognito_app_client_id: Optional[str] = None,
|
|
350
|
+
routes_prefix: Optional[str] = None,
|
|
351
|
+
company_name: Optional[str] = None,
|
|
352
|
+
turnstile_development_token: Optional[str] = None,
|
|
353
|
+
platform_auth_config: Optional[Mapping[str, Any]] = None,
|
|
354
|
+
) -> dict[str, Any]:
|
|
355
|
+
tenant = as_text(get_nested(assembly, "context", "tenant")) or "demo-tenant"
|
|
356
|
+
project = as_text(get_nested(assembly, "context", "project")) or "demo-project"
|
|
357
|
+
route_prefix = routes_prefix
|
|
358
|
+
if route_prefix is None:
|
|
359
|
+
route_prefix = as_text(get_nested(assembly, "proxy", "route_prefix"))
|
|
360
|
+
return build_frontend_config(
|
|
361
|
+
tenant=tenant,
|
|
362
|
+
project=project,
|
|
363
|
+
assembly=assembly,
|
|
364
|
+
template_data=template_data,
|
|
365
|
+
existing_data=existing_data,
|
|
366
|
+
token=token,
|
|
367
|
+
cognito_region=cognito_region,
|
|
368
|
+
cognito_user_pool_id=cognito_user_pool_id,
|
|
369
|
+
cognito_app_client_id=cognito_app_client_id,
|
|
370
|
+
routes_prefix=route_prefix or None,
|
|
371
|
+
company_name=company_name,
|
|
372
|
+
turnstile_development_token=turnstile_development_token,
|
|
373
|
+
platform_auth_config=platform_auth_config,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def write_frontend_config_file(path: Union[Path, str], **kwargs: Any) -> dict[str, Any]:
|
|
378
|
+
target = Path(path).expanduser()
|
|
379
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
380
|
+
template_path = kwargs.pop("template_path", None)
|
|
381
|
+
config = build_frontend_config(
|
|
382
|
+
template_data=load_json_file(template_path),
|
|
383
|
+
existing_data=load_json_file(target),
|
|
384
|
+
**kwargs,
|
|
385
|
+
)
|
|
386
|
+
target.write_text(json.dumps(config, indent=2) + "\n")
|
|
387
|
+
return config
|