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/__init__.py +47 -0
- dominus/config/__init__.py +24 -0
- dominus/config/endpoints.py +43 -0
- dominus/errors.py +201 -0
- dominus/helpers/__init__.py +2 -0
- dominus/helpers/auth.py +49 -0
- dominus/helpers/cache.py +192 -0
- dominus/helpers/core.py +603 -0
- dominus/helpers/crypto.py +118 -0
- dominus/namespaces/__init__.py +26 -0
- dominus/namespaces/_deprecated_crossover.py +10 -0
- dominus/namespaces/_deprecated_sql.py +1341 -0
- dominus/namespaces/auth.py +1025 -0
- dominus/namespaces/courier.py +251 -0
- dominus/namespaces/db.py +267 -0
- dominus/namespaces/ddl.py +590 -0
- dominus/namespaces/files.py +299 -0
- dominus/namespaces/health.py +59 -0
- dominus/namespaces/logs.py +367 -0
- dominus/namespaces/open.py +72 -0
- dominus/namespaces/portal.py +437 -0
- dominus/namespaces/redis.py +357 -0
- dominus/namespaces/secrets.py +126 -0
- dominus/services/__init__.py +2 -0
- dominus/services/_deprecated_architect.py +323 -0
- dominus/services/_deprecated_sovereign.py +93 -0
- dominus/start.py +942 -0
- dominus_sdk_python-2.0.0.dist-info/METADATA +381 -0
- dominus_sdk_python-2.0.0.dist-info/RECORD +31 -0
- dominus_sdk_python-2.0.0.dist-info/WHEEL +5 -0
- dominus_sdk_python-2.0.0.dist-info/top_level.txt +1 -0
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()
|