tsk-cli 0.1.0__tar.gz → 0.1.5__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: tsk-cli
3
- Version: 0.1.0
3
+ Version: 0.1.5
4
4
  Summary: CLI for TSK — pull projects and things for editor agents
5
5
  Author: Rob Crosby
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tsk-cli"
7
- version = "0.1.0"
7
+ version = "0.1.5"
8
8
  description = "CLI for TSK — pull projects and things for editor agents"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -5,11 +5,12 @@ HTTP client that uses stored session cookies to talk to the TSK backend.
5
5
  from __future__ import annotations
6
6
 
7
7
  import sys
8
+ from urllib.parse import urlparse
8
9
 
9
10
  import click
10
11
  import requests
11
12
 
12
- from .config import get_auth, get_server_url
13
+ from .config import get_auth, get_server_url, save_auth
13
14
 
14
15
 
15
16
  def create_session() -> requests.Session:
@@ -23,9 +24,31 @@ def create_session() -> requests.Session:
23
24
  if csrftoken:
24
25
  s.cookies.set("csrftoken", csrftoken)
25
26
  s.headers["X-CSRFToken"] = csrftoken
27
+
28
+ # Django's CSRF middleware requires Origin (or Referer on HTTPS) for unsafe
29
+ # methods. Browsers send these automatically; urllib3/requests does not.
30
+ base = get_server_url().rstrip("/")
31
+ parsed = urlparse(base)
32
+ if parsed.scheme in ("http", "https") and parsed.netloc:
33
+ origin = f"{parsed.scheme}://{parsed.netloc}"
34
+ s.headers.setdefault("Origin", origin)
35
+ s.headers.setdefault("Referer", f"{origin}/")
36
+
26
37
  return s
27
38
 
28
39
 
40
+ def _persist_csrf_from_response(resp: requests.Response) -> None:
41
+ """If the server rotated csrftoken, keep ~/.config/tsk/auth.json in sync."""
42
+ new = resp.cookies.get("csrftoken")
43
+ if not new:
44
+ return
45
+ auth = get_auth()
46
+ sid = auth.get("sessionid", "")
47
+ if not sid:
48
+ return
49
+ save_auth(sessionid=sid, csrftoken=new)
50
+
51
+
29
52
  def require_auth() -> tuple[requests.Session, str]:
30
53
  """
31
54
  Return (session, server_url) or exit with an error message
@@ -42,14 +65,35 @@ def require_auth() -> tuple[requests.Session, str]:
42
65
  return session, server_url
43
66
 
44
67
 
68
+ def _check_auth_response(resp: requests.Response) -> None:
69
+ """Exit with a helpful message on 401/403."""
70
+ if resp.status_code == 401:
71
+ click.echo("Session expired. Run: tsk login", err=True)
72
+ sys.exit(1)
73
+ if resp.status_code == 403:
74
+ detail = ""
75
+ try:
76
+ detail = resp.json().get("detail", "")
77
+ except Exception:
78
+ pass
79
+ if "CSRF" in detail:
80
+ click.echo("CSRF check failed. Run: tsk login", err=True)
81
+ else:
82
+ click.echo(
83
+ f"Permission denied ({detail or 'no details'}). "
84
+ "You may need a higher org role, or run: tsk login",
85
+ err=True,
86
+ )
87
+ sys.exit(1)
88
+
89
+
45
90
  def api_get(path: str) -> dict | list:
46
91
  """GET an API endpoint. Exits on auth failure."""
47
92
  session, server_url = require_auth()
48
93
  resp = session.get(f"{server_url}{path}")
49
- if resp.status_code == 401 or resp.status_code == 403:
50
- click.echo("Session expired. Run: tsk login", err=True)
51
- sys.exit(1)
94
+ _check_auth_response(resp)
52
95
  resp.raise_for_status()
96
+ _persist_csrf_from_response(resp)
53
97
  return resp.json()
54
98
 
55
99
 
@@ -57,10 +101,9 @@ def api_post(path: str, json_data: dict) -> dict:
57
101
  """POST to an API endpoint. Exits on auth failure."""
58
102
  session, server_url = require_auth()
59
103
  resp = session.post(f"{server_url}{path}", json=json_data)
60
- if resp.status_code in (401, 403):
61
- click.echo("Session expired. Run: tsk login", err=True)
62
- sys.exit(1)
104
+ _check_auth_response(resp)
63
105
  resp.raise_for_status()
106
+ _persist_csrf_from_response(resp)
64
107
  return resp.json()
65
108
 
66
109
 
@@ -68,8 +111,7 @@ def api_patch(path: str, json_data: dict) -> dict:
68
111
  """PATCH an API endpoint. Exits on auth failure."""
69
112
  session, server_url = require_auth()
70
113
  resp = session.patch(f"{server_url}{path}", json=json_data)
71
- if resp.status_code in (401, 403):
72
- click.echo("Session expired. Run: tsk login", err=True)
73
- sys.exit(1)
114
+ _check_auth_response(resp)
74
115
  resp.raise_for_status()
116
+ _persist_csrf_from_response(resp)
75
117
  return resp.json()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsk-cli
3
- Version: 0.1.0
3
+ Version: 0.1.5
4
4
  Summary: CLI for TSK — pull projects and things for editor agents
5
5
  Author: Rob Crosby
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes