movitera-cli 0.1.0__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.
@@ -0,0 +1,22 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*.*.*']
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v3
16
+ with:
17
+ python-version: "3.13"
18
+ - run: uv sync
19
+ - run: uv run pytest
20
+ - run: uv run mypy movitera_cli/
21
+ - run: uv build
22
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,18 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v3
14
+ with:
15
+ python-version: "3.13"
16
+ - run: uv sync
17
+ - run: uv run pytest
18
+ - run: uv run mypy movitera_cli/
@@ -0,0 +1,35 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+
8
+ # Virtual environments
9
+ .venv/
10
+ venv/
11
+ env/
12
+
13
+ # Distribution / packaging
14
+ dist/
15
+ build/
16
+ *.egg-info/
17
+ *.egg
18
+
19
+ # Test / coverage
20
+ .pytest_cache/
21
+ .mypy_cache/
22
+ .coverage
23
+ .coverage.*
24
+ coverage.xml
25
+ htmlcov/
26
+
27
+ # Editor / OS
28
+ .idea/
29
+ .vscode/
30
+ *.swp
31
+ .DS_Store
32
+
33
+ # Local env
34
+ .env
35
+ .env.local
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Movitera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: movitera-cli
3
+ Version: 0.1.0
4
+ Summary: Movitera CLI: inject vault secrets as environment variables, generate TOTP codes
5
+ Project-URL: Homepage, https://github.com/joaoheusi/movitera-cli
6
+ Project-URL: Repository, https://github.com/joaoheusi/movitera-cli
7
+ Project-URL: Issues, https://github.com/joaoheusi/movitera-cli/issues
8
+ Author: Movitera
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,dotenv,movitera,password-manager,secrets,totp,vault
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: System :: Systems Administration
23
+ Requires-Python: <4,>=3.11
24
+ Requires-Dist: click<9,>=8.1
25
+ Requires-Dist: httpx<1,>=0.28.1
26
+ Requires-Dist: keyring<26,>=25
27
+ Requires-Dist: pyotp<3,>=2.9
28
+ Description-Content-Type: text/markdown
29
+
30
+ # movitera CLI
31
+
32
+ Inject Movitera vault secrets as environment variables, render dotenv files, and generate TOTP codes from the terminal. Authenticates with a Movitera vault personal access token (PAT).
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install movitera-cli
38
+ # or install as an isolated CLI tool (recommended):
39
+ pipx install movitera-cli
40
+ # or with uv:
41
+ uv tool install movitera-cli
42
+ # or run without installing:
43
+ uvx --from movitera-cli movitera --help
44
+ ```
45
+
46
+ ## Quick start
47
+
48
+ ```bash
49
+ # 1. Mint a PAT in the web app (Settings → Vault → Access Tokens),
50
+ # then paste it once:
51
+ movitera login
52
+
53
+ # 2. Run any command with secrets injected as env vars:
54
+ movitera run --team <teamId> --credential myapp-prod -- npm start
55
+
56
+ # 3. Or dump as dotenv:
57
+ movitera secrets pull --team <teamId> --credential myapp-prod > .env
58
+
59
+ # 4. Or generate a TOTP code for a credential:
60
+ movitera totp --team <teamId> <credential-id>
61
+ ```
62
+
63
+ ## How it works
64
+
65
+ `movitera run -- cmd` fetches the dotenv body from
66
+ `GET /vault/credentials/by-name/{name}/env`, then `exec`s the child process
67
+ with those vars merged into its environment. The CLI never writes secrets
68
+ to disk — they exist only in the child process's `environ`.
69
+
70
+ Tokens are stored in the OS keyring (Keychain on macOS, libsecret on Linux,
71
+ Credential Manager on Windows) with a file fallback under
72
+ `~/.config/movitera/token` (mode `0o600`) if no keyring backend is
73
+ available.
74
+
75
+ ## Config
76
+
77
+ | Env var | Purpose | Default |
78
+ |---|---|---|
79
+ | `MOVITERA_API_URL` | API base URL | `https://api.movitera.com` |
80
+ | `MOVITERA_TOKEN` | PAT override (skips keyring lookup) | — |
81
+ | `MOVITERA_TEAM` | Default team id (you can omit `--team`) | — |
82
+
83
+ ## Commands
84
+
85
+ ### `movitera login`
86
+ Prompts for a PAT (or reads it from stdin via `--stdin`) and stores it.
87
+
88
+ ### `movitera logout`
89
+ Removes the stored PAT.
90
+
91
+ ### `movitera run --team T --credential N -- <cmd> [args...]`
92
+ Fetches the `ENV_BUNDLE` credential named `N` and execs `<cmd>` with those
93
+ vars in the environment.
94
+
95
+ ### `movitera secrets pull --team T --credential N`
96
+ Writes the dotenv body to stdout. Exits non-zero on resolution failures so
97
+ you can chain `movitera secrets pull ... > .env`.
98
+
99
+ ### `movitera totp --team T <credential-id>`
100
+ Prints the current TOTP code for the credential's `OTPAUTH_URI` field.
101
+
102
+ ### `movitera tokens list --team T` / `movitera tokens revoke <id>`
103
+ Manage your own PATs from the CLI.
104
+
105
+ ## Security model
106
+
107
+ - PATs have full 256-bit entropy and are stored as SHA-256 on the server,
108
+ so a server compromise cannot recover the plaintext.
109
+ - Every `movitera run` invocation is audited server-side, but PAT reads
110
+ are **coalesced per hour per token** (see the backend's
111
+ `FETCHED_VIA_TOKEN` aggregation) so a hot dev loop doesn't drown the log.
112
+ - Group/team access is re-checked on every request: if your vault scope is
113
+ revoked, your PAT immediately loses access — no token-cache divergence.
@@ -0,0 +1,84 @@
1
+ # movitera CLI
2
+
3
+ Inject Movitera vault secrets as environment variables, render dotenv files, and generate TOTP codes from the terminal. Authenticates with a Movitera vault personal access token (PAT).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install movitera-cli
9
+ # or install as an isolated CLI tool (recommended):
10
+ pipx install movitera-cli
11
+ # or with uv:
12
+ uv tool install movitera-cli
13
+ # or run without installing:
14
+ uvx --from movitera-cli movitera --help
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ # 1. Mint a PAT in the web app (Settings → Vault → Access Tokens),
21
+ # then paste it once:
22
+ movitera login
23
+
24
+ # 2. Run any command with secrets injected as env vars:
25
+ movitera run --team <teamId> --credential myapp-prod -- npm start
26
+
27
+ # 3. Or dump as dotenv:
28
+ movitera secrets pull --team <teamId> --credential myapp-prod > .env
29
+
30
+ # 4. Or generate a TOTP code for a credential:
31
+ movitera totp --team <teamId> <credential-id>
32
+ ```
33
+
34
+ ## How it works
35
+
36
+ `movitera run -- cmd` fetches the dotenv body from
37
+ `GET /vault/credentials/by-name/{name}/env`, then `exec`s the child process
38
+ with those vars merged into its environment. The CLI never writes secrets
39
+ to disk — they exist only in the child process's `environ`.
40
+
41
+ Tokens are stored in the OS keyring (Keychain on macOS, libsecret on Linux,
42
+ Credential Manager on Windows) with a file fallback under
43
+ `~/.config/movitera/token` (mode `0o600`) if no keyring backend is
44
+ available.
45
+
46
+ ## Config
47
+
48
+ | Env var | Purpose | Default |
49
+ |---|---|---|
50
+ | `MOVITERA_API_URL` | API base URL | `https://api.movitera.com` |
51
+ | `MOVITERA_TOKEN` | PAT override (skips keyring lookup) | — |
52
+ | `MOVITERA_TEAM` | Default team id (you can omit `--team`) | — |
53
+
54
+ ## Commands
55
+
56
+ ### `movitera login`
57
+ Prompts for a PAT (or reads it from stdin via `--stdin`) and stores it.
58
+
59
+ ### `movitera logout`
60
+ Removes the stored PAT.
61
+
62
+ ### `movitera run --team T --credential N -- <cmd> [args...]`
63
+ Fetches the `ENV_BUNDLE` credential named `N` and execs `<cmd>` with those
64
+ vars in the environment.
65
+
66
+ ### `movitera secrets pull --team T --credential N`
67
+ Writes the dotenv body to stdout. Exits non-zero on resolution failures so
68
+ you can chain `movitera secrets pull ... > .env`.
69
+
70
+ ### `movitera totp --team T <credential-id>`
71
+ Prints the current TOTP code for the credential's `OTPAUTH_URI` field.
72
+
73
+ ### `movitera tokens list --team T` / `movitera tokens revoke <id>`
74
+ Manage your own PATs from the CLI.
75
+
76
+ ## Security model
77
+
78
+ - PATs have full 256-bit entropy and are stored as SHA-256 on the server,
79
+ so a server compromise cannot recover the plaintext.
80
+ - Every `movitera run` invocation is audited server-side, but PAT reads
81
+ are **coalesced per hour per token** (see the backend's
82
+ `FETCHED_VIA_TOKEN` aggregation) so a hot dev loop doesn't drown the log.
83
+ - Group/team access is re-checked on every request: if your vault scope is
84
+ revoked, your PAT immediately loses access — no token-cache divergence.
@@ -0,0 +1,25 @@
1
+ """Movitera CLI for vault secrets, dotenv export, TOTP codes.
2
+
3
+ Also exposes a small SDK surface for programmatic use:
4
+
5
+ from movitera_cli import VaultClient
6
+ from movitera_cli.token_store import load_token
7
+
8
+ client = VaultClient(api_url="https://api.movitera.com", token=load_token())
9
+ env = client.get_env(team_id="...", name="myapp-prod") # raw dotenv text
10
+
11
+ That's intentionally thin — the API itself is the SDK. We don't wrap it in
12
+ domain objects because the surface evolves with the backend and the wire
13
+ shape is small enough to consume directly.
14
+ """
15
+
16
+ from movitera_cli.client import AccessTokenSummary, MoviteraError, VaultClient
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ __all__ = [
21
+ "AccessTokenSummary",
22
+ "MoviteraError",
23
+ "VaultClient",
24
+ "__version__",
25
+ ]
@@ -0,0 +1,131 @@
1
+ """HTTP client wrapping the Movitera vault API for the CLI.
2
+
3
+ Thin wrapper around `httpx` that:
4
+ - Adds the `Authorization: Bearer <pat>` header.
5
+ - Surfaces server `errorCode` strings in raised exceptions so the CLI can
6
+ print actionable messages (e.g. CREDENTIAL_NAME_NOT_UNIQUE → "try --id").
7
+ - Picks `text/plain` vs JSON correctly per endpoint.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ import httpx
16
+
17
+
18
+ class MoviteraError(Exception):
19
+ """API call returned a non-2xx response we want to surface to the user."""
20
+
21
+ def __init__(
22
+ self,
23
+ message: str,
24
+ *,
25
+ status_code: int | None = None,
26
+ error_code: str | None = None,
27
+ ):
28
+ super().__init__(message)
29
+ self.status_code = status_code
30
+ self.error_code = error_code
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class AccessTokenSummary:
35
+ id: str
36
+ name: str
37
+ prefix: str
38
+ clientKind: str
39
+ createdAt: str
40
+ expiresAt: str | None
41
+ lastUsedAt: str | None
42
+
43
+
44
+ class VaultClient:
45
+ def __init__(self, api_url: str, token: str, *, timeout: float = 30.0):
46
+ self._client = httpx.Client(
47
+ base_url=api_url,
48
+ headers={"Authorization": f"Bearer {token}"},
49
+ timeout=timeout,
50
+ )
51
+
52
+ def close(self) -> None:
53
+ self._client.close()
54
+
55
+ def __enter__(self) -> VaultClient:
56
+ return self
57
+
58
+ def __exit__(self, *_: Any) -> None:
59
+ self.close()
60
+
61
+ # ----- env bundle (CLI hot path) ------------------------------------
62
+
63
+ def get_env(self, team_id: str, name: str) -> str:
64
+ """Return raw dotenv text for the named ENV_BUNDLE credential."""
65
+ response = self._client.get(
66
+ f"/vault/credentials/by-name/{name}/env",
67
+ params={"teamId": team_id},
68
+ )
69
+ self._raise_for_status(response)
70
+ return response.text
71
+
72
+ # ----- TOTP ----------------------------------------------------------
73
+
74
+ def get_totp(self, credential_id: str) -> tuple[str, str]:
75
+ """Return (code, ISO-8601 expiresAt) from the server-side endpoint."""
76
+ response = self._client.get(f"/vault/credentials/{credential_id}/totp")
77
+ self._raise_for_status(response)
78
+ body = response.json()
79
+ return body["code"], body["expiresAt"]
80
+
81
+ # ----- access tokens (self-service) ---------------------------------
82
+
83
+ def list_access_tokens(self, team_id: str) -> list[AccessTokenSummary]:
84
+ response = self._client.get(
85
+ "/vault/access-tokens", params={"teamId": team_id}
86
+ )
87
+ self._raise_for_status(response)
88
+ return [
89
+ AccessTokenSummary(
90
+ id=item["id"],
91
+ name=item["name"],
92
+ prefix=item["prefix"],
93
+ clientKind=item["clientKind"],
94
+ createdAt=item["createdAt"],
95
+ expiresAt=item.get("expiresAt"),
96
+ lastUsedAt=item.get("lastUsedAt"),
97
+ )
98
+ for item in response.json()
99
+ ]
100
+
101
+ def revoke_access_token(self, token_id: str) -> None:
102
+ response = self._client.delete(f"/vault/access-tokens/{token_id}")
103
+ self._raise_for_status(response)
104
+
105
+ # ----- error handling -----------------------------------------------
106
+
107
+ @staticmethod
108
+ def _raise_for_status(response: httpx.Response) -> None:
109
+ if response.is_success:
110
+ return
111
+ error_code: str | None = None
112
+ message: str = response.text
113
+ try:
114
+ body = response.json()
115
+ detail = body.get("detail") if isinstance(body, dict) else None
116
+ if isinstance(detail, dict):
117
+ error_code = detail.get("errorCode")
118
+ messages = detail.get("errorMessages") or {}
119
+ # Prefer en-US; fall back to whatever's there.
120
+ message = (
121
+ messages.get("en-US")
122
+ or next(iter(messages.values()), None)
123
+ or message
124
+ )
125
+ except ValueError:
126
+ pass
127
+ raise MoviteraError(
128
+ message,
129
+ status_code=response.status_code,
130
+ error_code=error_code,
131
+ )
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+ _DEFAULT_API_URL = "https://api.movitera.com"
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Settings:
11
+ api_url: str
12
+ default_team_id: str | None
13
+ inline_token: str | None
14
+
15
+ @classmethod
16
+ def from_env(cls) -> Settings:
17
+ return cls(
18
+ api_url=os.environ.get("MOVITERA_API_URL", _DEFAULT_API_URL).rstrip("/"),
19
+ default_team_id=os.environ.get("MOVITERA_TEAM") or None,
20
+ # MOVITERA_TOKEN lets CI runners skip the keyring entirely.
21
+ inline_token=os.environ.get("MOVITERA_TOKEN") or None,
22
+ )