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.
@@ -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