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.
Files changed (96) hide show
  1. sovaria_cli-0.2.8/.github/dependabot.yml +12 -0
  2. sovaria_cli-0.2.8/.github/workflows/ci.yml +174 -0
  3. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/.github/workflows/publish.yml +5 -0
  4. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/.gitignore +4 -0
  5. sovaria_cli-0.2.8/.gitleaks.toml +17 -0
  6. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/PKG-INFO +43 -2
  7. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/README.md +37 -0
  8. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/__init__.py +1 -1
  9. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/admin.py +14 -14
  10. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/api.py +4 -4
  11. sovaria_cli-0.2.8/apps/cli/src/commands/audit.py +172 -0
  12. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/auth_product.py +52 -33
  13. sovaria_cli-0.2.8/apps/cli/src/commands/billing.py +498 -0
  14. sovaria_cli-0.2.8/apps/cli/src/commands/chrono.py +232 -0
  15. sovaria_cli-0.2.8/apps/cli/src/commands/collections.py +38 -0
  16. sovaria_cli-0.2.8/apps/cli/src/commands/connect.py +129 -0
  17. sovaria_cli-0.2.8/apps/cli/src/commands/context.py +190 -0
  18. sovaria_cli-0.2.8/apps/cli/src/commands/dashboard.py +166 -0
  19. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/data.py +37 -39
  20. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/db.py +373 -84
  21. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/deploy.py +631 -354
  22. sovaria_cli-0.2.8/apps/cli/src/commands/gen.py +85 -0
  23. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/governance.py +242 -248
  24. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/iam.py +188 -208
  25. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/init_cmd.py +17 -14
  26. sovaria_cli-0.2.8/apps/cli/src/commands/inspect.py +53 -0
  27. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/keys.py +32 -27
  28. sovaria_cli-0.2.8/apps/cli/src/commands/link_cmd.py +108 -0
  29. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/login.py +546 -329
  30. sovaria_cli-0.2.8/apps/cli/src/commands/logs.py +103 -0
  31. sovaria_cli-0.2.8/apps/cli/src/commands/mcp.py +79 -0
  32. sovaria_cli-0.2.8/apps/cli/src/commands/oidc_providers.py +199 -0
  33. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/org.py +18 -9
  34. sovaria_cli-0.2.8/apps/cli/src/commands/passkeys.py +82 -0
  35. sovaria_cli-0.2.8/apps/cli/src/commands/policy.py +299 -0
  36. sovaria_cli-0.2.8/apps/cli/src/commands/project.py +241 -0
  37. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/realtime.py +175 -155
  38. sovaria_cli-0.2.8/apps/cli/src/commands/schema.py +61 -0
  39. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/scope.py +36 -17
  40. sovaria_cli-0.2.8/apps/cli/src/commands/secrets.py +192 -0
  41. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/sim.py +92 -54
  42. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/storage.py +54 -58
  43. sovaria_cli-0.2.8/apps/cli/src/commands/test_cmd.py +167 -0
  44. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/vector.py +43 -40
  45. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/main.py +37 -1
  46. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/api_client.py +122 -14
  47. sovaria_cli-0.2.8/apps/cli/src/utils/api_helpers.py +52 -0
  48. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/auth_store.py +86 -19
  49. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/config.py +53 -0
  50. sovaria_cli-0.2.8/apps/cli/src/utils/display.py +102 -0
  51. sovaria_cli-0.2.8/apps/cli/src/utils/local_function_server.py +148 -0
  52. sovaria_cli-0.2.8/apps/cli/src/utils/log_tail.py +53 -0
  53. sovaria_cli-0.2.8/apps/cli/src/utils/output_mode.py +18 -0
  54. sovaria_cli-0.2.8/apps/cli/src/utils/premium_display.py +1349 -0
  55. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/project_context.py +21 -0
  56. sovaria_cli-0.2.8/apps/cli/src/utils/schema_introspect.py +223 -0
  57. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/scope_picker.py +6 -24
  58. sovaria_cli-0.2.8/apps/cli/src/utils/theme.py +24 -0
  59. sovaria_cli-0.2.8/apps/cli/tests/test_aug18_cli_tickets.py +450 -0
  60. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_cli_smoke_paths.py +36 -14
  61. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_config_profiles.py +9 -0
  62. sovaria_cli-0.2.8/apps/cli/tests/test_credential_file_permissions.py +29 -0
  63. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_main_startup.py +1 -1
  64. sovaria_cli-0.2.8/apps/cli/tests/test_mcp_command.py +31 -0
  65. sovaria_cli-0.2.8/apps/cli/tests/test_phase4_commands.py +91 -0
  66. sovaria_cli-0.2.8/apps/cli/tests/test_phase5_commands.py +97 -0
  67. sovaria_cli-0.2.8/apps/cli/tests/test_phase6_commands.py +133 -0
  68. sovaria_cli-0.2.8/apps/cli/tests/test_premium_display.py +132 -0
  69. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_project_and_commands.py +10 -10
  70. sovaria_cli-0.2.8/apps/cli/tests/test_supabase_style_commands.py +63 -0
  71. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/pyproject.toml +20 -2
  72. sovaria_cli-0.2.8/requirements-dev.txt +4 -0
  73. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/requirements.txt +3 -1
  74. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/scripts/cli_sdk_happy_path.sh +2 -2
  75. sovaria_cli-0.2.8/scripts/smoke_all_commands.sh +107 -0
  76. sovaria_cli-0.2.8/scripts/smoke_chunks.sh +90 -0
  77. sovaria_cli-0.2.5/.github/workflows/ci.yml +0 -39
  78. sovaria_cli-0.2.5/apps/cli/src/commands/audit.py +0 -181
  79. sovaria_cli-0.2.5/apps/cli/src/commands/billing.py +0 -513
  80. sovaria_cli-0.2.5/apps/cli/src/commands/chrono.py +0 -248
  81. sovaria_cli-0.2.5/apps/cli/src/commands/context.py +0 -208
  82. sovaria_cli-0.2.5/apps/cli/src/commands/policy.py +0 -317
  83. sovaria_cli-0.2.5/apps/cli/src/commands/project.py +0 -130
  84. sovaria_cli-0.2.5/apps/cli/src/utils/display.py +0 -62
  85. sovaria_cli-0.2.5/requirements-dev.txt +0 -2
  86. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/.python-version +0 -0
  87. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/Makefile +0 -0
  88. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/__init__.py +0 -0
  89. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/__init__.py +0 -0
  90. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/commands/shell.py +0 -0
  91. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/__init__.py +0 -0
  92. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/src/utils/migrations.py +0 -0
  93. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_auth_store_profiles.py +0 -0
  94. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_cli_security.py +0 -0
  95. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/apps/cli/tests/test_scope_picker.py +0 -0
  96. {sovaria_cli-0.2.5 → sovaria_cli-0.2.8}/pytest.ini +0 -0
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ open-pull-requests-limit: 5
8
+ - package-ecosystem: github-actions
9
+ directory: /
10
+ schedule:
11
+ interval: weekly
12
+ open-pull-requests-limit: 5
@@ -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:
@@ -4,3 +4,7 @@ __pycache__/
4
4
  .mypy_cache/
5
5
  dist/
6
6
  *.egg-info/
7
+ .env
8
+ .env.*
9
+ !.env.example
10
+ !.env.*.example
@@ -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.5
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: sovaria-sdk>=0.2.0
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
@@ -1,3 +1,3 @@
1
1
  """Sovaria CLI — sov command."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.8"
@@ -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
- typer.echo(str(exc), err=True)
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
- typer.echo(str(exc), err=True)
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
- typer.echo(str(exc), err=True)
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
- typer.echo(str(exc), err=True)
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(..., "--org", help="Organisation ID"),
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": 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=org_id,
155
+ org_id=resolved_org,
155
156
  require_scope=False,
156
157
  )
157
158
  except CLIRequestError as exc:
158
- typer.echo(str(exc), err=True)
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, print_json
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
- typer.echo(str(exc), err=True)
46
- raise typer.Exit(1) from exc
46
+ handle_cli_error(exc)
47
47
 
48
- typer.echo(print_json(result))
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
+ )