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