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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secshare-cli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: CLI for SecShare — zero-knowledge secret sharing
5
5
  Project-URL: Homepage, https://secshare.link
6
6
  Project-URL: Source, https://github.com/rdney/secshare
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "secshare-cli"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  description = "CLI for SecShare — zero-knowledge secret sharing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -13,12 +13,18 @@ def _headers(auth: bool = False) -> dict:
13
13
  return h
14
14
 
15
15
 
16
- def login(email: str, password: str) -> dict:
17
- r = httpx.post(
18
- f"{get_api_base()}/auth/login",
19
- json={"email": email, "password": password, "turnstile_token": ""},
20
- timeout=TIMEOUT,
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 sys
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 with SecShare."""
66
- email = click.prompt("Email")
67
- password = click.prompt("Password", hide_input=True)
65
+ """Authenticate via browser."""
68
66
  try:
69
- data = api_login(email, password)
67
+ session = cli_init()
70
68
  except Exception as e:
71
- raise click.ClickException(f"Login failed: {e}")
72
- cfg = load_config()
73
- cfg["token"] = data["access_token"]
74
- cfg["email"] = email
75
- save_config(cfg)
76
- click.echo(f"Logged in as {email}")
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
- def get(link):
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
- Examples:
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