sovaria-cli 0.2.7__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 (89) 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.7 → sovaria_cli-0.2.8}/.github/workflows/publish.yml +5 -0
  4. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/.gitignore +4 -0
  5. sovaria_cli-0.2.8/.gitleaks.toml +17 -0
  6. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/PKG-INFO +42 -2
  7. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/README.md +37 -0
  8. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/__init__.py +1 -1
  9. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/auth_product.py +17 -2
  10. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/deploy.py +75 -9
  11. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/governance.py +19 -0
  12. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/login.py +231 -37
  13. sovaria_cli-0.2.8/apps/cli/src/commands/mcp.py +79 -0
  14. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/project.py +109 -0
  15. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/realtime.py +37 -10
  16. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/main.py +2 -0
  17. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/api_client.py +109 -12
  18. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/auth_store.py +86 -19
  19. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/config.py +53 -0
  20. sovaria_cli-0.2.8/apps/cli/tests/test_aug18_cli_tickets.py +450 -0
  21. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_cli_smoke_paths.py +26 -0
  22. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_config_profiles.py +9 -0
  23. sovaria_cli-0.2.8/apps/cli/tests/test_credential_file_permissions.py +29 -0
  24. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_main_startup.py +1 -1
  25. sovaria_cli-0.2.8/apps/cli/tests/test_mcp_command.py +31 -0
  26. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_supabase_style_commands.py +11 -1
  27. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/pyproject.toml +19 -2
  28. sovaria_cli-0.2.8/requirements-dev.txt +4 -0
  29. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/requirements.txt +3 -1
  30. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/scripts/cli_sdk_happy_path.sh +2 -2
  31. sovaria_cli-0.2.7/.github/workflows/ci.yml +0 -39
  32. sovaria_cli-0.2.7/requirements-dev.txt +0 -2
  33. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/.python-version +0 -0
  34. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/Makefile +0 -0
  35. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/__init__.py +0 -0
  36. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/__init__.py +0 -0
  37. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/admin.py +0 -0
  38. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/api.py +0 -0
  39. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/audit.py +0 -0
  40. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/billing.py +0 -0
  41. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/chrono.py +0 -0
  42. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/collections.py +0 -0
  43. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/connect.py +0 -0
  44. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/context.py +0 -0
  45. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/dashboard.py +0 -0
  46. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/data.py +0 -0
  47. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/db.py +0 -0
  48. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/gen.py +0 -0
  49. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/iam.py +0 -0
  50. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/init_cmd.py +0 -0
  51. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/inspect.py +0 -0
  52. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/keys.py +0 -0
  53. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/link_cmd.py +0 -0
  54. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/logs.py +0 -0
  55. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/oidc_providers.py +0 -0
  56. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/org.py +0 -0
  57. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/passkeys.py +0 -0
  58. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/policy.py +0 -0
  59. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/schema.py +0 -0
  60. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/scope.py +0 -0
  61. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/secrets.py +0 -0
  62. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/shell.py +0 -0
  63. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/sim.py +0 -0
  64. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/storage.py +0 -0
  65. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/test_cmd.py +0 -0
  66. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/vector.py +0 -0
  67. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/__init__.py +0 -0
  68. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/api_helpers.py +0 -0
  69. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/display.py +0 -0
  70. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/local_function_server.py +0 -0
  71. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/log_tail.py +0 -0
  72. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/migrations.py +0 -0
  73. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/output_mode.py +0 -0
  74. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/premium_display.py +0 -0
  75. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/project_context.py +0 -0
  76. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/schema_introspect.py +0 -0
  77. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/scope_picker.py +0 -0
  78. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/theme.py +0 -0
  79. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_auth_store_profiles.py +0 -0
  80. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_cli_security.py +0 -0
  81. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_phase4_commands.py +0 -0
  82. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_phase5_commands.py +0 -0
  83. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_phase6_commands.py +0 -0
  84. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_premium_display.py +0 -0
  85. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_project_and_commands.py +0 -0
  86. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_scope_picker.py +0 -0
  87. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/pytest.ini +0 -0
  88. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/scripts/smoke_all_commands.sh +0 -0
  89. {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/scripts/smoke_chunks.sh +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.7
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
@@ -12,14 +12,17 @@ Requires-Dist: httpx>=0.28.1
12
12
  Requires-Dist: keyring>=25.0
13
13
  Requires-Dist: pyyaml>=6.0
14
14
  Requires-Dist: rich>=13.7.0
15
- Requires-Dist: sovaria-sdk>=0.2.0
15
+ Requires-Dist: sovaria-sdk==0.4.0
16
16
  Requires-Dist: tomli-w>=1.2.0
17
17
  Requires-Dist: typer>=0.17.4
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: build>=1.2; extra == 'dev'
20
+ Requires-Dist: mypy>=1.13; extra == 'dev'
20
21
  Requires-Dist: pytest>=8.0; extra == 'dev'
21
22
  Requires-Dist: ruff>=0.9; extra == 'dev'
22
23
  Requires-Dist: twine>=6.0; extra == 'dev'
24
+ Provides-Extra: mcp
25
+ Requires-Dist: sovaria-sdk[mcp]==0.4.0; extra == 'mcp'
23
26
  Description-Content-Type: text/markdown
24
27
 
25
28
  # Sovaria CLI
@@ -61,6 +64,43 @@ scripts/cli_sdk_happy_path.sh
61
64
 
62
65
  If you already have valid CLI tokens, omit `SOVARIA_EMAIL` and `SOVARIA_PASSWORD`.
63
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
+
64
104
  ## Core Command Groups
65
105
 
66
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.7"
3
+ __version__ = "0.2.8"
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import getpass
6
+ import os
5
7
  from typing import TypedDict
6
8
  from urllib.parse import quote
7
9
 
@@ -9,7 +11,7 @@ import typer
9
11
 
10
12
  from apps.cli.src.utils.api_client import CLIRequestError, call_api
11
13
  from apps.cli.src.utils.display import handle_cli_error, print_json
12
- from apps.cli.src.utils.premium_display import run_with_spinner
14
+ from apps.cli.src.utils.premium_display import print_error_panel, run_with_spinner
13
15
  from apps.cli.src.utils.config import get_scope
14
16
 
15
17
  app = typer.Typer(help="Manage product users within an org/project scope")
@@ -33,6 +35,19 @@ def _scope_args(
33
35
  }
34
36
 
35
37
 
38
+ def _read_new_user_password() -> str:
39
+ env = (os.getenv("SOVARIA_USER_PASSWORD") or "").strip()
40
+ if env:
41
+ return env
42
+ password = getpass.getpass("New user password: ")
43
+ if not password:
44
+ print_error_panel(
45
+ "Password is required. Enter it at the prompt or set SOVARIA_USER_PASSWORD."
46
+ )
47
+ raise typer.Exit(1)
48
+ return password
49
+
50
+
36
51
  def _project_auth_path(profile: str | None, project_id: str | None, suffix: str) -> str:
37
52
  resolved_project = (project_id or get_scope(profile).get("project_id") or "").strip()
38
53
  if not resolved_project:
@@ -164,7 +179,6 @@ def set_roles(
164
179
  @app.command("create")
165
180
  def create_user(
166
181
  email: str = typer.Option(..., "--email"),
167
- password: str = typer.Option(..., "--password"),
168
182
  name: str | None = typer.Option(None, "--name", help="Display name (defaults to email local-part)"),
169
183
  profile: str | None = typer.Option(None, "--profile"),
170
184
  org_id: str | None = typer.Option(None, "--org"),
@@ -173,6 +187,7 @@ def create_user(
173
187
  ) -> None:
174
188
  """Create a product user in the current project scope."""
175
189
  display_name = name or email.split("@", maxsplit=1)[0]
190
+ password = _read_new_user_password()
176
191
  try:
177
192
  result = run_with_spinner(
178
193
  f"Creating user {email}…",
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import io
6
6
  import json
7
+ import os
8
+ import subprocess
7
9
  import time
8
10
  import zipfile
9
11
  from pathlib import Path
@@ -13,8 +15,18 @@ import httpx
13
15
  import typer
14
16
 
15
17
  from apps.cli.src.utils.api_client import CLIRequestError, build_request_context, call_api
16
- from apps.cli.src.utils.display import handle_cli_error, handle_command_error, print_json, print_success
17
- from apps.cli.src.utils.premium_display import format_http_error, poll_with_progress, print_error_panel, run_with_spinner
18
+ from apps.cli.src.utils.display import (
19
+ handle_cli_error,
20
+ handle_command_error,
21
+ print_json,
22
+ print_success,
23
+ )
24
+ from apps.cli.src.utils.premium_display import (
25
+ format_http_error,
26
+ poll_with_progress,
27
+ print_error_panel,
28
+ run_with_spinner,
29
+ )
18
30
  from apps.cli.src.utils.project_context import resolve_scope
19
31
 
20
32
  app = typer.Typer()
@@ -25,6 +37,27 @@ _ASYNC_POLL_INTERVAL = 2 # seconds
25
37
  _ASYNC_POLL_TIMEOUT = 600 # seconds
26
38
 
27
39
 
40
+ def _github_pat() -> str | None:
41
+ """Resolve a GitHub PAT from SOVARIA_GITHUB_TOKEN or `gh auth token`."""
42
+ env = (os.getenv("SOVARIA_GITHUB_TOKEN") or "").strip()
43
+ if env:
44
+ return env
45
+ try:
46
+ completed = subprocess.run(
47
+ ["gh", "auth", "token"],
48
+ check=False,
49
+ capture_output=True,
50
+ text=True,
51
+ timeout=8,
52
+ )
53
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
54
+ return None
55
+ if completed.returncode != 0:
56
+ return None
57
+ token = (completed.stdout or "").strip()
58
+ return token or None
59
+
60
+
28
61
  def _http_error_panel(exc: httpx.HTTPStatusError, *, path: str) -> None:
29
62
  detail: object = exc.response.text
30
63
  try:
@@ -64,8 +97,10 @@ def _forge_context(
64
97
  return api_url, headers, resolved_org, resolved_project
65
98
  except CLIRequestError as exc:
66
99
  handle_cli_error(exc)
100
+ raise
67
101
  except ValueError as exc:
68
102
  handle_command_error(exc)
103
+ raise
69
104
 
70
105
 
71
106
  @app.command("deploy")
@@ -112,10 +147,32 @@ def deploy(
112
147
 
113
148
  headers = dict(headers)
114
149
 
150
+ def _plan() -> dict[str, Any]:
151
+ resp = httpx.post(
152
+ f"{api_url}/api/v1/fn/plan",
153
+ headers=headers,
154
+ files={"bundle": (f"{fn_name}.zip", zip_bytes, "application/zip")},
155
+ data={"name": fn_name, "project_id": effective_project},
156
+ timeout=60,
157
+ )
158
+ resp.raise_for_status()
159
+ return resp.json()
160
+
161
+ # Plan first: Forge validates the bundle and signs a plan bound to these exact
162
+ # bytes; the deploy below applies only what was planned.
163
+ try:
164
+ plan = run_with_spinner("Planning deploy…", _plan, success="Plan approved")
165
+ except httpx.HTTPStatusError as exc:
166
+ _http_error_panel(exc, path="/api/v1/fn/plan")
167
+ plan_token = str(plan.get("token") or "")
168
+ if not wants_json():
169
+ for check in plan.get("checks") or []:
170
+ typer.echo(f" ✓ {check.get('check')}")
171
+
115
172
  def _upload() -> dict[str, Any]:
116
173
  resp = httpx.post(
117
174
  f"{api_url}/api/v1/fn/deploy",
118
- headers=headers,
175
+ headers={**headers, "X-Sovaria-Deploy-Plan": plan_token} if plan_token else headers,
119
176
  files={"bundle": (f"{fn_name}.zip", zip_bytes, "application/zip")},
120
177
  data={
121
178
  "name": fn_name,
@@ -1111,15 +1168,18 @@ def bindings_set(
1111
1168
 
1112
1169
  @app.command("github-repos")
1113
1170
  def github_repos(
1114
- token: str | None = typer.Option(None, "--token", help="Optional GitHub PAT override"),
1115
1171
  limit: int = typer.Option(30, "--limit", min=1, max=100),
1116
1172
  profile: str | None = typer.Option(None, "--profile"),
1117
1173
  org_id: str | None = typer.Option(None, "--org"),
1118
1174
  project_id: str | None = typer.Option(None, "--project"),
1119
1175
  env_id: str | None = typer.Option(None, "--env"),
1120
1176
  ) -> None:
1121
- """List GitHub repositories available for import."""
1177
+ """List GitHub repositories available for import.
1178
+
1179
+ Optional PAT: set SOVARIA_GITHUB_TOKEN or authenticate with `gh auth login`.
1180
+ """
1122
1181
  body: dict[str, object] = {"limit": limit}
1182
+ token = _github_pat()
1123
1183
  if token:
1124
1184
  body["token"] = token
1125
1185
  try:
@@ -1148,19 +1208,22 @@ def github_import(
1148
1208
  repository: str = typer.Option(..., "--repo", help="owner/repo"),
1149
1209
  path: str = typer.Option(..., "--path", help="Path within the repository"),
1150
1210
  ref: str = typer.Option("main", "--ref"),
1151
- token: str | None = typer.Option(None, "--token"),
1152
1211
  profile: str | None = typer.Option(None, "--profile"),
1153
1212
  org_id: str | None = typer.Option(None, "--org"),
1154
1213
  project_id: str | None = typer.Option(None, "--project"),
1155
1214
  env_id: str | None = typer.Option(None, "--env"),
1156
1215
  ) -> None:
1157
- """Import a function from a GitHub repository path."""
1216
+ """Import a function from a GitHub repository path.
1217
+
1218
+ Optional PAT: set SOVARIA_GITHUB_TOKEN or authenticate with `gh auth login`.
1219
+ """
1158
1220
  body: dict[str, object] = {
1159
1221
  "function_name": function_name,
1160
1222
  "repository": repository,
1161
1223
  "path": path,
1162
1224
  "ref": ref,
1163
1225
  }
1226
+ token = _github_pat()
1164
1227
  if token:
1165
1228
  body["token"] = token
1166
1229
  try:
@@ -1190,7 +1253,6 @@ def github_commit(
1190
1253
  path: str = typer.Option(..., "--path", help="Target path in the repository"),
1191
1254
  message: str = typer.Option(..., "--message", help="Commit message"),
1192
1255
  ref: str = typer.Option("main", "--ref"),
1193
- token: str | None = typer.Option(None, "--token"),
1194
1256
  committer_name: str | None = typer.Option(None, "--committer-name"),
1195
1257
  committer_email: str | None = typer.Option(None, "--committer-email"),
1196
1258
  profile: str | None = typer.Option(None, "--profile"),
@@ -1198,7 +1260,10 @@ def github_commit(
1198
1260
  project_id: str | None = typer.Option(None, "--project"),
1199
1261
  env_id: str | None = typer.Option(None, "--env"),
1200
1262
  ) -> None:
1201
- """Commit local function source back to GitHub."""
1263
+ """Commit local function source back to GitHub.
1264
+
1265
+ Optional PAT: set SOVARIA_GITHUB_TOKEN or authenticate with `gh auth login`.
1266
+ """
1202
1267
  body: dict[str, object] = {
1203
1268
  "function_name": function_name,
1204
1269
  "repository": repository,
@@ -1206,6 +1271,7 @@ def github_commit(
1206
1271
  "message": message,
1207
1272
  "ref": ref,
1208
1273
  }
1274
+ token = _github_pat()
1209
1275
  if token:
1210
1276
  body["token"] = token
1211
1277
  if committer_name:
@@ -277,6 +277,25 @@ def db_resize(
277
277
  )
278
278
 
279
279
 
280
+ @app.command("db-reconcile")
281
+ def db_reconcile(
282
+ org_id: str = typer.Argument(..., help="Organisation / tenant ID"),
283
+ project_id: str = typer.Argument(..., help="Project ID"),
284
+ profile: str | None = typer.Option(None, "--profile"),
285
+ ) -> None:
286
+ """Backfill tenant Postgres mirrors (policies, functions, platform schemas)."""
287
+ premium_mutate(
288
+ "Reconciling tenant data plane…",
289
+ success="Reconciled",
290
+ title="Database reconcile",
291
+ method="POST",
292
+ path=_gov_path("tenants", org_id, "databases", project_id, "reconcile"),
293
+ profile=profile,
294
+ require_scope=False,
295
+ body={},
296
+ )
297
+
298
+
280
299
  @app.command("db-backups")
281
300
  def db_backups(
282
301
  org_id: str = typer.Argument(..., help="Organisation / tenant ID"),