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.
@@ -0,0 +1,1341 @@
1
+ """SQL operations namespace - Role-based database operations"""
2
+ from typing import Dict, Any, List, Optional
3
+
4
+ from ..helpers.crypto import hash_password, hash_psk, hash_token, generate_token
5
+
6
+
7
+ class SQLNamespace:
8
+ """SQL operations namespace"""
9
+
10
+ def __init__(self, _execute_command, _execute_sovereign_command=None):
11
+ """
12
+ Initialize SQL namespace.
13
+
14
+ Args:
15
+ _execute_command: Internal function to execute commands (routes to Architect)
16
+ _execute_sovereign_command: Internal function for Sovereign commands (optional)
17
+ """
18
+ self._execute = _execute_command
19
+ self._execute_sovereign = _execute_sovereign_command
20
+ self.app = SQLRoleNamespace("app", _execute_command)
21
+ self.secure = SQLRoleNamespace("secure", _execute_command)
22
+ self.secure_machine = SQLRoleNamespace("secure_machine", _execute_command)
23
+ self.auth = SQLAuthNamespace(_execute_command, _execute_sovereign_command)
24
+ self.schema = SQLSchemaNamespace(_execute_command)
25
+ self.open = SQLOpenNamespace(_execute_command)
26
+
27
+
28
+ class SQLRoleNamespace:
29
+ """Role-specific SQL operations (app_user, secure_user, secure_machine_user)"""
30
+
31
+ def __init__(self, role: str, _execute_command):
32
+ """
33
+ Initialize role namespace.
34
+
35
+ Args:
36
+ role: Role name ("app", "secure", "secure_machine")
37
+ _execute_command: Internal function to execute commands
38
+ """
39
+ self._role = role
40
+ self._command_prefix = f"sql.{role}"
41
+ self._execute = _execute_command
42
+ self._default_schema = "app" if role == "app" else "secure"
43
+
44
+ async def list_tables(self, schema: Optional[str] = None) -> List[Dict[str, Any]]:
45
+ """
46
+ List tables in a schema.
47
+
48
+ Args:
49
+ schema: Schema name (default: "app" for app_user, "secure" for secure_user)
50
+
51
+ Returns:
52
+ List of table information dictionaries
53
+ """
54
+ if schema is None:
55
+ schema = self._default_schema
56
+ command = f"{self._command_prefix}.list_tables"
57
+ return await self._execute(command, schema=schema)
58
+
59
+ async def query_table(
60
+ self,
61
+ table_name: str,
62
+ schema: Optional[str] = None,
63
+ filters: Optional[Dict[str, Any]] = None,
64
+ sort_by: Optional[str] = None,
65
+ sort_order: str = "ASC",
66
+ limit: int = 100,
67
+ offset: int = 0
68
+ ) -> Dict[str, Any]:
69
+ """
70
+ Query table data with pagination and filters.
71
+
72
+ Args:
73
+ table_name: Table name
74
+ schema: Schema name (default: "app" for app_user, "secure" for secure_user)
75
+ filters: Optional dictionary of column:value filters
76
+ sort_by: Optional column name to sort by
77
+ sort_order: Sort order (ASC or DESC, default: ASC)
78
+ limit: Maximum number of rows to return (default: 100)
79
+ offset: Number of rows to skip (default: 0)
80
+
81
+ Returns:
82
+ Dictionary with 'rows' and 'total' keys
83
+ """
84
+ if schema is None:
85
+ schema = self._default_schema
86
+ command = f"{self._command_prefix}.query_table"
87
+ return await self._execute(
88
+ command,
89
+ table_name=table_name,
90
+ schema=schema,
91
+ filters=filters,
92
+ sort_by=sort_by,
93
+ sort_order=sort_order,
94
+ limit=limit,
95
+ offset=offset
96
+ )
97
+
98
+ async def insert_row(
99
+ self,
100
+ table_name: str,
101
+ data: Dict[str, Any],
102
+ schema: Optional[str] = None
103
+ ) -> Dict[str, Any]:
104
+ """
105
+ Insert a single row into a table.
106
+
107
+ Args:
108
+ table_name: Table name
109
+ data: Dictionary of column:value pairs
110
+ schema: Schema name (default: "app" for app_user, "secure" for secure_user)
111
+
112
+ Returns:
113
+ Dictionary with inserted row data
114
+ """
115
+ if schema is None:
116
+ schema = self._default_schema
117
+ command = f"{self._command_prefix}.insert_row"
118
+ return await self._execute(
119
+ command,
120
+ table_name=table_name,
121
+ data=data,
122
+ schema=schema
123
+ )
124
+
125
+ async def update_rows(
126
+ self,
127
+ table_name: str,
128
+ data: Dict[str, Any],
129
+ filters: Dict[str, Any],
130
+ schema: Optional[str] = None
131
+ ) -> Dict[str, Any]:
132
+ """
133
+ Update rows matching filters.
134
+
135
+ Args:
136
+ table_name: Table name
137
+ data: Dictionary of column:value pairs to update
138
+ filters: Dictionary of column:value pairs for WHERE clause
139
+ schema: Schema name (default: "app" for app_user, "secure" for secure_user)
140
+
141
+ Returns:
142
+ Dictionary with 'affected_rows' key
143
+ """
144
+ if schema is None:
145
+ schema = self._default_schema
146
+ command = f"{self._command_prefix}.update_rows"
147
+ return await self._execute(
148
+ command,
149
+ table_name=table_name,
150
+ data=data,
151
+ filters=filters,
152
+ schema=schema
153
+ )
154
+
155
+ async def delete_rows(
156
+ self,
157
+ table_name: str,
158
+ filters: Dict[str, Any],
159
+ schema: Optional[str] = None
160
+ ) -> Dict[str, Any]:
161
+ """
162
+ Delete rows matching filters.
163
+
164
+ Note: For secure_user and secure_machine_user, DELETE only works on app schema,
165
+ not on secure schema (PHI data uses soft deletes only).
166
+
167
+ Args:
168
+ table_name: Table name
169
+ filters: Dictionary of column:value pairs for WHERE clause
170
+ schema: Schema name (default: "app" - secure roles can only delete from app schema)
171
+
172
+ Returns:
173
+ Dictionary with 'affected_rows' key
174
+ """
175
+ if schema is None:
176
+ schema = "app" # Always app for delete operations (secure schema doesn't allow DELETE)
177
+ command = f"{self._command_prefix}.delete_rows"
178
+ return await self._execute(
179
+ command,
180
+ table_name=table_name,
181
+ filters=filters,
182
+ schema=schema
183
+ )
184
+
185
+ async def list_columns(
186
+ self,
187
+ table_name: str,
188
+ schema: Optional[str] = None
189
+ ) -> List[Dict[str, Any]]:
190
+ """
191
+ List columns in a table.
192
+
193
+ Args:
194
+ table_name: Table name
195
+ schema: Schema name (default: "app" for app_user, "secure" for secure_user)
196
+
197
+ Returns:
198
+ List of column information dictionaries
199
+ """
200
+ if schema is None:
201
+ schema = self._default_schema
202
+ command = f"{self._command_prefix}.list_columns"
203
+ return await self._execute(
204
+ command,
205
+ table_name=table_name,
206
+ schema=schema
207
+ )
208
+
209
+ async def get_table_size(
210
+ self,
211
+ table_name: str,
212
+ schema: Optional[str] = None
213
+ ) -> Dict[str, Any]:
214
+ """
215
+ Get table size information.
216
+
217
+ Args:
218
+ table_name: Table name
219
+ schema: Schema name (default: "app" for app_user, "secure" for secure_user)
220
+
221
+ Returns:
222
+ Dictionary with table size information
223
+ """
224
+ if schema is None:
225
+ schema = self._default_schema
226
+ command = f"{self._command_prefix}.get_table_size"
227
+ return await self._execute(
228
+ command,
229
+ table_name=table_name,
230
+ schema=schema
231
+ )
232
+
233
+
234
+ class SQLOpenNamespace:
235
+ """Open user namespace - Returns DSN string for dominus_open database"""
236
+
237
+ def __init__(self, _execute_command):
238
+ """
239
+ Initialize open namespace.
240
+
241
+ Args:
242
+ _execute_command: Internal function to execute commands
243
+ """
244
+ self._execute = _execute_command
245
+
246
+ async def dsn(self) -> str:
247
+ """
248
+ Get the full DSN connection string for open_user role.
249
+
250
+ This returns the complete PostgreSQL connection URI for the dominus_open
251
+ database that can be used directly by clients to connect.
252
+
253
+ Returns:
254
+ PostgreSQL connection URI string in format:
255
+ postgresql://{userpwcombo}@{branchtargetstring}/dominus_open?sslmode=require&channel_binding=require
256
+
257
+ Note:
258
+ This is the only role that exposes the DSN string directly.
259
+ All other roles use the DSN internally for operations.
260
+ """
261
+ return await self._execute("sql.open.get_dsn")
262
+
263
+
264
+ class SQLAuthNamespace:
265
+ """
266
+ Authentication operations namespace.
267
+
268
+ Provides scoped functions for managing auth schema tables:
269
+ - scopes: Permission definitions
270
+ - roles: Role definitions with assigned scopes
271
+ - users: User accounts with assigned roles
272
+ - clients: Service account PSKs with assigned roles
273
+ - refresh_tokens: Token management
274
+ - JWT operations: Minting and validation
275
+
276
+ DB User: auth_user (CRUD on auth schema only)
277
+ """
278
+
279
+ def __init__(self, _execute_command, _execute_sovereign_command=None):
280
+ """
281
+ Initialize auth namespace.
282
+
283
+ Args:
284
+ _execute_command: Internal function to execute commands (Architect)
285
+ _execute_sovereign_command: Internal function for Sovereign commands
286
+ """
287
+ self._execute = _execute_command
288
+ self._execute_sovereign = _execute_sovereign_command
289
+ self._public_key_cache = None
290
+
291
+ # ========================================
292
+ # SCOPES (auth.scopes table)
293
+ # ========================================
294
+
295
+ async def add_scope(
296
+ self,
297
+ slug: str,
298
+ display_name: str,
299
+ category_id: Optional[str] = None,
300
+ description: Optional[str] = None
301
+ ) -> Dict[str, Any]:
302
+ """
303
+ Add a new scope.
304
+
305
+ Args:
306
+ slug: Unique scope identifier (e.g., "read", "write", "admin")
307
+ display_name: Human-readable name
308
+ category_id: Optional tenant category UUID to link via scope_categories
309
+ description: Optional description
310
+
311
+ Returns:
312
+ Created scope record
313
+ """
314
+ return await self._execute(
315
+ "auth.add_scope",
316
+ slug=slug,
317
+ display_name=display_name,
318
+ category_id=category_id,
319
+ description=description
320
+ )
321
+
322
+ async def delete_scope(self, scope_id: str) -> Dict[str, Any]:
323
+ """
324
+ Delete a scope by ID.
325
+
326
+ Args:
327
+ scope_id: UUID of scope to delete
328
+
329
+ Returns:
330
+ Deletion confirmation
331
+ """
332
+ return await self._execute("auth.delete_scope", scope_id=scope_id)
333
+
334
+ async def list_scopes(
335
+ self,
336
+ category_id: Optional[str] = None
337
+ ) -> List[Dict[str, Any]]:
338
+ """
339
+ List all scopes, optionally filtered by category.
340
+
341
+ Args:
342
+ category_id: Optional tenant category UUID filter
343
+
344
+ Returns:
345
+ List of scope records
346
+ """
347
+ return await self._execute(
348
+ "auth.list_scopes",
349
+ category_id=category_id
350
+ )
351
+
352
+ async def get_scope(
353
+ self,
354
+ scope_id: Optional[str] = None,
355
+ slug: Optional[str] = None
356
+ ) -> Dict[str, Any]:
357
+ """
358
+ Get a scope by ID or slug.
359
+
360
+ Args:
361
+ scope_id: UUID of scope (preferred)
362
+ slug: Scope slug (alternative lookup)
363
+
364
+ Returns:
365
+ Scope record
366
+ """
367
+ return await self._execute(
368
+ "auth.get_scope",
369
+ scope_id=scope_id,
370
+ slug=slug
371
+ )
372
+
373
+ # ========================================
374
+ # ROLES (auth.roles table)
375
+ # ========================================
376
+
377
+ async def add_role(
378
+ self,
379
+ name: str,
380
+ scope_slugs: Optional[List[str]] = None,
381
+ description: Optional[str] = None,
382
+ tenant_id: Optional[str] = None,
383
+ category_id: Optional[str] = None
384
+ ) -> Dict[str, Any]:
385
+ """
386
+ Add a new role.
387
+
388
+ Args:
389
+ name: Role name (unique per tenant)
390
+ scope_slugs: List of scope slugs to assign
391
+ description: Optional description
392
+ tenant_id: Optional tenant UUID to link via role_tenants
393
+ category_id: Optional category UUID to link via role_categories
394
+
395
+ Returns:
396
+ Created role record
397
+ """
398
+ return await self._execute(
399
+ "auth.add_role",
400
+ name=name,
401
+ scope_slugs=scope_slugs or [],
402
+ description=description,
403
+ tenant_id=tenant_id,
404
+ category_id=category_id
405
+ )
406
+
407
+ async def delete_role(self, role_id: str) -> Dict[str, Any]:
408
+ """
409
+ Delete a role by ID.
410
+
411
+ Args:
412
+ role_id: UUID of role to delete
413
+
414
+ Returns:
415
+ Deletion confirmation
416
+ """
417
+ return await self._execute("auth.delete_role", role_id=role_id)
418
+
419
+ async def list_roles(self) -> List[Dict[str, Any]]:
420
+ """
421
+ List all roles for the tenant.
422
+
423
+ Returns:
424
+ List of role records
425
+ """
426
+ return await self._execute("auth.list_roles")
427
+
428
+ async def get_role(
429
+ self,
430
+ role_id: Optional[str] = None,
431
+ name: Optional[str] = None
432
+ ) -> Dict[str, Any]:
433
+ """
434
+ Get a role by ID or name.
435
+
436
+ Args:
437
+ role_id: UUID of role (preferred)
438
+ name: Role name (alternative lookup)
439
+
440
+ Returns:
441
+ Role record
442
+ """
443
+ return await self._execute(
444
+ "auth.get_role",
445
+ role_id=role_id,
446
+ name=name
447
+ )
448
+
449
+ async def update_role_scopes(
450
+ self,
451
+ role_id: str,
452
+ scope_slugs: List[str]
453
+ ) -> Dict[str, Any]:
454
+ """
455
+ Update the scopes assigned to a role.
456
+
457
+ Args:
458
+ role_id: UUID of role to update
459
+ scope_slugs: New list of scope slugs
460
+
461
+ Returns:
462
+ Updated role record
463
+ """
464
+ return await self._execute(
465
+ "auth.update_role_scopes",
466
+ role_id=role_id,
467
+ scope_slugs=scope_slugs
468
+ )
469
+
470
+ # ========================================
471
+ # USERS (auth.users table)
472
+ # ========================================
473
+
474
+ async def add_user(
475
+ self,
476
+ username: str,
477
+ password: str,
478
+ role_id: Optional[str] = None,
479
+ tenant_id: Optional[str] = None,
480
+ email: Optional[str] = None
481
+ ) -> Dict[str, Any]:
482
+ """
483
+ Add a new user.
484
+
485
+ Password is hashed client-side before sending to Architect.
486
+
487
+ Args:
488
+ username: Unique username
489
+ password: Raw password (will be hashed)
490
+ role_id: Optional UUID of role to assign
491
+ tenant_id: Optional UUID of tenant to link (defaults from role if provided)
492
+ email: Optional email address
493
+
494
+ Returns:
495
+ Created user record (without password_hash)
496
+ """
497
+ password_hash = hash_password(password)
498
+ return await self._execute(
499
+ "auth.add_user",
500
+ username=username,
501
+ password_hash=password_hash,
502
+ role_id=role_id,
503
+ tenant_id=tenant_id,
504
+ email=email
505
+ )
506
+
507
+ async def delete_user(self, user_id: str) -> Dict[str, Any]:
508
+ """
509
+ Delete a user by ID.
510
+
511
+ Args:
512
+ user_id: UUID of user to delete
513
+
514
+ Returns:
515
+ Deletion confirmation
516
+ """
517
+ return await self._execute("auth.delete_user", user_id=user_id)
518
+
519
+ async def list_users(self) -> List[Dict[str, Any]]:
520
+ """
521
+ List all users for the tenant.
522
+
523
+ Returns:
524
+ List of user records (without password_hash)
525
+ """
526
+ return await self._execute("auth.list_users")
527
+
528
+ async def get_user(
529
+ self,
530
+ user_id: Optional[str] = None,
531
+ username: Optional[str] = None
532
+ ) -> Dict[str, Any]:
533
+ """
534
+ Get a user by ID or username.
535
+
536
+ Args:
537
+ user_id: UUID of user (preferred)
538
+ username: Username (alternative lookup)
539
+
540
+ Returns:
541
+ User record (without password_hash)
542
+ """
543
+ return await self._execute(
544
+ "auth.get_user",
545
+ user_id=user_id,
546
+ username=username
547
+ )
548
+
549
+ async def update_user_status(
550
+ self,
551
+ user_id: str,
552
+ status: str
553
+ ) -> Dict[str, Any]:
554
+ """
555
+ Update a user's status.
556
+
557
+ Args:
558
+ user_id: UUID of user to update
559
+ status: New status (e.g., "active", "inactive", "suspended")
560
+
561
+ Returns:
562
+ Updated user record
563
+ """
564
+ return await self._execute(
565
+ "auth.update_user_status",
566
+ user_id=user_id,
567
+ status=status
568
+ )
569
+
570
+ async def update_user_role(
571
+ self,
572
+ user_id: str,
573
+ role_id: str
574
+ ) -> Dict[str, Any]:
575
+ """
576
+ Update a user's assigned role.
577
+
578
+ Args:
579
+ user_id: UUID of user to update
580
+ role_id: UUID of new role
581
+
582
+ Returns:
583
+ Updated user record
584
+ """
585
+ return await self._execute(
586
+ "auth.update_user_role",
587
+ user_id=user_id,
588
+ role_id=role_id
589
+ )
590
+
591
+ async def update_user(
592
+ self,
593
+ user_id: str,
594
+ username: str = None,
595
+ email: str = None
596
+ ) -> Dict[str, Any]:
597
+ """
598
+ Update a user's profile fields (username, email).
599
+
600
+ Args:
601
+ user_id: UUID of user to update
602
+ username: New username (optional)
603
+ email: New email (optional, can be None to clear)
604
+
605
+ Returns:
606
+ Updated user record
607
+ """
608
+ params = {"user_id": user_id}
609
+ if username is not None:
610
+ params["username"] = username
611
+ if email is not None:
612
+ params["email"] = email
613
+ return await self._execute("auth.update_user", **params)
614
+
615
+ async def update_user_password(
616
+ self,
617
+ user_id: str,
618
+ password: str
619
+ ) -> Dict[str, Any]:
620
+ """
621
+ Update a user's password.
622
+
623
+ Args:
624
+ user_id: UUID of user to update
625
+ password: New raw password (will be hashed by Architect)
626
+
627
+ Returns:
628
+ Updated user record (without password_hash)
629
+ """
630
+ return await self._execute(
631
+ "auth.update_user_password",
632
+ user_id=user_id,
633
+ password=password
634
+ )
635
+
636
+ async def verify_user_password(
637
+ self,
638
+ username: str,
639
+ password: str
640
+ ) -> Dict[str, Any]:
641
+ """
642
+ Verify a user's password.
643
+
644
+ Raw password is sent to Architect which does bcrypt comparison.
645
+
646
+ Args:
647
+ username: Username to verify
648
+ password: Raw password to check
649
+
650
+ Returns:
651
+ {"valid": True/False, "user": {...}} if valid
652
+ """
653
+ return await self._execute(
654
+ "auth.verify_user_password",
655
+ username=username,
656
+ password=password
657
+ )
658
+
659
+ # ========================================
660
+ # CLIENTS / PSK (auth.client_psk table)
661
+ # ========================================
662
+
663
+ async def add_client(
664
+ self,
665
+ label: str,
666
+ role_id: str
667
+ ) -> Dict[str, Any]:
668
+ """
669
+ Add a new service client with PSK.
670
+
671
+ Generates a PSK, hashes it, stores the hash, and returns
672
+ the raw PSK (one-time visible).
673
+
674
+ Args:
675
+ label: Human-readable label for the client
676
+ role_id: UUID of role to assign
677
+
678
+ Returns:
679
+ {"client": {...}, "psk": "raw-psk-one-time-visible"}
680
+ """
681
+ from ..helpers.crypto import generate_psk_local
682
+ # Generate PSK (prefer Sovereign route if available, fallback to local)
683
+ if self._execute_sovereign:
684
+ try:
685
+ result = await self._execute_sovereign("auth.generate_psk")
686
+ raw_psk = result.get("psk")
687
+ except Exception:
688
+ raw_psk = generate_psk_local()
689
+ else:
690
+ raw_psk = generate_psk_local()
691
+
692
+ psk_hash = hash_psk(raw_psk)
693
+ client_result = await self._execute(
694
+ "auth.add_client",
695
+ label=label,
696
+ role_id=role_id,
697
+ psk_hash=psk_hash
698
+ )
699
+ return {
700
+ "client": client_result,
701
+ "psk": raw_psk # One-time visible
702
+ }
703
+
704
+ async def delete_client(self, client_id: str) -> Dict[str, Any]:
705
+ """
706
+ Delete a client by ID.
707
+
708
+ Args:
709
+ client_id: UUID of client to delete
710
+
711
+ Returns:
712
+ Deletion confirmation
713
+ """
714
+ return await self._execute("auth.delete_client", client_id=client_id)
715
+
716
+ async def list_clients(self) -> List[Dict[str, Any]]:
717
+ """
718
+ List all clients for the tenant.
719
+
720
+ Returns:
721
+ List of client records (without psk_hash)
722
+ """
723
+ return await self._execute("auth.list_clients")
724
+
725
+ async def get_client(self, client_id: str) -> Dict[str, Any]:
726
+ """
727
+ Get a client by ID.
728
+
729
+ Args:
730
+ client_id: UUID of client
731
+
732
+ Returns:
733
+ Client record (without psk_hash)
734
+ """
735
+ return await self._execute("auth.get_client", client_id=client_id)
736
+
737
+ async def regenerate_client_psk(self, client_id: str) -> Dict[str, Any]:
738
+ """
739
+ Regenerate a client's PSK.
740
+
741
+ Generates a new PSK, hashes it, updates the stored hash,
742
+ and returns the new raw PSK (one-time visible).
743
+
744
+ Args:
745
+ client_id: UUID of client
746
+
747
+ Returns:
748
+ {"client": {...}, "psk": "new-raw-psk-one-time-visible"}
749
+ """
750
+ from ..helpers.crypto import generate_psk_local
751
+ # Generate new PSK
752
+ if self._execute_sovereign:
753
+ try:
754
+ result = await self._execute_sovereign("auth.generate_psk")
755
+ raw_psk = result.get("psk")
756
+ except Exception:
757
+ raw_psk = generate_psk_local()
758
+ else:
759
+ raw_psk = generate_psk_local()
760
+
761
+ psk_hash = hash_psk(raw_psk)
762
+ client_result = await self._execute(
763
+ "auth.regenerate_client_psk",
764
+ client_id=client_id,
765
+ psk_hash=psk_hash
766
+ )
767
+ return {
768
+ "client": client_result,
769
+ "psk": raw_psk # One-time visible
770
+ }
771
+
772
+ async def verify_client_psk(
773
+ self,
774
+ client_id: str,
775
+ psk: str
776
+ ) -> Dict[str, Any]:
777
+ """
778
+ Verify a client's PSK.
779
+
780
+ Raw PSK is sent to Architect which does bcrypt comparison.
781
+
782
+ Args:
783
+ client_id: UUID of client
784
+ psk: Raw PSK to verify
785
+
786
+ Returns:
787
+ {"valid": True/False, "client": {...}} if valid
788
+ """
789
+ return await self._execute(
790
+ "auth.verify_client_psk",
791
+ client_id=client_id,
792
+ psk=psk
793
+ )
794
+
795
+ # ========================================
796
+ # REFRESH TOKENS (auth.refresh_tokens table)
797
+ # ========================================
798
+
799
+ async def add_refresh_token(
800
+ self,
801
+ user_id: Optional[str] = None,
802
+ client_psk_id: Optional[str] = None,
803
+ expires_in_seconds: int = 86400 * 30 # 30 days default
804
+ ) -> Dict[str, Any]:
805
+ """
806
+ Create a new refresh token.
807
+
808
+ Either user_id or client_psk_id must be provided.
809
+
810
+ Args:
811
+ user_id: UUID of user (for user tokens)
812
+ client_psk_id: UUID of client (for service tokens)
813
+ expires_in_seconds: Token lifetime (default: 30 days)
814
+
815
+ Returns:
816
+ {"token_id": "...", "token": "raw-token-one-time-visible"}
817
+ """
818
+ raw_token = generate_token()
819
+ token_hash = hash_token(raw_token)
820
+
821
+ result = await self._execute(
822
+ "auth.add_refresh_token",
823
+ user_id=user_id,
824
+ client_psk_id=client_psk_id,
825
+ token_hash=token_hash,
826
+ expires_in_seconds=expires_in_seconds
827
+ )
828
+ return {
829
+ "token_id": result.get("id"),
830
+ "token": raw_token, # One-time visible
831
+ "expires_at": result.get("expires_at")
832
+ }
833
+
834
+ async def delete_refresh_token(self, token_id: str) -> Dict[str, Any]:
835
+ """
836
+ Delete/revoke a refresh token.
837
+
838
+ Args:
839
+ token_id: UUID of token to delete
840
+
841
+ Returns:
842
+ Deletion confirmation
843
+ """
844
+ return await self._execute(
845
+ "auth.delete_refresh_token",
846
+ token_id=token_id
847
+ )
848
+
849
+ async def list_refresh_tokens(
850
+ self,
851
+ user_id: Optional[str] = None,
852
+ client_psk_id: Optional[str] = None
853
+ ) -> List[Dict[str, Any]]:
854
+ """
855
+ List refresh tokens, optionally filtered.
856
+
857
+ Args:
858
+ user_id: Filter by user
859
+ client_psk_id: Filter by client
860
+
861
+ Returns:
862
+ List of token records (without token_hash)
863
+ """
864
+ return await self._execute(
865
+ "auth.list_refresh_tokens",
866
+ user_id=user_id,
867
+ client_psk_id=client_psk_id
868
+ )
869
+
870
+ # ========================================
871
+ # JWT OPERATIONS (via Sovereign)
872
+ # ========================================
873
+
874
+ async def mint_subsidiary_jwt(
875
+ self,
876
+ user_id: str,
877
+ scope: Optional[List[str]] = None,
878
+ expires_in: int = 900,
879
+ system: str = "user"
880
+ ) -> Dict[str, Any]:
881
+ """
882
+ Mint a JWT for a subsidiary user.
883
+
884
+ Uses DOMINUS_TOKEN to authenticate with Sovereign, then
885
+ requests a JWT with the specified subsidiary user_id.
886
+
887
+ Args:
888
+ user_id: Subsidiary user identifier to embed in JWT
889
+ scope: List of scopes to include
890
+ expires_in: Token lifetime in seconds (default: 15 min)
891
+ system: System identifier (default: "user")
892
+
893
+ Returns:
894
+ {"access_token": "...", "token_type": "Bearer", "expires_in": ...}
895
+ """
896
+ if not self._execute_sovereign:
897
+ raise RuntimeError("Sovereign command executor not available")
898
+
899
+ return await self._execute_sovereign(
900
+ "auth.mint_token",
901
+ user_id=user_id,
902
+ scope=scope,
903
+ system=system
904
+ )
905
+
906
+ async def get_public_key(self) -> str:
907
+ """
908
+ Get Sovereign's public key for JWT validation.
909
+
910
+ Caches the key after first fetch.
911
+
912
+ Returns:
913
+ PEM-encoded public key string
914
+ """
915
+ if self._public_key_cache:
916
+ return self._public_key_cache
917
+
918
+ if not self._execute_sovereign:
919
+ raise RuntimeError("Sovereign command executor not available")
920
+
921
+ result = await self._execute_sovereign("auth.get_public_key")
922
+ self._public_key_cache = result.get("public_key")
923
+ return self._public_key_cache
924
+
925
+ async def validate_jwt(self, token: str) -> Dict[str, Any]:
926
+ """
927
+ Validate a JWT and return its claims.
928
+
929
+ Fetches public key from Sovereign (cached) and validates locally.
930
+
931
+ Args:
932
+ token: JWT string to validate
933
+
934
+ Returns:
935
+ Decoded JWT claims if valid
936
+
937
+ Raises:
938
+ ValueError: If token is invalid or expired
939
+ """
940
+ import jwt as pyjwt
941
+
942
+ public_key = await self.get_public_key()
943
+
944
+ try:
945
+ claims = pyjwt.decode(
946
+ token,
947
+ public_key,
948
+ algorithms=["RS256"],
949
+ options={"verify_exp": True}
950
+ )
951
+ return claims
952
+ except pyjwt.ExpiredSignatureError:
953
+ raise ValueError("Token has expired")
954
+ except pyjwt.InvalidTokenError as e:
955
+ raise ValueError(f"Invalid token: {e}")
956
+
957
+ # ========================================
958
+ # PAGE ACCESS CONTROL (auth.pages table)
959
+ # ========================================
960
+
961
+ async def check_page_access(
962
+ self,
963
+ page_path: str,
964
+ user_jwt: Optional[str] = None
965
+ ) -> Dict[str, Any]:
966
+ """
967
+ Check if user has access to a specific page.
968
+
969
+ Args:
970
+ page_path: Page path (e.g., "/dashboard/admin")
971
+ user_jwt: Optional user's JWT token (for extracting scopes)
972
+
973
+ Returns:
974
+ {
975
+ "allowed": bool,
976
+ "reason": str (optional, if not allowed)
977
+ }
978
+
979
+ Logic (handled by Architect backend):
980
+ 1. Query auth.pages for page_id
981
+ 2. If page not found, return allowed=True (unregistered pages are open)
982
+ 3. Extract scopes from user JWT
983
+ 4. Compare JWT scopes with page's required_scopes
984
+ 5. Return access decision
985
+ """
986
+ return await self._execute(
987
+ "auth.check_page_access",
988
+ page_path=page_path,
989
+ user_jwt=user_jwt
990
+ )
991
+
992
+ # ========================================
993
+ # SCOPE NAVIGATION (auth.scope_navigation table)
994
+ # ========================================
995
+
996
+ async def get_scope_navigation(
997
+ self,
998
+ scope_id: str
999
+ ) -> List[Dict[str, Any]]:
1000
+ """
1001
+ Get navigation items for a specific scope.
1002
+
1003
+ Args:
1004
+ scope_id: UUID of the scope
1005
+
1006
+ Returns:
1007
+ List of navigation items (from auth.scope_navigation table)
1008
+ Sorted by order field
1009
+ """
1010
+ return await self._execute(
1011
+ "auth.get_scope_navigation",
1012
+ scope_id=scope_id
1013
+ )
1014
+
1015
+ async def get_user_navigation_scopes(
1016
+ self,
1017
+ user_id: str,
1018
+ jwt_scopes: List[str],
1019
+ tenant_category_name: str = 'admin'
1020
+ ) -> List[Dict[str, Any]]:
1021
+ """
1022
+ Get all navigation scopes available to a user.
1023
+
1024
+ Args:
1025
+ user_id: User UUID
1026
+ jwt_scopes: List of scope slugs from user's JWT
1027
+ tenant_category_name: Name of tenant category to filter by (default: 'admin')
1028
+
1029
+ Returns:
1030
+ List of Scope objects for navigation
1031
+ Filtered by:
1032
+ - tenant_category_id = category matching tenant_category_name
1033
+ - scope slug in jwt_scopes
1034
+ """
1035
+ return await self._execute(
1036
+ "auth.get_user_navigation_scopes",
1037
+ user_id=user_id,
1038
+ jwt_scopes=jwt_scopes,
1039
+ tenant_category_name=tenant_category_name
1040
+ )
1041
+
1042
+ # ========================================
1043
+ # USER PREFERENCES (auth.user_preferences table)
1044
+ # ========================================
1045
+
1046
+ async def get_user_preferences(
1047
+ self,
1048
+ user_id: str
1049
+ ) -> Dict[str, Any]:
1050
+ """
1051
+ Get user preferences from auth.user_preferences table.
1052
+
1053
+ Args:
1054
+ user_id: User UUID
1055
+
1056
+ Returns:
1057
+ Preferences JSONB object (defaults to empty dict if not found)
1058
+ """
1059
+ result = await self._execute(
1060
+ "auth.get_user_preferences",
1061
+ user_id=user_id
1062
+ )
1063
+ # Return empty dict if not found, otherwise return preferences
1064
+ return result.get("preferences", {}) if result else {}
1065
+
1066
+ async def set_user_preference(
1067
+ self,
1068
+ user_id: str,
1069
+ key: str,
1070
+ value: Any
1071
+ ) -> Dict[str, Any]:
1072
+ """
1073
+ Set a user preference.
1074
+
1075
+ Args:
1076
+ user_id: User UUID
1077
+ key: Preference key (e.g., "default_scope_id", "theme")
1078
+ value: Preference value
1079
+
1080
+ Returns:
1081
+ Updated preferences object
1082
+ """
1083
+ return await self._execute(
1084
+ "auth.set_user_preference",
1085
+ user_id=user_id,
1086
+ key=key,
1087
+ value=value
1088
+ )
1089
+
1090
+
1091
+ class SQLSchemaNamespace:
1092
+ """
1093
+ Schema DDL operations namespace.
1094
+
1095
+ Provides functions for managing table structure in app and secure schemas:
1096
+ - Create/drop tables
1097
+ - Add/drop columns
1098
+ - List tables and columns
1099
+
1100
+ DB User: schema_user (DDL on app/secure, CRUD on meta)
1101
+ """
1102
+
1103
+ def __init__(self, _execute_command):
1104
+ """
1105
+ Initialize schema namespace.
1106
+
1107
+ Args:
1108
+ _execute_command: Internal function to execute commands
1109
+ """
1110
+ self._execute = _execute_command
1111
+
1112
+ # ========================================
1113
+ # APP SCHEMA (default)
1114
+ # ========================================
1115
+
1116
+ async def add_table(
1117
+ self,
1118
+ table_name: str,
1119
+ columns: List[Dict[str, Any]]
1120
+ ) -> Dict[str, Any]:
1121
+ """
1122
+ Create a new table in the app schema.
1123
+
1124
+ Args:
1125
+ table_name: Name of table to create
1126
+ columns: List of column definitions
1127
+ [{"name": "id", "type": "UUID", "constraints": ["PRIMARY KEY"]}]
1128
+
1129
+ Returns:
1130
+ Creation confirmation
1131
+ """
1132
+ return await self._execute(
1133
+ "schema.add_table",
1134
+ table_name=table_name,
1135
+ columns=columns
1136
+ )
1137
+
1138
+ async def delete_table(self, table_name: str) -> Dict[str, Any]:
1139
+ """
1140
+ Drop a table from the app schema.
1141
+
1142
+ Args:
1143
+ table_name: Name of table to drop
1144
+
1145
+ Returns:
1146
+ Deletion confirmation
1147
+ """
1148
+ return await self._execute(
1149
+ "schema.delete_table",
1150
+ table_name=table_name
1151
+ )
1152
+
1153
+ async def list_tables(self) -> List[Dict[str, Any]]:
1154
+ """
1155
+ List all tables in the app schema.
1156
+
1157
+ Returns:
1158
+ List of table information
1159
+ """
1160
+ return await self._execute("schema.list_tables")
1161
+
1162
+ async def list_columns(self, table_name: str) -> List[Dict[str, Any]]:
1163
+ """
1164
+ List columns in a table in the app schema.
1165
+
1166
+ Args:
1167
+ table_name: Name of table
1168
+
1169
+ Returns:
1170
+ List of column information
1171
+ """
1172
+ return await self._execute(
1173
+ "schema.list_columns",
1174
+ table_name=table_name
1175
+ )
1176
+
1177
+ async def add_column(
1178
+ self,
1179
+ table_name: str,
1180
+ column_name: str,
1181
+ column_type: str,
1182
+ constraints: Optional[List[str]] = None,
1183
+ default: Optional[str] = None
1184
+ ) -> Dict[str, Any]:
1185
+ """
1186
+ Add a column to a table in the app schema.
1187
+
1188
+ Args:
1189
+ table_name: Name of table
1190
+ column_name: Name of new column
1191
+ column_type: PostgreSQL type (e.g., "VARCHAR(100)", "INTEGER")
1192
+ constraints: Optional constraints (e.g., ["NOT NULL"])
1193
+ default: Optional default value
1194
+
1195
+ Returns:
1196
+ Alteration confirmation
1197
+ """
1198
+ return await self._execute(
1199
+ "schema.add_column",
1200
+ table_name=table_name,
1201
+ column_name=column_name,
1202
+ column_type=column_type,
1203
+ constraints=constraints,
1204
+ default=default
1205
+ )
1206
+
1207
+ async def delete_column(
1208
+ self,
1209
+ table_name: str,
1210
+ column_name: str
1211
+ ) -> Dict[str, Any]:
1212
+ """
1213
+ Drop a column from a table in the app schema.
1214
+
1215
+ Args:
1216
+ table_name: Name of table
1217
+ column_name: Name of column to drop
1218
+
1219
+ Returns:
1220
+ Alteration confirmation
1221
+ """
1222
+ return await self._execute(
1223
+ "schema.delete_column",
1224
+ table_name=table_name,
1225
+ column_name=column_name
1226
+ )
1227
+
1228
+ # ========================================
1229
+ # SECURE SCHEMA (explicit prefix)
1230
+ # ========================================
1231
+
1232
+ async def secure_add_table(
1233
+ self,
1234
+ table_name: str,
1235
+ columns: List[Dict[str, Any]]
1236
+ ) -> Dict[str, Any]:
1237
+ """
1238
+ Create a new table in the secure schema.
1239
+
1240
+ Args:
1241
+ table_name: Name of table to create
1242
+ columns: List of column definitions
1243
+
1244
+ Returns:
1245
+ Creation confirmation
1246
+ """
1247
+ return await self._execute(
1248
+ "schema.secure_add_table",
1249
+ table_name=table_name,
1250
+ columns=columns
1251
+ )
1252
+
1253
+ async def secure_delete_table(self, table_name: str) -> Dict[str, Any]:
1254
+ """
1255
+ Drop a table from the secure schema.
1256
+
1257
+ Args:
1258
+ table_name: Name of table to drop
1259
+
1260
+ Returns:
1261
+ Deletion confirmation
1262
+ """
1263
+ return await self._execute(
1264
+ "schema.secure_delete_table",
1265
+ table_name=table_name
1266
+ )
1267
+
1268
+ async def secure_list_tables(self) -> List[Dict[str, Any]]:
1269
+ """
1270
+ List all tables in the secure schema.
1271
+
1272
+ Returns:
1273
+ List of table information
1274
+ """
1275
+ return await self._execute("schema.secure_list_tables")
1276
+
1277
+ async def secure_list_columns(self, table_name: str) -> List[Dict[str, Any]]:
1278
+ """
1279
+ List columns in a table in the secure schema.
1280
+
1281
+ Args:
1282
+ table_name: Name of table
1283
+
1284
+ Returns:
1285
+ List of column information
1286
+ """
1287
+ return await self._execute(
1288
+ "schema.secure_list_columns",
1289
+ table_name=table_name
1290
+ )
1291
+
1292
+ async def secure_add_column(
1293
+ self,
1294
+ table_name: str,
1295
+ column_name: str,
1296
+ column_type: str,
1297
+ constraints: Optional[List[str]] = None,
1298
+ default: Optional[str] = None
1299
+ ) -> Dict[str, Any]:
1300
+ """
1301
+ Add a column to a table in the secure schema.
1302
+
1303
+ Args:
1304
+ table_name: Name of table
1305
+ column_name: Name of new column
1306
+ column_type: PostgreSQL type
1307
+ constraints: Optional constraints
1308
+ default: Optional default value
1309
+
1310
+ Returns:
1311
+ Alteration confirmation
1312
+ """
1313
+ return await self._execute(
1314
+ "schema.secure_add_column",
1315
+ table_name=table_name,
1316
+ column_name=column_name,
1317
+ column_type=column_type,
1318
+ constraints=constraints,
1319
+ default=default
1320
+ )
1321
+
1322
+ async def secure_delete_column(
1323
+ self,
1324
+ table_name: str,
1325
+ column_name: str
1326
+ ) -> Dict[str, Any]:
1327
+ """
1328
+ Drop a column from a table in the secure schema.
1329
+
1330
+ Args:
1331
+ table_name: Name of table
1332
+ column_name: Name of column to drop
1333
+
1334
+ Returns:
1335
+ Alteration confirmation
1336
+ """
1337
+ return await self._execute(
1338
+ "schema.secure_delete_column",
1339
+ table_name=table_name,
1340
+ column_name=column_name
1341
+ )