sovaria-cli 0.2.5__tar.gz → 0.2.8__tar.gz
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.
- sovaria_cli-0.2.8/.github/dependabot.yml +12 -0
- sovaria_cli-0.2.8/.github/workflows/ci.yml +174 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/.github/workflows/publish.yml +5 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/.gitignore +4 -0
- sovaria_cli-0.2.8/.gitleaks.toml +17 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/PKG-INFO +43 -2
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/README.md +37 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/__init__.py +1 -1
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/admin.py +14 -14
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/api.py +4 -4
- sovaria_cli-0.2.8/apps/cli/src/commands/audit.py +172 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/auth_product.py +52 -33
- sovaria_cli-0.2.8/apps/cli/src/commands/billing.py +498 -0
- sovaria_cli-0.2.8/apps/cli/src/commands/chrono.py +232 -0
- sovaria_cli-0.2.8/apps/cli/src/commands/collections.py +38 -0
- sovaria_cli-0.2.8/apps/cli/src/commands/connect.py +129 -0
- sovaria_cli-0.2.8/apps/cli/src/commands/context.py +190 -0
- sovaria_cli-0.2.8/apps/cli/src/commands/dashboard.py +166 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/data.py +37 -39
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/db.py +373 -84
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/deploy.py +631 -354
- sovaria_cli-0.2.8/apps/cli/src/commands/gen.py +85 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/governance.py +242 -248
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/iam.py +188 -208
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/init_cmd.py +17 -14
- sovaria_cli-0.2.8/apps/cli/src/commands/inspect.py +53 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/keys.py +32 -27
- sovaria_cli-0.2.8/apps/cli/src/commands/link_cmd.py +108 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/login.py +546 -329
- sovaria_cli-0.2.8/apps/cli/src/commands/logs.py +103 -0
- sovaria_cli-0.2.8/apps/cli/src/commands/mcp.py +79 -0
- sovaria_cli-0.2.8/apps/cli/src/commands/oidc_providers.py +199 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/org.py +18 -9
- sovaria_cli-0.2.8/apps/cli/src/commands/passkeys.py +82 -0
- sovaria_cli-0.2.8/apps/cli/src/commands/policy.py +299 -0
- sovaria_cli-0.2.8/apps/cli/src/commands/project.py +241 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/realtime.py +175 -155
- sovaria_cli-0.2.8/apps/cli/src/commands/schema.py +61 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/scope.py +36 -17
- sovaria_cli-0.2.8/apps/cli/src/commands/secrets.py +192 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/sim.py +92 -54
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/storage.py +54 -58
- sovaria_cli-0.2.8/apps/cli/src/commands/test_cmd.py +167 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/vector.py +43 -40
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/main.py +37 -1
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/api_client.py +122 -14
- sovaria_cli-0.2.8/apps/cli/src/utils/api_helpers.py +52 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/auth_store.py +86 -19
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/config.py +53 -0
- sovaria_cli-0.2.8/apps/cli/src/utils/display.py +102 -0
- sovaria_cli-0.2.8/apps/cli/src/utils/local_function_server.py +148 -0
- sovaria_cli-0.2.8/apps/cli/src/utils/log_tail.py +53 -0
- sovaria_cli-0.2.8/apps/cli/src/utils/output_mode.py +18 -0
- sovaria_cli-0.2.8/apps/cli/src/utils/premium_display.py +1349 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/project_context.py +21 -0
- sovaria_cli-0.2.8/apps/cli/src/utils/schema_introspect.py +223 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/scope_picker.py +6 -24
- sovaria_cli-0.2.8/apps/cli/src/utils/theme.py +24 -0
- sovaria_cli-0.2.8/apps/cli/tests/test_aug18_cli_tickets.py +450 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_cli_smoke_paths.py +36 -14
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_config_profiles.py +9 -0
- sovaria_cli-0.2.8/apps/cli/tests/test_credential_file_permissions.py +29 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_main_startup.py +1 -1
- sovaria_cli-0.2.8/apps/cli/tests/test_mcp_command.py +31 -0
- sovaria_cli-0.2.8/apps/cli/tests/test_phase4_commands.py +91 -0
- sovaria_cli-0.2.8/apps/cli/tests/test_phase5_commands.py +97 -0
- sovaria_cli-0.2.8/apps/cli/tests/test_phase6_commands.py +133 -0
- sovaria_cli-0.2.8/apps/cli/tests/test_premium_display.py +132 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_project_and_commands.py +10 -10
- sovaria_cli-0.2.8/apps/cli/tests/test_supabase_style_commands.py +63 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/pyproject.toml +20 -2
- sovaria_cli-0.2.8/requirements-dev.txt +4 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/requirements.txt +3 -1
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/scripts/cli_sdk_happy_path.sh +2 -2
- sovaria_cli-0.2.8/scripts/smoke_all_commands.sh +107 -0
- sovaria_cli-0.2.8/scripts/smoke_chunks.sh +90 -0
- sovaria_cli-0.2.5/.github/workflows/ci.yml +0 -39
- sovaria_cli-0.2.5/apps/cli/src/commands/audit.py +0 -181
- sovaria_cli-0.2.5/apps/cli/src/commands/billing.py +0 -513
- sovaria_cli-0.2.5/apps/cli/src/commands/chrono.py +0 -248
- sovaria_cli-0.2.5/apps/cli/src/commands/context.py +0 -208
- sovaria_cli-0.2.5/apps/cli/src/commands/policy.py +0 -317
- sovaria_cli-0.2.5/apps/cli/src/commands/project.py +0 -130
- sovaria_cli-0.2.5/apps/cli/src/utils/display.py +0 -62
- sovaria_cli-0.2.5/requirements-dev.txt +0 -2
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/.python-version +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/Makefile +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/__init__.py +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/__init__.py +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/shell.py +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/__init__.py +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/migrations.py +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_auth_store_profiles.py +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_cli_security.py +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_scope_picker.py +0 -0
- {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/pytest.ini +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
secret-scan:
|
|
12
|
+
name: Secret scan
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
- name: Install Gitleaks
|
|
19
|
+
run: |
|
|
20
|
+
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.24.2/gitleaks_8.24.2_linux_x64.tar.gz -o /tmp/gitleaks.tar.gz
|
|
21
|
+
tar -xzf /tmp/gitleaks.tar.gz -C /tmp
|
|
22
|
+
sudo install -m 0755 /tmp/gitleaks /usr/local/bin/gitleaks
|
|
23
|
+
- name: Run Gitleaks
|
|
24
|
+
run: |
|
|
25
|
+
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
26
|
+
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
|
27
|
+
else
|
|
28
|
+
RANGE="HEAD~1..HEAD"
|
|
29
|
+
fi
|
|
30
|
+
echo "Scanning commit range: $RANGE"
|
|
31
|
+
gitleaks detect --source . --redact --no-banner --config .gitleaks.toml --log-opts="$RANGE"
|
|
32
|
+
|
|
33
|
+
lint:
|
|
34
|
+
name: Ruff
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
needs: secret-scan
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
- name: Set up Python
|
|
40
|
+
uses: actions/setup-python@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: "3.12"
|
|
43
|
+
- name: Install ruff
|
|
44
|
+
run: pip install "ruff>=0.9"
|
|
45
|
+
- name: Ruff check
|
|
46
|
+
run: |
|
|
47
|
+
ruff check \
|
|
48
|
+
apps/cli/src/commands/login.py \
|
|
49
|
+
apps/cli/src/commands/deploy.py \
|
|
50
|
+
apps/cli/src/commands/realtime.py \
|
|
51
|
+
apps/cli/src/utils/api_client.py \
|
|
52
|
+
apps/cli/src/utils/auth_store.py \
|
|
53
|
+
apps/cli/src/utils/config.py \
|
|
54
|
+
apps/cli/tests/test_aug18_cli_tickets.py \
|
|
55
|
+
apps/cli/tests/test_config_profiles.py
|
|
56
|
+
|
|
57
|
+
typecheck:
|
|
58
|
+
name: Mypy
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
needs: secret-scan
|
|
61
|
+
env:
|
|
62
|
+
# Fine-grained token with contents:read on sovaria/sovaria-sdk. Without it, CI installs the
|
|
63
|
+
# pinned SDK release from PyPI instead.
|
|
64
|
+
SDK_TOKEN: ${{ secrets.SOVARIA_SDK_READ_TOKEN }}
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
- name: Check out the matching SDK (when SOVARIA_SDK_READ_TOKEN is set)
|
|
68
|
+
if: env.SDK_TOKEN != ''
|
|
69
|
+
uses: actions/checkout@v4
|
|
70
|
+
with:
|
|
71
|
+
repository: sovaria/sovaria-sdk
|
|
72
|
+
path: sovaria-sdk
|
|
73
|
+
token: ${{ env.SDK_TOKEN }}
|
|
74
|
+
- name: Set up Python
|
|
75
|
+
uses: actions/setup-python@v5
|
|
76
|
+
with:
|
|
77
|
+
python-version: "3.12"
|
|
78
|
+
- name: Install SDK + CLI
|
|
79
|
+
run: |
|
|
80
|
+
set -euo pipefail
|
|
81
|
+
if [ -f ../sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
82
|
+
pip install -e ../sovaria-sdk/apps/sdk/python
|
|
83
|
+
elif [ -f sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
84
|
+
pip install -e sovaria-sdk/apps/sdk/python
|
|
85
|
+
else
|
|
86
|
+
pip install "sovaria-sdk==0.4.0"
|
|
87
|
+
fi
|
|
88
|
+
pip install -e ".[dev]"
|
|
89
|
+
- name: Mypy
|
|
90
|
+
run: |
|
|
91
|
+
mypy \
|
|
92
|
+
apps/cli/src/commands/login.py \
|
|
93
|
+
apps/cli/src/commands/deploy.py \
|
|
94
|
+
apps/cli/src/commands/realtime.py \
|
|
95
|
+
apps/cli/src/utils/api_client.py \
|
|
96
|
+
apps/cli/src/utils/auth_store.py \
|
|
97
|
+
apps/cli/src/utils/config.py
|
|
98
|
+
|
|
99
|
+
install-requirements:
|
|
100
|
+
name: requirements.txt install
|
|
101
|
+
runs-on: ubuntu-latest
|
|
102
|
+
needs: secret-scan
|
|
103
|
+
env:
|
|
104
|
+
# Fine-grained token with contents:read on sovaria/sovaria-sdk. Without it, CI installs the
|
|
105
|
+
# pinned SDK release from PyPI instead.
|
|
106
|
+
SDK_TOKEN: ${{ secrets.SOVARIA_SDK_READ_TOKEN }}
|
|
107
|
+
steps:
|
|
108
|
+
- uses: actions/checkout@v4
|
|
109
|
+
- name: Check out the matching SDK (when SOVARIA_SDK_READ_TOKEN is set)
|
|
110
|
+
if: env.SDK_TOKEN != ''
|
|
111
|
+
uses: actions/checkout@v4
|
|
112
|
+
with:
|
|
113
|
+
repository: sovaria/sovaria-sdk
|
|
114
|
+
path: sovaria-sdk
|
|
115
|
+
token: ${{ env.SDK_TOKEN }}
|
|
116
|
+
- name: Set up Python
|
|
117
|
+
uses: actions/setup-python@v5
|
|
118
|
+
with:
|
|
119
|
+
python-version: "3.12"
|
|
120
|
+
- name: Install from requirements.txt
|
|
121
|
+
run: |
|
|
122
|
+
set -euo pipefail
|
|
123
|
+
if [ -f ../sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
124
|
+
pip install -e ../sovaria-sdk/apps/sdk/python
|
|
125
|
+
elif [ -f sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
126
|
+
pip install -e sovaria-sdk/apps/sdk/python
|
|
127
|
+
else
|
|
128
|
+
pip install "sovaria-sdk==0.4.0"
|
|
129
|
+
fi
|
|
130
|
+
pip install -r requirements.txt
|
|
131
|
+
pip install --no-deps .
|
|
132
|
+
- name: sov --help
|
|
133
|
+
run: sov --help
|
|
134
|
+
|
|
135
|
+
test:
|
|
136
|
+
runs-on: ubuntu-latest
|
|
137
|
+
needs: secret-scan
|
|
138
|
+
env:
|
|
139
|
+
SDK_TOKEN: ${{ secrets.SOVARIA_SDK_READ_TOKEN }}
|
|
140
|
+
steps:
|
|
141
|
+
- uses: actions/checkout@v4
|
|
142
|
+
- name: Check out the matching SDK (when SOVARIA_SDK_READ_TOKEN is set)
|
|
143
|
+
if: env.SDK_TOKEN != ''
|
|
144
|
+
uses: actions/checkout@v4
|
|
145
|
+
with:
|
|
146
|
+
repository: sovaria/sovaria-sdk
|
|
147
|
+
path: sovaria-sdk
|
|
148
|
+
token: ${{ env.SDK_TOKEN }}
|
|
149
|
+
|
|
150
|
+
- name: Set up Python
|
|
151
|
+
uses: actions/setup-python@v5
|
|
152
|
+
with:
|
|
153
|
+
python-version: "3.12"
|
|
154
|
+
|
|
155
|
+
- name: Install SDK + CLI
|
|
156
|
+
run: |
|
|
157
|
+
set -euo pipefail
|
|
158
|
+
if [ -f ../sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
159
|
+
pip install -e ../sovaria-sdk/apps/sdk/python
|
|
160
|
+
elif [ -f sovaria-sdk/apps/sdk/python/pyproject.toml ]; then
|
|
161
|
+
pip install -e sovaria-sdk/apps/sdk/python
|
|
162
|
+
else
|
|
163
|
+
pip install "sovaria-sdk==0.4.0"
|
|
164
|
+
fi
|
|
165
|
+
pip install -e ".[dev]"
|
|
166
|
+
|
|
167
|
+
- name: Run tests
|
|
168
|
+
run: pytest apps/cli/tests -q
|
|
169
|
+
|
|
170
|
+
- name: Build wheel
|
|
171
|
+
run: python -m build
|
|
172
|
+
|
|
173
|
+
- name: Validate wheel metadata
|
|
174
|
+
run: python -m twine check dist/*
|
|
@@ -8,8 +8,13 @@ on:
|
|
|
8
8
|
required: true
|
|
9
9
|
|
|
10
10
|
jobs:
|
|
11
|
+
test:
|
|
12
|
+
if: github.event.inputs.confirm == 'publish'
|
|
13
|
+
uses: ./.github/workflows/ci.yml
|
|
14
|
+
|
|
11
15
|
publish:
|
|
12
16
|
if: github.event.inputs.confirm == 'publish'
|
|
17
|
+
needs: test
|
|
13
18
|
runs-on: ubuntu-latest
|
|
14
19
|
environment: pypi
|
|
15
20
|
permissions:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
title = "Sovaria CLI gitleaks config"
|
|
2
|
+
|
|
3
|
+
[extend]
|
|
4
|
+
useDefault = true
|
|
5
|
+
|
|
6
|
+
[allowlist]
|
|
7
|
+
description = "Placeholder credentials and CLI test fixtures"
|
|
8
|
+
paths = [
|
|
9
|
+
'''\.env\.example$''',
|
|
10
|
+
'''apps/cli/tests/''',
|
|
11
|
+
]
|
|
12
|
+
regexes = [
|
|
13
|
+
'''change-me''',
|
|
14
|
+
'''CHANGE_ME''',
|
|
15
|
+
'''do-not-use-in-prod''',
|
|
16
|
+
'''replace-with''',
|
|
17
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sovaria-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Sovaria BaaS command-line interface (sov)
|
|
5
5
|
Project-URL: Homepage, https://sovaria.in
|
|
6
6
|
Project-URL: Documentation, https://docs.sovaria.in/cli
|
|
@@ -11,14 +11,18 @@ Requires-Python: >=3.11
|
|
|
11
11
|
Requires-Dist: httpx>=0.28.1
|
|
12
12
|
Requires-Dist: keyring>=25.0
|
|
13
13
|
Requires-Dist: pyyaml>=6.0
|
|
14
|
-
Requires-Dist:
|
|
14
|
+
Requires-Dist: rich>=13.7.0
|
|
15
|
+
Requires-Dist: sovaria-sdk==0.4.0
|
|
15
16
|
Requires-Dist: tomli-w>=1.2.0
|
|
16
17
|
Requires-Dist: typer>=0.17.4
|
|
17
18
|
Provides-Extra: dev
|
|
18
19
|
Requires-Dist: build>=1.2; extra == 'dev'
|
|
20
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
19
21
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
22
|
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
21
23
|
Requires-Dist: twine>=6.0; extra == 'dev'
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: sovaria-sdk[mcp]==0.4.0; extra == 'mcp'
|
|
22
26
|
Description-Content-Type: text/markdown
|
|
23
27
|
|
|
24
28
|
# Sovaria CLI
|
|
@@ -60,6 +64,43 @@ scripts/cli_sdk_happy_path.sh
|
|
|
60
64
|
|
|
61
65
|
If you already have valid CLI tokens, omit `SOVARIA_EMAIL` and `SOVARIA_PASSWORD`.
|
|
62
66
|
|
|
67
|
+
## Authentication
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
sov login
|
|
71
|
+
sov login --email you@example.com
|
|
72
|
+
sov login --file-storage # headless: tokens in ~/.sovaria/credentials.toml
|
|
73
|
+
sov logout
|
|
74
|
+
sov logout --force # clear local tokens if server revoke fails
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- **Browser (PKCE):** authorize opens the console origin (`get_console_url`); the token exchange POSTs to the gateway `api_url` `/oauth/token` proxy. That split is intentional.
|
|
78
|
+
- **Password:** never pass `--password`. Operator login prompts with `getpass`, or reads `SOVARIA_PASSWORD`.
|
|
79
|
+
- **Product user create:** `sov auth-product create --email ...` prompts for the new user's password, or reads `SOVARIA_USER_PASSWORD`. Do not reuse `SOVARIA_PASSWORD` (that is the operator login secret).
|
|
80
|
+
- **Magic link:** `sov auth magic-link verify` reads the token from stdin or `SOVARIA_MAGIC_TOKEN`.
|
|
81
|
+
- **GitHub deploy PAT:** set `SOVARIA_GITHUB_TOKEN` or use `gh auth token`. There is no `--token` flag.
|
|
82
|
+
- **OAuth callback:** binds `127.0.0.1` by default. Non-loopback `SOVARIA_CLI_CALLBACK_HOST` requires `--insecure-callback`.
|
|
83
|
+
|
|
84
|
+
### Token refresh contract
|
|
85
|
+
|
|
86
|
+
Axis supports two refresh modes. Do not mix them:
|
|
87
|
+
|
|
88
|
+
| Client | How refresh is sent |
|
|
89
|
+
|---|---|
|
|
90
|
+
| **CLI / Python SDK** | JSON body: `POST /api/v1/auth/refresh` with `{"refresh_token": "<stored>"}`. The CLI keeps the refresh token in OS keyring (or `credentials.toml` with `--file-storage`). |
|
|
91
|
+
| **Console / browser** | Empty JSON body `{}` plus the HttpOnly refresh cookie. The browser never puts the refresh token in the request body. |
|
|
92
|
+
|
|
93
|
+
Copying the console cookie pattern into a CLI or SDK client will fail silently (no cookie jar). Copying the CLI JSON-body pattern into a browser app bypasses HttpOnly cookie storage.
|
|
94
|
+
|
|
95
|
+
### Pulse WebSocket URLs
|
|
96
|
+
|
|
97
|
+
`sov realtime ws-url` issues an opaque `ticket=` query parameter. Never put a JWT in `?token=` — production gateways disable `GATEWAY_WS_ALLOW_QUERY_TOKEN`. Local `.env.local` may still enable the legacy query-token path; do not copy that into clients.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
sov realtime ws-url my-channel --org <org_id> --project <project_id>
|
|
101
|
+
# wss://api.sovaria.in/api/v1/rt/ws/my-channel?ticket=...&org_id=...&project_id=...
|
|
102
|
+
```
|
|
103
|
+
|
|
63
104
|
## Core Command Groups
|
|
64
105
|
|
|
65
106
|
- `sov auth ...` auth login/logout
|
|
@@ -37,6 +37,43 @@ scripts/cli_sdk_happy_path.sh
|
|
|
37
37
|
|
|
38
38
|
If you already have valid CLI tokens, omit `SOVARIA_EMAIL` and `SOVARIA_PASSWORD`.
|
|
39
39
|
|
|
40
|
+
## Authentication
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
sov login
|
|
44
|
+
sov login --email you@example.com
|
|
45
|
+
sov login --file-storage # headless: tokens in ~/.sovaria/credentials.toml
|
|
46
|
+
sov logout
|
|
47
|
+
sov logout --force # clear local tokens if server revoke fails
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **Browser (PKCE):** authorize opens the console origin (`get_console_url`); the token exchange POSTs to the gateway `api_url` `/oauth/token` proxy. That split is intentional.
|
|
51
|
+
- **Password:** never pass `--password`. Operator login prompts with `getpass`, or reads `SOVARIA_PASSWORD`.
|
|
52
|
+
- **Product user create:** `sov auth-product create --email ...` prompts for the new user's password, or reads `SOVARIA_USER_PASSWORD`. Do not reuse `SOVARIA_PASSWORD` (that is the operator login secret).
|
|
53
|
+
- **Magic link:** `sov auth magic-link verify` reads the token from stdin or `SOVARIA_MAGIC_TOKEN`.
|
|
54
|
+
- **GitHub deploy PAT:** set `SOVARIA_GITHUB_TOKEN` or use `gh auth token`. There is no `--token` flag.
|
|
55
|
+
- **OAuth callback:** binds `127.0.0.1` by default. Non-loopback `SOVARIA_CLI_CALLBACK_HOST` requires `--insecure-callback`.
|
|
56
|
+
|
|
57
|
+
### Token refresh contract
|
|
58
|
+
|
|
59
|
+
Axis supports two refresh modes. Do not mix them:
|
|
60
|
+
|
|
61
|
+
| Client | How refresh is sent |
|
|
62
|
+
|---|---|
|
|
63
|
+
| **CLI / Python SDK** | JSON body: `POST /api/v1/auth/refresh` with `{"refresh_token": "<stored>"}`. The CLI keeps the refresh token in OS keyring (or `credentials.toml` with `--file-storage`). |
|
|
64
|
+
| **Console / browser** | Empty JSON body `{}` plus the HttpOnly refresh cookie. The browser never puts the refresh token in the request body. |
|
|
65
|
+
|
|
66
|
+
Copying the console cookie pattern into a CLI or SDK client will fail silently (no cookie jar). Copying the CLI JSON-body pattern into a browser app bypasses HttpOnly cookie storage.
|
|
67
|
+
|
|
68
|
+
### Pulse WebSocket URLs
|
|
69
|
+
|
|
70
|
+
`sov realtime ws-url` issues an opaque `ticket=` query parameter. Never put a JWT in `?token=` — production gateways disable `GATEWAY_WS_ALLOW_QUERY_TOKEN`. Local `.env.local` may still enable the legacy query-token path; do not copy that into clients.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
sov realtime ws-url my-channel --org <org_id> --project <project_id>
|
|
74
|
+
# wss://api.sovaria.in/api/v1/rt/ws/my-channel?ticket=...&org_id=...&project_id=...
|
|
75
|
+
```
|
|
76
|
+
|
|
40
77
|
## Core Command Groups
|
|
41
78
|
|
|
42
79
|
- `sov auth ...` auth login/logout
|
|
@@ -7,7 +7,8 @@ from urllib.parse import quote, urlencode
|
|
|
7
7
|
import typer
|
|
8
8
|
|
|
9
9
|
from apps.cli.src.utils.api_client import CLIRequestError, call_api
|
|
10
|
-
from apps.cli.src.utils.display import print_json
|
|
10
|
+
from apps.cli.src.utils.display import handle_cli_error, handle_command_error, print_json
|
|
11
|
+
from apps.cli.src.utils.project_context import resolve_org_id
|
|
11
12
|
|
|
12
13
|
app = typer.Typer(help="Platform admin: error traces, usage, transparency")
|
|
13
14
|
|
|
@@ -52,8 +53,7 @@ def error_traces_list(
|
|
|
52
53
|
params=params,
|
|
53
54
|
)
|
|
54
55
|
except CLIRequestError as exc:
|
|
55
|
-
|
|
56
|
-
raise typer.Exit(1) from exc
|
|
56
|
+
handle_cli_error(exc)
|
|
57
57
|
print_json(result)
|
|
58
58
|
|
|
59
59
|
|
|
@@ -71,8 +71,7 @@ def error_traces_get(
|
|
|
71
71
|
require_scope=False,
|
|
72
72
|
)
|
|
73
73
|
except CLIRequestError as exc:
|
|
74
|
-
|
|
75
|
-
raise typer.Exit(1) from exc
|
|
74
|
+
handle_cli_error(exc)
|
|
76
75
|
print_json(result)
|
|
77
76
|
|
|
78
77
|
|
|
@@ -92,8 +91,7 @@ def error_traces_resolve(
|
|
|
92
91
|
body={"resolved": resolved},
|
|
93
92
|
)
|
|
94
93
|
except CLIRequestError as exc:
|
|
95
|
-
|
|
96
|
-
raise typer.Exit(1) from exc
|
|
94
|
+
handle_cli_error(exc)
|
|
97
95
|
print_json(result)
|
|
98
96
|
|
|
99
97
|
|
|
@@ -118,14 +116,13 @@ def usage_overview(
|
|
|
118
116
|
params=params or None,
|
|
119
117
|
)
|
|
120
118
|
except CLIRequestError as exc:
|
|
121
|
-
|
|
122
|
-
raise typer.Exit(1) from exc
|
|
119
|
+
handle_cli_error(exc)
|
|
123
120
|
print_json(result)
|
|
124
121
|
|
|
125
122
|
|
|
126
123
|
@transparency_app.command("list")
|
|
127
124
|
def transparency_list(
|
|
128
|
-
org_id: str = typer.Option(
|
|
125
|
+
org_id: str | None = typer.Option(None, "--org", help="Organisation ID (defaults to scoped org)"),
|
|
129
126
|
project_id: str | None = typer.Option(None, "--project"),
|
|
130
127
|
event_type: str | None = typer.Option(None, "--event-type"),
|
|
131
128
|
since: str | None = typer.Option(None, "--since", help="ISO-8601 lower bound"),
|
|
@@ -134,8 +131,12 @@ def transparency_list(
|
|
|
134
131
|
profile: str | None = typer.Option(None, "--profile"),
|
|
135
132
|
) -> None:
|
|
136
133
|
"""List tenant-visible staff access transparency events."""
|
|
134
|
+
try:
|
|
135
|
+
resolved_org = resolve_org_id(profile=profile, org_id=org_id, require=True)
|
|
136
|
+
except ValueError as exc:
|
|
137
|
+
handle_command_error(exc)
|
|
137
138
|
query: dict[str, str] = {
|
|
138
|
-
"org_id":
|
|
139
|
+
"org_id": resolved_org,
|
|
139
140
|
"limit": str(limit),
|
|
140
141
|
"offset": str(offset),
|
|
141
142
|
}
|
|
@@ -151,10 +152,9 @@ def transparency_list(
|
|
|
151
152
|
method="GET",
|
|
152
153
|
path=path,
|
|
153
154
|
profile=profile,
|
|
154
|
-
org_id=
|
|
155
|
+
org_id=resolved_org,
|
|
155
156
|
require_scope=False,
|
|
156
157
|
)
|
|
157
158
|
except CLIRequestError as exc:
|
|
158
|
-
|
|
159
|
-
raise typer.Exit(1) from exc
|
|
159
|
+
handle_cli_error(exc)
|
|
160
160
|
print_json(result)
|
|
@@ -4,7 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
|
|
7
|
-
from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
|
|
7
|
+
from apps.cli.src.utils.api_client import CLIRequestError, call_api, parse_json_arg
|
|
8
|
+
from apps.cli.src.utils.display import handle_cli_error, print_json
|
|
8
9
|
|
|
9
10
|
app = typer.Typer(help="Raw gateway API calls")
|
|
10
11
|
|
|
@@ -42,7 +43,6 @@ def call_command(
|
|
|
42
43
|
env_id=env_id,
|
|
43
44
|
)
|
|
44
45
|
except CLIRequestError as exc:
|
|
45
|
-
|
|
46
|
-
raise typer.Exit(1) from exc
|
|
46
|
+
handle_cli_error(exc)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
print_json(result)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Echo audit and simulation commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from apps.cli.src.utils.api_client import CLIRequestError, parse_json_arg
|
|
8
|
+
from apps.cli.src.utils.api_helpers import premium_api, premium_mutate
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Echo audit trail and simulation operations")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("append")
|
|
14
|
+
def append_audit(
|
|
15
|
+
event_type: str = typer.Option(..., "--event-type"),
|
|
16
|
+
actor_id: str = typer.Option(..., "--actor-id"),
|
|
17
|
+
source: str = typer.Option(..., "--source"),
|
|
18
|
+
action: str = typer.Option(..., "--action"),
|
|
19
|
+
resource_id: str | None = typer.Option(None, "--resource-id"),
|
|
20
|
+
metadata: str | None = typer.Option(None, "--metadata", help="JSON object"),
|
|
21
|
+
ip_address: str | None = typer.Option(None, "--ip-address"),
|
|
22
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Append an immutable audit entry."""
|
|
25
|
+
parsed_metadata = parse_json_arg(metadata, label="--metadata") if metadata else {}
|
|
26
|
+
if parsed_metadata is not None and not isinstance(parsed_metadata, dict):
|
|
27
|
+
raise CLIRequestError("--metadata must be a JSON object")
|
|
28
|
+
premium_mutate(
|
|
29
|
+
"Appending audit entry…",
|
|
30
|
+
success="Appended",
|
|
31
|
+
title="Audit entry",
|
|
32
|
+
method="POST",
|
|
33
|
+
path="/api/v1/audit/",
|
|
34
|
+
profile=profile,
|
|
35
|
+
body={
|
|
36
|
+
"event_type": event_type,
|
|
37
|
+
"actor_id": actor_id,
|
|
38
|
+
"resource_id": resource_id,
|
|
39
|
+
"source": source,
|
|
40
|
+
"action": action,
|
|
41
|
+
"metadata": parsed_metadata or {},
|
|
42
|
+
"ip_address": ip_address,
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command("list")
|
|
48
|
+
def list_audit(
|
|
49
|
+
actor_id: str | None = typer.Option(None, "--actor-id"),
|
|
50
|
+
event_type: str | None = typer.Option(None, "--event-type"),
|
|
51
|
+
source: str | None = typer.Option(None, "--source"),
|
|
52
|
+
resource_id: str | None = typer.Option(None, "--resource-id"),
|
|
53
|
+
limit: int = typer.Option(50, "--limit"),
|
|
54
|
+
offset: int = typer.Option(0, "--offset"),
|
|
55
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Query audit log entries."""
|
|
58
|
+
params = {
|
|
59
|
+
"limit": str(limit),
|
|
60
|
+
"offset": str(offset),
|
|
61
|
+
}
|
|
62
|
+
if actor_id:
|
|
63
|
+
params["actor_id"] = actor_id
|
|
64
|
+
if event_type:
|
|
65
|
+
params["event_type"] = event_type
|
|
66
|
+
if source:
|
|
67
|
+
params["source"] = source
|
|
68
|
+
if resource_id:
|
|
69
|
+
params["resource_id"] = resource_id
|
|
70
|
+
|
|
71
|
+
premium_api(
|
|
72
|
+
"Loading audit entries…",
|
|
73
|
+
success="Loaded",
|
|
74
|
+
title="Audit log",
|
|
75
|
+
method="GET",
|
|
76
|
+
path="/api/v1/audit/",
|
|
77
|
+
profile=profile,
|
|
78
|
+
params=params,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command("verify")
|
|
83
|
+
def verify_chain(profile: str | None = typer.Option(None, "--profile")) -> None:
|
|
84
|
+
"""Verify audit hash-chain integrity."""
|
|
85
|
+
premium_api(
|
|
86
|
+
"Verifying audit chain…",
|
|
87
|
+
success="Verified",
|
|
88
|
+
title="Audit verification",
|
|
89
|
+
method="GET",
|
|
90
|
+
path="/api/v1/audit/verify",
|
|
91
|
+
profile=profile,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app.command("count")
|
|
96
|
+
def audit_count(profile: str | None = typer.Option(None, "--profile")) -> None:
|
|
97
|
+
"""Return audit log entry count."""
|
|
98
|
+
premium_api(
|
|
99
|
+
"Counting audit entries…",
|
|
100
|
+
success="Loaded",
|
|
101
|
+
title="Audit count",
|
|
102
|
+
method="GET",
|
|
103
|
+
path="/api/v1/audit/count",
|
|
104
|
+
profile=profile,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("sim-run")
|
|
109
|
+
def sim_run(
|
|
110
|
+
simulation_name: str = typer.Argument(...),
|
|
111
|
+
operations: str = typer.Argument(..., help="JSON array operations"),
|
|
112
|
+
context: str | None = typer.Option(None, "--context", help="JSON object"),
|
|
113
|
+
include_execution_tree: bool = typer.Option(False, "--include-execution-tree"),
|
|
114
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Run an Echo phantom simulation."""
|
|
117
|
+
parsed_ops = parse_json_arg(operations, label="operations")
|
|
118
|
+
if not isinstance(parsed_ops, list):
|
|
119
|
+
raise CLIRequestError("operations must be a JSON array")
|
|
120
|
+
|
|
121
|
+
parsed_context = parse_json_arg(context, label="--context") if context else {}
|
|
122
|
+
if parsed_context is not None and not isinstance(parsed_context, dict):
|
|
123
|
+
raise CLIRequestError("--context must be a JSON object")
|
|
124
|
+
|
|
125
|
+
premium_api(
|
|
126
|
+
f"Running simulation '{simulation_name}'…",
|
|
127
|
+
success="Completed",
|
|
128
|
+
title="Simulation",
|
|
129
|
+
method="POST",
|
|
130
|
+
path="/api/v1/sim/run",
|
|
131
|
+
profile=profile,
|
|
132
|
+
body={
|
|
133
|
+
"simulation_name": simulation_name,
|
|
134
|
+
"operations": parsed_ops,
|
|
135
|
+
"echo_flag": True,
|
|
136
|
+
"include_execution_tree": include_execution_tree,
|
|
137
|
+
"context": parsed_context or {},
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command("sim-plan")
|
|
143
|
+
def sim_plan(
|
|
144
|
+
simulation_name: str = typer.Argument(...),
|
|
145
|
+
operations: str = typer.Argument(..., help="JSON array operations"),
|
|
146
|
+
context: str | None = typer.Option(None, "--context", help="JSON object"),
|
|
147
|
+
profile: str | None = typer.Option(None, "--profile"),
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Preview an Echo simulation plan."""
|
|
150
|
+
parsed_ops = parse_json_arg(operations, label="operations")
|
|
151
|
+
if not isinstance(parsed_ops, list):
|
|
152
|
+
raise CLIRequestError("operations must be a JSON array")
|
|
153
|
+
|
|
154
|
+
parsed_context = parse_json_arg(context, label="--context") if context else {}
|
|
155
|
+
if parsed_context is not None and not isinstance(parsed_context, dict):
|
|
156
|
+
raise CLIRequestError("--context must be a JSON object")
|
|
157
|
+
|
|
158
|
+
premium_api(
|
|
159
|
+
f"Planning simulation '{simulation_name}'…",
|
|
160
|
+
success="Planned",
|
|
161
|
+
title="Simulation plan",
|
|
162
|
+
method="POST",
|
|
163
|
+
path="/api/v1/sim/plan",
|
|
164
|
+
profile=profile,
|
|
165
|
+
body={
|
|
166
|
+
"simulation_name": simulation_name,
|
|
167
|
+
"operations": parsed_ops,
|
|
168
|
+
"echo_flag": True,
|
|
169
|
+
"include_execution_tree": True,
|
|
170
|
+
"context": parsed_context or {},
|
|
171
|
+
},
|
|
172
|
+
)
|