secshare-cli 0.1.0__tar.gz → 0.2.1__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.
- {secshare_cli-0.1.0 → secshare_cli-0.2.1}/PKG-INFO +1 -1
- {secshare_cli-0.1.0 → secshare_cli-0.2.1}/pyproject.toml +1 -1
- {secshare_cli-0.1.0 → secshare_cli-0.2.1}/secshare_cli/api.py +12 -6
- {secshare_cli-0.1.0 → secshare_cli-0.2.1}/secshare_cli/cli.py +56 -16
- {secshare_cli-0.1.0 → secshare_cli-0.2.1}/.gitignore +0 -0
- {secshare_cli-0.1.0 → secshare_cli-0.2.1}/README.md +0 -0
- {secshare_cli-0.1.0 → secshare_cli-0.2.1}/secshare_cli/__init__.py +0 -0
- {secshare_cli-0.1.0 → secshare_cli-0.2.1}/secshare_cli/config.py +0 -0
- {secshare_cli-0.1.0 → secshare_cli-0.2.1}/secshare_cli/crypto.py +0 -0
|
@@ -13,12 +13,18 @@ def _headers(auth: bool = False) -> dict:
|
|
|
13
13
|
return h
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
r = httpx.post(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
def cli_init() -> dict:
|
|
17
|
+
r = httpx.post(f"{get_api_base()}/auth/cli-init", timeout=TIMEOUT)
|
|
18
|
+
r.raise_for_status()
|
|
19
|
+
return r.json()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cli_poll(state: str) -> dict | None:
|
|
23
|
+
r = httpx.get(f"{get_api_base()}/auth/cli-token?state={state}", timeout=TIMEOUT)
|
|
24
|
+
if r.status_code == 202:
|
|
25
|
+
return None
|
|
26
|
+
if r.status_code == 410:
|
|
27
|
+
raise RuntimeError("Session expired. Run: secshare login")
|
|
22
28
|
r.raise_for_status()
|
|
23
29
|
return r.json()
|
|
24
30
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import
|
|
2
|
+
import time
|
|
3
|
+
import webbrowser
|
|
3
4
|
from urllib.parse import urlparse
|
|
4
5
|
|
|
5
6
|
import click
|
|
6
7
|
|
|
7
|
-
from .api import create_secret, fetch_secret
|
|
8
|
-
from .api import login as api_login
|
|
8
|
+
from .api import cli_init, cli_poll, create_secret, fetch_secret
|
|
9
9
|
from .api import whoami as api_whoami
|
|
10
10
|
from .config import get_token, get_api_base, load as load_config, save as save_config
|
|
11
11
|
from .crypto import decrypt, encrypt
|
|
@@ -62,18 +62,45 @@ def cli():
|
|
|
62
62
|
|
|
63
63
|
@cli.command()
|
|
64
64
|
def login():
|
|
65
|
-
"""Authenticate
|
|
66
|
-
email = click.prompt("Email")
|
|
67
|
-
password = click.prompt("Password", hide_input=True)
|
|
65
|
+
"""Authenticate via browser."""
|
|
68
66
|
try:
|
|
69
|
-
|
|
67
|
+
session = cli_init()
|
|
70
68
|
except Exception as e:
|
|
71
|
-
raise click.ClickException(f"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
raise click.ClickException(f"Could not start login: {e}")
|
|
70
|
+
|
|
71
|
+
state = session["state"]
|
|
72
|
+
api_host = get_api_base().replace("/api/v1", "").replace("api.", "")
|
|
73
|
+
url = f"{api_host}/cli-auth?state={state}"
|
|
74
|
+
|
|
75
|
+
click.echo(f"Opening browser to authorize...\n {url}")
|
|
76
|
+
webbrowser.open(url)
|
|
77
|
+
click.echo("Waiting for authorization (Ctrl+C to cancel)...")
|
|
78
|
+
|
|
79
|
+
deadline = time.time() + 300
|
|
80
|
+
while time.time() < deadline:
|
|
81
|
+
time.sleep(2)
|
|
82
|
+
try:
|
|
83
|
+
result = cli_poll(state)
|
|
84
|
+
except RuntimeError as e:
|
|
85
|
+
raise click.ClickException(str(e))
|
|
86
|
+
except Exception:
|
|
87
|
+
continue
|
|
88
|
+
if result is None:
|
|
89
|
+
continue
|
|
90
|
+
cfg = load_config()
|
|
91
|
+
cfg["token"] = result["access_token"]
|
|
92
|
+
try:
|
|
93
|
+
import httpx as _httpx
|
|
94
|
+
from .config import get_api_base as _base
|
|
95
|
+
me = _httpx.get(f"{_base()}/auth/me", headers={"Authorization": f"Bearer {result['access_token']}"}, timeout=10)
|
|
96
|
+
cfg["email"] = me.json().get("email", "")
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
save_config(cfg)
|
|
100
|
+
click.echo(f"Logged in as {cfg.get('email', 'unknown')}")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
raise click.ClickException("Timed out waiting for browser authorization.")
|
|
77
104
|
|
|
78
105
|
|
|
79
106
|
@cli.command()
|
|
@@ -100,14 +127,20 @@ def whoami():
|
|
|
100
127
|
|
|
101
128
|
@cli.command()
|
|
102
129
|
@click.argument("link")
|
|
103
|
-
|
|
130
|
+
@click.option("--show", is_flag=True, help="Print plaintext to terminal (use only when not piping).")
|
|
131
|
+
def get(link, show):
|
|
104
132
|
"""Fetch and decrypt a secret.
|
|
105
133
|
|
|
106
134
|
\b
|
|
107
|
-
|
|
108
|
-
secshare get https://secshare.link/s/abc123#KEY
|
|
135
|
+
Inject .env secrets into your shell (recommended):
|
|
109
136
|
eval "$(secshare get https://secshare.link/s/abc123#KEY)"
|
|
137
|
+
|
|
138
|
+
\b
|
|
139
|
+
Explicitly print to terminal:
|
|
140
|
+
secshare get https://secshare.link/s/abc123#KEY --show
|
|
110
141
|
"""
|
|
142
|
+
import sys
|
|
143
|
+
|
|
111
144
|
try:
|
|
112
145
|
secret_id, key_fragment = _parse_link(link)
|
|
113
146
|
data = fetch_secret(secret_id)
|
|
@@ -117,6 +150,13 @@ def get(link):
|
|
|
117
150
|
except Exception as e:
|
|
118
151
|
raise click.ClickException(str(e))
|
|
119
152
|
|
|
153
|
+
if sys.stdout.isatty() and not show:
|
|
154
|
+
raise click.ClickException(
|
|
155
|
+
"Refusing to print secret to terminal.\n"
|
|
156
|
+
" To inject into your shell: eval \"$(secshare get <link>)\"\n"
|
|
157
|
+
" To print explicitly: secshare get <link> --show"
|
|
158
|
+
)
|
|
159
|
+
|
|
120
160
|
# .env payload — output KEY=VALUE lines
|
|
121
161
|
try:
|
|
122
162
|
parsed = json.loads(plaintext)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|