dominus-sdk-python-staging 2.1.6__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.
dominus/start.py ADDED
@@ -0,0 +1,1034 @@
1
+ """
2
+ CB Dominus SDK
3
+
4
+ Setup:
5
+ export DOMINUS_TOKEN="your_token"
6
+
7
+ Service URLs are hardcoded in the SDK (dominus/config/endpoints.py).
8
+ No need to set URL environment variables.
9
+
10
+ Note: In Infisical, the secret is stored as PROVISION_DOMINUS_TOKEN.
11
+ When fetching via dominus.secrets.get("PROVISION_DOMINUS_TOKEN"),
12
+ set the result as DOMINUS_TOKEN environment variable (drop PROVISION_ prefix).
13
+
14
+ Usage (Ultra-Flat API):
15
+ from dominus import dominus
16
+
17
+ # Secrets (root level)
18
+ value = await dominus.get("DB_URL")
19
+ await dominus.upsert("KEY", "value")
20
+
21
+ # Auth (root level)
22
+ await dominus.add_user(username="john", password="secret", role_id="...")
23
+ await dominus.add_scope(slug="read", display_name="Read", tenant_category_id=1)
24
+ await dominus.add_role(name="admin", scope_slugs=["read", "write"])
25
+ result = await dominus.verify_user_password(username="john", password="secret")
26
+
27
+ # SQL data - app schema (root level)
28
+ tables = await dominus.list_tables()
29
+ rows = await dominus.query_table("users")
30
+ await dominus.insert_row("users", {"name": "John"})
31
+
32
+ # SQL data - secure schema (secure namespace)
33
+ rows = await dominus.secure.query_table("patients")
34
+ await dominus.secure.insert_row("patients", {"mrn": "12345"})
35
+
36
+ # Schema DDL - app schema (root level)
37
+ await dominus.add_table("users", [{"name": "id", "type": "UUID"}])
38
+ await dominus.add_column("users", "email", "VARCHAR(255)")
39
+
40
+ # Schema DDL - secure schema (secure namespace)
41
+ await dominus.secure.add_table("patients", [...])
42
+
43
+ # Open DSN
44
+ dsn = await dominus.open.dsn()
45
+
46
+ # Health
47
+ status = await dominus.health.check()
48
+
49
+ Usage (String-based API - Backward Compatible):
50
+ result = await dominus("secrets.get", key="DB_URL")
51
+ """
52
+ import os
53
+ import httpx
54
+ import base64
55
+ import json
56
+ from typing import Optional, Any, Dict, List
57
+
58
+ # Module-level state
59
+ _VALIDATED = False
60
+ _TOKEN: Optional[str] = None
61
+ _BASE_URL: Optional[str] = None
62
+ _VALIDATION_ERROR: Optional[str] = None
63
+
64
+
65
+ # === MODULE-LEVEL INITIALIZATION ===
66
+ from .helpers.auth import _resolve_token
67
+ from .config.endpoints import BASE_URL
68
+
69
+ _TOKEN = _resolve_token()
70
+ _BASE_URL = BASE_URL
71
+
72
+ # Initialize cache encryption
73
+ from .helpers.cache import dominus_cache
74
+ if _TOKEN:
75
+ dominus_cache.set_encryption_key(_TOKEN)
76
+
77
+ # Validate token presence
78
+ if not _TOKEN:
79
+ _VALIDATION_ERROR = (
80
+ "DOMINUS_TOKEN not found.\n\n"
81
+ "Set the environment variable:\n"
82
+ " export DOMINUS_TOKEN=your_token\n\n"
83
+ "Or in production, set it via your deployment platform.\n"
84
+ )
85
+
86
+
87
+ class SecureNamespace:
88
+ """
89
+ Secure schema operations namespace.
90
+
91
+ Provides SQL data operations and schema DDL for the secure schema.
92
+ Used for PHI/sensitive data with audit logging.
93
+ """
94
+
95
+ def __init__(self, _execute_command):
96
+ self._execute = _execute_command
97
+
98
+ # ========================================
99
+ # SECURE SQL DATA OPERATIONS
100
+ # ========================================
101
+
102
+ async def list_tables(self) -> List[Dict[str, Any]]:
103
+ """List tables in the secure schema."""
104
+ return await self._execute("sql.secure.list_tables", schema="secure")
105
+
106
+ async def query_table(
107
+ self,
108
+ table_name: str,
109
+ filters: Optional[Dict[str, Any]] = None,
110
+ sort_by: Optional[str] = None,
111
+ sort_order: str = "ASC",
112
+ limit: int = 100,
113
+ offset: int = 0
114
+ ) -> Dict[str, Any]:
115
+ """Query table data from the secure schema."""
116
+ return await self._execute(
117
+ "sql.secure.query_table",
118
+ table_name=table_name,
119
+ schema="secure",
120
+ filters=filters,
121
+ sort_by=sort_by,
122
+ sort_order=sort_order,
123
+ limit=limit,
124
+ offset=offset
125
+ )
126
+
127
+ async def insert_row(
128
+ self,
129
+ table_name: str,
130
+ data: Dict[str, Any]
131
+ ) -> Dict[str, Any]:
132
+ """Insert a row into a table in the secure schema."""
133
+ return await self._execute(
134
+ "sql.secure.insert_row",
135
+ table_name=table_name,
136
+ data=data,
137
+ schema="secure"
138
+ )
139
+
140
+ async def update_rows(
141
+ self,
142
+ table_name: str,
143
+ data: Dict[str, Any],
144
+ filters: Dict[str, Any]
145
+ ) -> Dict[str, Any]:
146
+ """Update rows in a table in the secure schema."""
147
+ return await self._execute(
148
+ "sql.secure.update_rows",
149
+ table_name=table_name,
150
+ data=data,
151
+ filters=filters,
152
+ schema="secure"
153
+ )
154
+
155
+ async def delete_rows(
156
+ self,
157
+ table_name: str,
158
+ filters: Dict[str, Any]
159
+ ) -> Dict[str, Any]:
160
+ """
161
+ Delete rows from app schema (secure schema doesn't allow DELETE).
162
+
163
+ Note: This uses app schema even from secure namespace because
164
+ PHI data in secure schema uses soft deletes only.
165
+ """
166
+ return await self._execute(
167
+ "sql.secure.delete_rows",
168
+ table_name=table_name,
169
+ filters=filters,
170
+ schema="app"
171
+ )
172
+
173
+ async def list_columns(self, table_name: str) -> List[Dict[str, Any]]:
174
+ """List columns in a table in the secure schema."""
175
+ return await self._execute(
176
+ "sql.secure.list_columns",
177
+ table_name=table_name,
178
+ schema="secure"
179
+ )
180
+
181
+ async def get_table_size(self, table_name: str) -> Dict[str, Any]:
182
+ """Get table size information from the secure schema."""
183
+ return await self._execute(
184
+ "sql.secure.get_table_size",
185
+ table_name=table_name,
186
+ schema="secure"
187
+ )
188
+
189
+ # ========================================
190
+ # SECURE SCHEMA DDL OPERATIONS
191
+ # ========================================
192
+
193
+ async def add_table(
194
+ self,
195
+ table_name: str,
196
+ columns: List[Dict[str, Any]]
197
+ ) -> Dict[str, Any]:
198
+ """Create a new table in the secure schema."""
199
+ return await self._execute(
200
+ "schema.secure_add_table",
201
+ table_name=table_name,
202
+ columns=columns
203
+ )
204
+
205
+ async def delete_table(self, table_name: str) -> Dict[str, Any]:
206
+ """Drop a table from the secure schema."""
207
+ return await self._execute(
208
+ "schema.secure_delete_table",
209
+ table_name=table_name
210
+ )
211
+
212
+ async def ddl_list_tables(self) -> List[Dict[str, Any]]:
213
+ """List all tables in the secure schema (DDL view)."""
214
+ return await self._execute("schema.secure_list_tables")
215
+
216
+ async def ddl_list_columns(self, table_name: str) -> List[Dict[str, Any]]:
217
+ """List columns in a table (DDL view)."""
218
+ return await self._execute(
219
+ "schema.secure_list_columns",
220
+ table_name=table_name
221
+ )
222
+
223
+ async def add_column(
224
+ self,
225
+ table_name: str,
226
+ column_name: str,
227
+ column_type: str,
228
+ constraints: Optional[List[str]] = None,
229
+ default: Optional[str] = None
230
+ ) -> Dict[str, Any]:
231
+ """Add a column to a table in the secure schema."""
232
+ return await self._execute(
233
+ "schema.secure_add_column",
234
+ table_name=table_name,
235
+ column_name=column_name,
236
+ column_type=column_type,
237
+ constraints=constraints,
238
+ default=default
239
+ )
240
+
241
+ async def delete_column(
242
+ self,
243
+ table_name: str,
244
+ column_name: str
245
+ ) -> Dict[str, Any]:
246
+ """Drop a column from a table in the secure schema."""
247
+ return await self._execute(
248
+ "schema.secure_delete_column",
249
+ table_name=table_name,
250
+ column_name=column_name
251
+ )
252
+
253
+
254
+ class OpenNamespace:
255
+ """Open user namespace - Returns DSN string for dominus_open database."""
256
+
257
+ def __init__(self, _execute_command):
258
+ self._execute = _execute_command
259
+
260
+ async def dsn(self) -> str:
261
+ """
262
+ Get the full DSN connection string for open_user role.
263
+
264
+ Returns the complete PostgreSQL connection URI for the dominus_open
265
+ database that can be used directly by clients to connect.
266
+ """
267
+ return await self._execute("sql.open.get_dsn")
268
+
269
+
270
+ class HealthNamespace:
271
+ """Health check operations namespace."""
272
+
273
+ def __init__(self, _execute_command):
274
+ self._execute = _execute_command
275
+
276
+ async def check(self) -> Dict[str, Any]:
277
+ """Check health of all services."""
278
+ return await self._execute("health.check")
279
+
280
+
281
+ class Dominus:
282
+ """
283
+ Main SDK entry point with ultra-flat API.
284
+
285
+ Most operations are directly on the root object:
286
+ - dominus.get(), dominus.upsert() - secrets
287
+ - dominus.add_user(), dominus.add_scope() - auth
288
+ - dominus.query_table(), dominus.insert_row() - SQL (app schema)
289
+ - dominus.add_table(), dominus.add_column() - DDL (app schema)
290
+
291
+ Namespaces for specific use cases:
292
+ - dominus.secure.* - secure schema operations
293
+ - dominus.open.dsn() - open user DSN
294
+ - dominus.health.check() - health checks
295
+ """
296
+
297
+ def __init__(self):
298
+ """Initialize Dominus with flat API and minimal namespaces."""
299
+ # Create internal execute function that handles validation
300
+ async def _execute_command(command: str, **kwargs) -> Any:
301
+ """Internal command executor with validation"""
302
+ global _VALIDATED
303
+
304
+ if _VALIDATION_ERROR:
305
+ raise RuntimeError(_VALIDATION_ERROR)
306
+
307
+ # Validate on first call
308
+ if not _VALIDATED:
309
+ from .helpers.core import verify_token_format, health_check_all
310
+
311
+ # Basic token format check
312
+ verify_token_format(_TOKEN)
313
+
314
+ # Health check to warm up the server
315
+ result = await health_check_all(_BASE_URL)
316
+ if result["status"] != "healthy" and result["status"] != "ok":
317
+ raise RuntimeError(f"Services unhealthy: {result['message']}")
318
+
319
+ _VALIDATED = True
320
+
321
+ # Execute command
322
+ from .helpers.core import execute_command
323
+ return await execute_command(command, _TOKEN, _BASE_URL, **kwargs)
324
+
325
+ self._execute = _execute_command
326
+
327
+ # Initialize minimal namespaces
328
+ self.secure = SecureNamespace(_execute_command)
329
+ self.open = OpenNamespace(_execute_command)
330
+ self.health = HealthNamespace(_execute_command)
331
+
332
+ # Initialize portal namespace (needs client reference for _request)
333
+ from .namespaces.portal import PortalNamespace
334
+ self.portal = PortalNamespace(self)
335
+
336
+ # Cache for JWT public key
337
+ self._public_key_cache = None
338
+
339
+ # ========================================
340
+ # INTERNAL REQUEST METHOD (for user JWT)
341
+ # ========================================
342
+
343
+ async def _request(
344
+ self,
345
+ endpoint: str,
346
+ method: str = "POST",
347
+ body: Optional[Dict[str, Any]] = None,
348
+ user_token: Optional[str] = None
349
+ ) -> Dict[str, Any]:
350
+ """
351
+ Make an HTTP request to the orchestrator.
352
+
353
+ If user_token is provided, uses that JWT directly (for user-authenticated requests).
354
+ Otherwise, uses the service token flow (PSK -> JWT).
355
+
356
+ Args:
357
+ endpoint: API endpoint path (e.g., "/api/portal/auth/me")
358
+ method: HTTP method (GET, POST, PUT, DELETE)
359
+ body: Request body (will be base64-encoded)
360
+ user_token: Optional user JWT for user-authenticated requests
361
+
362
+ Returns:
363
+ Response data dict
364
+ """
365
+ global _VALIDATED
366
+
367
+ # Base64 helpers
368
+ def _b64_encode(d: dict) -> str:
369
+ return base64.b64encode(json.dumps(d).encode()).decode()
370
+
371
+ def _b64_decode(s: str) -> dict:
372
+ return json.loads(base64.b64decode(s.encode('utf-8')).decode('utf-8'))
373
+
374
+ # Determine which JWT to use
375
+ if user_token:
376
+ # Use the provided user JWT directly (no validation needed - orchestrator validates it)
377
+ jwt = user_token
378
+ else:
379
+ # Use service token flow - validate first if needed
380
+ if _VALIDATION_ERROR:
381
+ raise RuntimeError(_VALIDATION_ERROR)
382
+
383
+ if not _VALIDATED:
384
+ from .helpers.core import verify_token_format, health_check_all
385
+
386
+ # Basic token format check
387
+ verify_token_format(_TOKEN)
388
+
389
+ # Health check to warm up the server
390
+ result = await health_check_all(_BASE_URL)
391
+ if result["status"] != "healthy" and result["status"] != "ok":
392
+ raise RuntimeError(f"Services unhealthy: {result['message']}")
393
+
394
+ _VALIDATED = True
395
+
396
+ # Get service JWT
397
+ from .helpers.core import _ensure_valid_jwt
398
+ jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
399
+
400
+ # Prepare request
401
+ headers = {
402
+ "Authorization": f"Bearer {jwt}",
403
+ "Content-Type": "text/plain"
404
+ }
405
+
406
+ # Prepare body
407
+ body_b64 = _b64_encode(body or {})
408
+
409
+ # Make request
410
+ async with httpx.AsyncClient(base_url=_BASE_URL, headers=headers, timeout=30.0) as client:
411
+ if method == "GET":
412
+ response = await client.get(endpoint)
413
+ elif method == "DELETE":
414
+ response = await client.delete(endpoint)
415
+ elif method == "PUT":
416
+ response = await client.put(endpoint, content=body_b64)
417
+ else:
418
+ response = await client.post(endpoint, content=body_b64)
419
+
420
+ response.raise_for_status()
421
+
422
+ # Decode base64 response
423
+ result = _b64_decode(response.text)
424
+
425
+ # Check for success
426
+ if not result.get("success"):
427
+ error_msg = result.get("error", "Unknown error")
428
+ raise RuntimeError(f"Request error: {error_msg}")
429
+
430
+ return result.get("data", {})
431
+
432
+ # ========================================
433
+ # SECRETS (root level)
434
+ # ========================================
435
+
436
+ async def get(self, key: str) -> Any:
437
+ """
438
+ Get a secret value.
439
+
440
+ Args:
441
+ key: Secret key name
442
+
443
+ Returns:
444
+ Secret value
445
+ """
446
+ return await self._execute("secrets.get", key=key)
447
+
448
+ async def upsert(self, key: str, value: str) -> Dict[str, Any]:
449
+ """
450
+ Create or update a secret.
451
+
452
+ Args:
453
+ key: Secret key name
454
+ value: Secret value
455
+
456
+ Returns:
457
+ Operation result
458
+ """
459
+ return await self._execute("secrets.upsert", key=key, value=value)
460
+
461
+ # ========================================
462
+ # SQL DATA - APP SCHEMA (root level)
463
+ # ========================================
464
+
465
+ async def list_tables(self, schema: str = "app") -> List[Dict[str, Any]]:
466
+ """List tables in the app schema (or specified schema)."""
467
+ return await self._execute("sql.app.list_tables", schema=schema)
468
+
469
+ async def query_table(
470
+ self,
471
+ table_name: str,
472
+ schema: str = "app",
473
+ filters: Optional[Dict[str, Any]] = None,
474
+ sort_by: Optional[str] = None,
475
+ sort_order: str = "ASC",
476
+ limit: int = 100,
477
+ offset: int = 0
478
+ ) -> Dict[str, Any]:
479
+ """Query table data from the app schema (default)."""
480
+ return await self._execute(
481
+ "sql.app.query_table",
482
+ table_name=table_name,
483
+ schema=schema,
484
+ filters=filters,
485
+ sort_by=sort_by,
486
+ sort_order=sort_order,
487
+ limit=limit,
488
+ offset=offset
489
+ )
490
+
491
+ async def insert_row(
492
+ self,
493
+ table_name: str,
494
+ data: Dict[str, Any],
495
+ schema: str = "app"
496
+ ) -> Dict[str, Any]:
497
+ """Insert a row into a table in the app schema (default)."""
498
+ return await self._execute(
499
+ "sql.app.insert_row",
500
+ table_name=table_name,
501
+ data=data,
502
+ schema=schema
503
+ )
504
+
505
+ async def update_rows(
506
+ self,
507
+ table_name: str,
508
+ data: Dict[str, Any],
509
+ filters: Dict[str, Any],
510
+ schema: str = "app"
511
+ ) -> Dict[str, Any]:
512
+ """Update rows in a table in the app schema (default)."""
513
+ return await self._execute(
514
+ "sql.app.update_rows",
515
+ table_name=table_name,
516
+ data=data,
517
+ filters=filters,
518
+ schema=schema
519
+ )
520
+
521
+ async def delete_rows(
522
+ self,
523
+ table_name: str,
524
+ filters: Dict[str, Any],
525
+ schema: str = "app"
526
+ ) -> Dict[str, Any]:
527
+ """Delete rows from a table in the app schema (default)."""
528
+ return await self._execute(
529
+ "sql.app.delete_rows",
530
+ table_name=table_name,
531
+ filters=filters,
532
+ schema=schema
533
+ )
534
+
535
+ async def list_columns(
536
+ self,
537
+ table_name: str,
538
+ schema: str = "app"
539
+ ) -> List[Dict[str, Any]]:
540
+ """List columns in a table."""
541
+ return await self._execute(
542
+ "sql.app.list_columns",
543
+ table_name=table_name,
544
+ schema=schema
545
+ )
546
+
547
+ async def get_table_size(
548
+ self,
549
+ table_name: str,
550
+ schema: str = "app"
551
+ ) -> Dict[str, Any]:
552
+ """Get table size information."""
553
+ return await self._execute(
554
+ "sql.app.get_table_size",
555
+ table_name=table_name,
556
+ schema=schema
557
+ )
558
+
559
+ # ========================================
560
+ # SCHEMA DDL - APP SCHEMA (root level)
561
+ # ========================================
562
+
563
+ async def add_table(
564
+ self,
565
+ table_name: str,
566
+ columns: List[Dict[str, Any]]
567
+ ) -> Dict[str, Any]:
568
+ """
569
+ Create a new table in the app schema.
570
+
571
+ Args:
572
+ table_name: Name of table to create
573
+ columns: List of column definitions
574
+ [{"name": "id", "type": "UUID", "constraints": ["PRIMARY KEY"]}]
575
+ """
576
+ return await self._execute(
577
+ "schema.add_table",
578
+ table_name=table_name,
579
+ columns=columns
580
+ )
581
+
582
+ async def delete_table(self, table_name: str) -> Dict[str, Any]:
583
+ """Drop a table from the app schema."""
584
+ return await self._execute(
585
+ "schema.delete_table",
586
+ table_name=table_name
587
+ )
588
+
589
+ async def ddl_list_tables(self) -> List[Dict[str, Any]]:
590
+ """List all tables in the app schema (DDL view)."""
591
+ return await self._execute("schema.list_tables")
592
+
593
+ async def ddl_list_columns(self, table_name: str) -> List[Dict[str, Any]]:
594
+ """List columns in a table (DDL view)."""
595
+ return await self._execute(
596
+ "schema.list_columns",
597
+ table_name=table_name
598
+ )
599
+
600
+ async def add_column(
601
+ self,
602
+ table_name: str,
603
+ column_name: str,
604
+ column_type: str,
605
+ constraints: Optional[List[str]] = None,
606
+ default: Optional[str] = None
607
+ ) -> Dict[str, Any]:
608
+ """Add a column to a table in the app schema."""
609
+ return await self._execute(
610
+ "schema.add_column",
611
+ table_name=table_name,
612
+ column_name=column_name,
613
+ column_type=column_type,
614
+ constraints=constraints,
615
+ default=default
616
+ )
617
+
618
+ async def delete_column(
619
+ self,
620
+ table_name: str,
621
+ column_name: str
622
+ ) -> Dict[str, Any]:
623
+ """Drop a column from a table in the app schema."""
624
+ return await self._execute(
625
+ "schema.delete_column",
626
+ table_name=table_name,
627
+ column_name=column_name
628
+ )
629
+
630
+ # ========================================
631
+ # AUTH - SCOPES (root level)
632
+ # ========================================
633
+
634
+ async def add_scope(
635
+ self,
636
+ slug: str,
637
+ display_name: str,
638
+ tenant_category_id: int,
639
+ description: Optional[str] = None
640
+ ) -> Dict[str, Any]:
641
+ """Add a new scope."""
642
+ return await self._execute(
643
+ "auth.add_scope",
644
+ slug=slug,
645
+ display_name=display_name,
646
+ tenant_category_id=tenant_category_id,
647
+ description=description
648
+ )
649
+
650
+ async def delete_scope(self, scope_id: str) -> Dict[str, Any]:
651
+ """Delete a scope by ID."""
652
+ return await self._execute("auth.delete_scope", scope_id=scope_id)
653
+
654
+ async def list_scopes(
655
+ self,
656
+ tenant_category_id: Optional[int] = None
657
+ ) -> List[Dict[str, Any]]:
658
+ """List all scopes, optionally filtered by category."""
659
+ return await self._execute(
660
+ "auth.list_scopes",
661
+ tenant_category_id=tenant_category_id
662
+ )
663
+
664
+ async def get_scope(
665
+ self,
666
+ scope_id: Optional[str] = None,
667
+ slug: Optional[str] = None
668
+ ) -> Dict[str, Any]:
669
+ """Get a scope by ID or slug."""
670
+ return await self._execute(
671
+ "auth.get_scope",
672
+ scope_id=scope_id,
673
+ slug=slug
674
+ )
675
+
676
+ # ========================================
677
+ # AUTH - ROLES (root level)
678
+ # ========================================
679
+
680
+ async def add_role(
681
+ self,
682
+ name: str,
683
+ scope_slugs: Optional[List[str]] = None,
684
+ description: Optional[str] = None,
685
+ role_type: str = "user"
686
+ ) -> Dict[str, Any]:
687
+ """Add a new role."""
688
+ return await self._execute(
689
+ "auth.add_role",
690
+ name=name,
691
+ scope_slugs=scope_slugs or [],
692
+ description=description,
693
+ role_type=role_type
694
+ )
695
+
696
+ async def delete_role(self, role_id: str) -> Dict[str, Any]:
697
+ """Delete a role by ID."""
698
+ return await self._execute("auth.delete_role", role_id=role_id)
699
+
700
+ async def list_roles(self) -> List[Dict[str, Any]]:
701
+ """List all roles for the tenant."""
702
+ return await self._execute("auth.list_roles")
703
+
704
+ async def get_role(
705
+ self,
706
+ role_id: Optional[str] = None,
707
+ name: Optional[str] = None
708
+ ) -> Dict[str, Any]:
709
+ """Get a role by ID or name."""
710
+ return await self._execute(
711
+ "auth.get_role",
712
+ role_id=role_id,
713
+ name=name
714
+ )
715
+
716
+ async def update_role_scopes(
717
+ self,
718
+ role_id: str,
719
+ scope_slugs: List[str]
720
+ ) -> Dict[str, Any]:
721
+ """Update the scopes assigned to a role."""
722
+ return await self._execute(
723
+ "auth.update_role_scopes",
724
+ role_id=role_id,
725
+ scope_slugs=scope_slugs
726
+ )
727
+
728
+ # ========================================
729
+ # AUTH - USERS (root level)
730
+ # ========================================
731
+
732
+ async def add_user(
733
+ self,
734
+ username: str,
735
+ password: str,
736
+ role_id: str,
737
+ email: Optional[str] = None
738
+ ) -> Dict[str, Any]:
739
+ """
740
+ Add a new user.
741
+
742
+ Password is hashed client-side before sending to Architect.
743
+ """
744
+ from .helpers.crypto import hash_password
745
+ password_hash = hash_password(password)
746
+ return await self._execute(
747
+ "auth.add_user",
748
+ username=username,
749
+ password_hash=password_hash,
750
+ role_id=role_id,
751
+ email=email
752
+ )
753
+
754
+ async def delete_user(self, user_id: str) -> Dict[str, Any]:
755
+ """Delete a user by ID."""
756
+ return await self._execute("auth.delete_user", user_id=user_id)
757
+
758
+ async def list_users(self) -> List[Dict[str, Any]]:
759
+ """List all users for the tenant."""
760
+ return await self._execute("auth.list_users")
761
+
762
+ async def get_user(
763
+ self,
764
+ user_id: Optional[str] = None,
765
+ username: Optional[str] = None
766
+ ) -> Dict[str, Any]:
767
+ """Get a user by ID or username."""
768
+ return await self._execute(
769
+ "auth.get_user",
770
+ user_id=user_id,
771
+ username=username
772
+ )
773
+
774
+ async def update_user_status(
775
+ self,
776
+ user_id: str,
777
+ status: str
778
+ ) -> Dict[str, Any]:
779
+ """Update a user's status (active, inactive, suspended)."""
780
+ return await self._execute(
781
+ "auth.update_user_status",
782
+ user_id=user_id,
783
+ status=status
784
+ )
785
+
786
+ async def update_user_role(
787
+ self,
788
+ user_id: str,
789
+ role_id: str
790
+ ) -> Dict[str, Any]:
791
+ """Update a user's assigned role."""
792
+ return await self._execute(
793
+ "auth.update_user_role",
794
+ user_id=user_id,
795
+ role_id=role_id
796
+ )
797
+
798
+ async def verify_user_password(
799
+ self,
800
+ username: str,
801
+ password: str
802
+ ) -> Dict[str, Any]:
803
+ """
804
+ Verify a user's password.
805
+
806
+ Raw password is sent to Architect which does bcrypt comparison.
807
+ """
808
+ return await self._execute(
809
+ "auth.verify_user_password",
810
+ username=username,
811
+ password=password
812
+ )
813
+
814
+ # ========================================
815
+ # AUTH - CLIENTS / PSK (root level)
816
+ # ========================================
817
+
818
+ async def add_client(
819
+ self,
820
+ label: str,
821
+ role_id: str
822
+ ) -> Dict[str, Any]:
823
+ """
824
+ Add a new service client with PSK.
825
+
826
+ Returns {"client": {...}, "psk": "raw-psk-one-time-visible"}
827
+ """
828
+ from .helpers.crypto import hash_psk, generate_psk_local
829
+ raw_psk = generate_psk_local()
830
+ psk_hash = hash_psk(raw_psk)
831
+ client_result = await self._execute(
832
+ "auth.add_client",
833
+ label=label,
834
+ role_id=role_id,
835
+ psk_hash=psk_hash
836
+ )
837
+ return {
838
+ "client": client_result,
839
+ "psk": raw_psk # One-time visible
840
+ }
841
+
842
+ async def delete_client(self, client_id: str) -> Dict[str, Any]:
843
+ """Delete a client by ID."""
844
+ return await self._execute("auth.delete_client", client_id=client_id)
845
+
846
+ async def list_clients(self) -> List[Dict[str, Any]]:
847
+ """List all clients for the tenant."""
848
+ return await self._execute("auth.list_clients")
849
+
850
+ async def get_client(self, client_id: str) -> Dict[str, Any]:
851
+ """Get a client by ID."""
852
+ return await self._execute("auth.get_client", client_id=client_id)
853
+
854
+ async def regenerate_client_psk(self, client_id: str) -> Dict[str, Any]:
855
+ """
856
+ Regenerate a client's PSK.
857
+
858
+ Returns {"client": {...}, "psk": "new-raw-psk-one-time-visible"}
859
+ """
860
+ from .helpers.crypto import hash_psk, generate_psk_local
861
+ raw_psk = generate_psk_local()
862
+ psk_hash = hash_psk(raw_psk)
863
+ client_result = await self._execute(
864
+ "auth.regenerate_client_psk",
865
+ client_id=client_id,
866
+ psk_hash=psk_hash
867
+ )
868
+ return {
869
+ "client": client_result,
870
+ "psk": raw_psk
871
+ }
872
+
873
+ async def verify_client_psk(
874
+ self,
875
+ client_id: str,
876
+ psk: str
877
+ ) -> Dict[str, Any]:
878
+ """
879
+ Verify a client's PSK.
880
+
881
+ Raw PSK is sent to Architect which does bcrypt comparison.
882
+ """
883
+ return await self._execute(
884
+ "auth.verify_client_psk",
885
+ client_id=client_id,
886
+ psk=psk
887
+ )
888
+
889
+ # ========================================
890
+ # AUTH - REFRESH TOKENS (root level)
891
+ # ========================================
892
+
893
+ async def add_refresh_token(
894
+ self,
895
+ user_id: Optional[str] = None,
896
+ client_psk_id: Optional[str] = None,
897
+ expires_in_seconds: int = 86400 * 30 # 30 days
898
+ ) -> Dict[str, Any]:
899
+ """
900
+ Create a new refresh token.
901
+
902
+ Either user_id or client_psk_id must be provided.
903
+ Returns {"token_id": "...", "token": "raw-token-one-time-visible"}
904
+ """
905
+ from .helpers.crypto import hash_token, generate_token
906
+ raw_token = generate_token()
907
+ token_hash = hash_token(raw_token)
908
+
909
+ result = await self._execute(
910
+ "auth.add_refresh_token",
911
+ user_id=user_id,
912
+ client_psk_id=client_psk_id,
913
+ token_hash=token_hash,
914
+ expires_in_seconds=expires_in_seconds
915
+ )
916
+ return {
917
+ "token_id": result.get("id"),
918
+ "token": raw_token,
919
+ "expires_at": result.get("expires_at")
920
+ }
921
+
922
+ async def delete_refresh_token(self, token_id: str) -> Dict[str, Any]:
923
+ """Delete/revoke a refresh token."""
924
+ return await self._execute(
925
+ "auth.delete_refresh_token",
926
+ token_id=token_id
927
+ )
928
+
929
+ async def list_refresh_tokens(
930
+ self,
931
+ user_id: Optional[str] = None,
932
+ client_psk_id: Optional[str] = None
933
+ ) -> List[Dict[str, Any]]:
934
+ """List refresh tokens, optionally filtered by user or client."""
935
+ return await self._execute(
936
+ "auth.list_refresh_tokens",
937
+ user_id=user_id,
938
+ client_psk_id=client_psk_id
939
+ )
940
+
941
+ # ========================================
942
+ # AUTH - JWT OPERATIONS (root level)
943
+ # ========================================
944
+
945
+ async def mint_jwt(
946
+ self,
947
+ user_id: str,
948
+ scope: Optional[List[str]] = None,
949
+ expires_in: int = 900,
950
+ system: str = "user"
951
+ ) -> Dict[str, Any]:
952
+ """
953
+ Mint a JWT for a subsidiary user.
954
+
955
+ Returns {"access_token": "...", "token_type": "Bearer", "expires_in": ...}
956
+ """
957
+ return await self._execute(
958
+ "auth.mint",
959
+ user_id=user_id,
960
+ scope=scope,
961
+ system=system
962
+ )
963
+
964
+ async def get_public_key(self) -> str:
965
+ """Get Sovereign's public key for JWT validation (cached)."""
966
+ if self._public_key_cache:
967
+ return self._public_key_cache
968
+
969
+ result = await self._execute("auth.jwks")
970
+ self._public_key_cache = result.get("public_key")
971
+ return self._public_key_cache
972
+
973
+ async def validate_jwt(self, token: str) -> Dict[str, Any]:
974
+ """
975
+ Validate a JWT and return its claims.
976
+
977
+ Raises ValueError if token is invalid or expired.
978
+ """
979
+ import jwt as pyjwt
980
+
981
+ public_key = await self.get_public_key()
982
+
983
+ try:
984
+ claims = pyjwt.decode(
985
+ token,
986
+ public_key,
987
+ algorithms=["RS256"],
988
+ options={"verify_exp": True}
989
+ )
990
+ return claims
991
+ except pyjwt.ExpiredSignatureError:
992
+ raise ValueError("Token has expired")
993
+ except pyjwt.InvalidTokenError as e:
994
+ raise ValueError(f"Invalid token: {e}")
995
+
996
+ # ========================================
997
+ # STRING-BASED API (backward compatible)
998
+ # ========================================
999
+
1000
+ async def __call__(self, command: str, **kwargs) -> Any:
1001
+ """
1002
+ String-based command execution (backward compatible).
1003
+
1004
+ Examples:
1005
+ await dominus("health.check")
1006
+ await dominus("secrets.get", key="DATABASE_URL")
1007
+ await dominus("sql.app.list_tables", schema="app")
1008
+ """
1009
+ global _VALIDATED
1010
+
1011
+ if _VALIDATION_ERROR:
1012
+ raise RuntimeError(_VALIDATION_ERROR)
1013
+
1014
+ # Validate on first call
1015
+ if not _VALIDATED:
1016
+ from .helpers.core import verify_token_format, health_check_all
1017
+
1018
+ # Basic token format check
1019
+ verify_token_format(_TOKEN)
1020
+
1021
+ # Health check to warm up the server
1022
+ result = await health_check_all(_BASE_URL)
1023
+ if result["status"] != "healthy" and result["status"] != "ok":
1024
+ raise RuntimeError(f"Services unhealthy: {result['message']}")
1025
+
1026
+ _VALIDATED = True
1027
+
1028
+ # Execute command
1029
+ from .helpers.core import execute_command
1030
+ return await execute_command(command, _TOKEN, _BASE_URL, **kwargs)
1031
+
1032
+
1033
+ # Create singleton instance
1034
+ dominus = Dominus()