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.
- 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.7 → sovaria_cli-0.2.8}/.github/workflows/publish.yml +5 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/.gitignore +4 -0
- sovaria_cli-0.2.8/.gitleaks.toml +17 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/PKG-INFO +42 -2
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/README.md +37 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/__init__.py +1 -1
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/auth_product.py +17 -2
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/deploy.py +75 -9
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/governance.py +19 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/login.py +231 -37
- sovaria_cli-0.2.8/apps/cli/src/commands/mcp.py +79 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/project.py +109 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/realtime.py +37 -10
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/main.py +2 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/api_client.py +109 -12
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/auth_store.py +86 -19
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/config.py +53 -0
- sovaria_cli-0.2.8/apps/cli/tests/test_aug18_cli_tickets.py +450 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_cli_smoke_paths.py +26 -0
- {sovaria_cli-0.2.7 → 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.7 → 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.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_supabase_style_commands.py +11 -1
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/pyproject.toml +19 -2
- sovaria_cli-0.2.8/requirements-dev.txt +4 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/requirements.txt +3 -1
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/scripts/cli_sdk_happy_path.sh +2 -2
- sovaria_cli-0.2.7/.github/workflows/ci.yml +0 -39
- sovaria_cli-0.2.7/requirements-dev.txt +0 -2
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/.python-version +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/Makefile +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/__init__.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/__init__.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/admin.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/api.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/audit.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/billing.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/chrono.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/collections.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/connect.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/context.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/dashboard.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/data.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/db.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/gen.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/iam.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/init_cmd.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/inspect.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/keys.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/link_cmd.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/logs.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/oidc_providers.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/org.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/passkeys.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/policy.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/schema.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/scope.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/secrets.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/shell.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/sim.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/storage.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/test_cmd.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/commands/vector.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/__init__.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/api_helpers.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/display.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/local_function_server.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/log_tail.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/migrations.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/output_mode.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/premium_display.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/project_context.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/schema_introspect.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/scope_picker.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/src/utils/theme.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_auth_store_profiles.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_cli_security.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_phase4_commands.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_phase5_commands.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_phase6_commands.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_premium_display.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_project_and_commands.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/apps/cli/tests/test_scope_picker.py +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/pytest.ini +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/scripts/smoke_all_commands.sh +0 -0
- {sovaria_cli-0.2.7 → sovaria_cli-0.2.8}/scripts/smoke_chunks.sh +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
|
|
@@ -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
|
|
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
|
|
@@ -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
|
|
17
|
-
|
|
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"),
|