web2cli 0.2.0__py3-none-any.whl
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.
- web2cli/__init__.py +3 -0
- web2cli/__main__.py +5 -0
- web2cli/adapter/__init__.py +0 -0
- web2cli/adapter/lint.py +667 -0
- web2cli/adapter/loader.py +157 -0
- web2cli/adapter/validator.py +127 -0
- web2cli/adapters/discord.com/web2cli.yaml +476 -0
- web2cli/adapters/mail.google.com/parsers/inbox.py +200 -0
- web2cli/adapters/mail.google.com/web2cli.yaml +52 -0
- web2cli/adapters/news.ycombinator.com/web2cli.yaml +356 -0
- web2cli/adapters/reddit.com/web2cli.yaml +233 -0
- web2cli/adapters/slack.com/web2cli.yaml +445 -0
- web2cli/adapters/stackoverflow.com/web2cli.yaml +257 -0
- web2cli/adapters/x.com/providers/x_graphql.py +299 -0
- web2cli/adapters/x.com/web2cli.yaml +449 -0
- web2cli/auth/__init__.py +0 -0
- web2cli/auth/browser_login.py +820 -0
- web2cli/auth/manager.py +166 -0
- web2cli/auth/store.py +68 -0
- web2cli/cli.py +1286 -0
- web2cli/executor/__init__.py +0 -0
- web2cli/executor/http.py +113 -0
- web2cli/output/__init__.py +0 -0
- web2cli/output/formatter.py +116 -0
- web2cli/parser/__init__.py +0 -0
- web2cli/parser/custom.py +21 -0
- web2cli/parser/html_parser.py +111 -0
- web2cli/parser/transforms.py +127 -0
- web2cli/pipe.py +10 -0
- web2cli/providers/__init__.py +6 -0
- web2cli/providers/base.py +22 -0
- web2cli/providers/registry.py +86 -0
- web2cli/runtime/__init__.py +1 -0
- web2cli/runtime/cache.py +42 -0
- web2cli/runtime/engine.py +743 -0
- web2cli/runtime/parser.py +398 -0
- web2cli/runtime/template.py +52 -0
- web2cli/types.py +71 -0
- web2cli-0.2.0.dist-info/METADATA +467 -0
- web2cli-0.2.0.dist-info/RECORD +44 -0
- web2cli-0.2.0.dist-info/WHEEL +5 -0
- web2cli-0.2.0.dist-info/entry_points.txt +2 -0
- web2cli-0.2.0.dist-info/licenses/LICENSE +202 -0
- web2cli-0.2.0.dist-info/top_level.txt +1 -0
web2cli/auth/manager.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Session management — create, get, remove, check.
|
|
2
|
+
|
|
3
|
+
Handles cookie parsing, env var fallback, and persistence via store.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from web2cli.auth.store import (
|
|
12
|
+
delete_session,
|
|
13
|
+
load_session,
|
|
14
|
+
save_session,
|
|
15
|
+
session_exists,
|
|
16
|
+
)
|
|
17
|
+
from web2cli.types import Session
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_cookie_string(raw: str) -> dict[str, str]:
|
|
21
|
+
"""Parse "k=v; k2=v2" into dict."""
|
|
22
|
+
cookies = {}
|
|
23
|
+
for pair in raw.split(";"):
|
|
24
|
+
pair = pair.strip()
|
|
25
|
+
if "=" in pair:
|
|
26
|
+
key, _, value = pair.partition("=")
|
|
27
|
+
cookies[key.strip()] = value.strip()
|
|
28
|
+
return cookies
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_cookie_file(path: str) -> dict[str, str]:
|
|
32
|
+
"""Load a JSON file as cookie dict."""
|
|
33
|
+
with open(path) as f:
|
|
34
|
+
data = json.load(f)
|
|
35
|
+
if not isinstance(data, dict):
|
|
36
|
+
raise ValueError(f"Cookie file must contain a JSON object, got {type(data).__name__}")
|
|
37
|
+
return {str(k): str(v) for k, v in data.items()}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_session(
|
|
41
|
+
domain: str,
|
|
42
|
+
cookies: dict[str, str] | None = None,
|
|
43
|
+
token: str | None = None,
|
|
44
|
+
) -> Session:
|
|
45
|
+
"""Create and persist a session."""
|
|
46
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
47
|
+
|
|
48
|
+
if cookies and token:
|
|
49
|
+
auth_type = "cookies+token"
|
|
50
|
+
data = {"cookies": cookies, "token": token}
|
|
51
|
+
elif cookies:
|
|
52
|
+
auth_type = "cookies"
|
|
53
|
+
data = {"cookies": cookies}
|
|
54
|
+
elif token:
|
|
55
|
+
auth_type = "token"
|
|
56
|
+
data = {"token": token}
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError("Either cookies or token must be provided")
|
|
59
|
+
|
|
60
|
+
session = Session(
|
|
61
|
+
domain=domain,
|
|
62
|
+
auth_type=auth_type,
|
|
63
|
+
data=data,
|
|
64
|
+
created_at=now,
|
|
65
|
+
last_used=now,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
save_session(domain, {
|
|
69
|
+
"domain": session.domain,
|
|
70
|
+
"auth_type": session.auth_type,
|
|
71
|
+
"data": session.data,
|
|
72
|
+
"created_at": session.created_at,
|
|
73
|
+
"last_used": session.last_used,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return session
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_session(domain: str, auth_spec: dict | None = None) -> Session | None:
|
|
80
|
+
"""Retrieve session for a domain.
|
|
81
|
+
|
|
82
|
+
Checks env var first (from auth_spec), then stored session file.
|
|
83
|
+
"""
|
|
84
|
+
# Env var fallback
|
|
85
|
+
if auth_spec:
|
|
86
|
+
env_cookies: dict[str, str] = {}
|
|
87
|
+
env_token: str | None = None
|
|
88
|
+
for method in auth_spec.get("methods", []):
|
|
89
|
+
env_var = method.get("env_var")
|
|
90
|
+
if env_var:
|
|
91
|
+
env_val = os.environ.get(env_var)
|
|
92
|
+
if env_val:
|
|
93
|
+
method_type = str(method.get("type", "cookies")).lower()
|
|
94
|
+
|
|
95
|
+
if method_type == "token":
|
|
96
|
+
token = env_val.strip()
|
|
97
|
+
if token:
|
|
98
|
+
env_token = token
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# Default to cookies when method type is omitted.
|
|
102
|
+
cookies = parse_cookie_string(env_val)
|
|
103
|
+
env_cookies.update(cookies)
|
|
104
|
+
|
|
105
|
+
if env_cookies and env_token:
|
|
106
|
+
return Session(
|
|
107
|
+
domain=domain,
|
|
108
|
+
auth_type="cookies+token",
|
|
109
|
+
data={"cookies": env_cookies, "token": env_token},
|
|
110
|
+
)
|
|
111
|
+
if env_cookies:
|
|
112
|
+
return Session(
|
|
113
|
+
domain=domain,
|
|
114
|
+
auth_type="cookies",
|
|
115
|
+
data={"cookies": env_cookies},
|
|
116
|
+
)
|
|
117
|
+
if env_token:
|
|
118
|
+
return Session(
|
|
119
|
+
domain=domain,
|
|
120
|
+
auth_type="token",
|
|
121
|
+
data={"token": env_token},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Stored session
|
|
125
|
+
raw = load_session(domain)
|
|
126
|
+
if raw is None:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
return Session(
|
|
130
|
+
domain=raw.get("domain", domain),
|
|
131
|
+
auth_type=raw.get("auth_type", "cookies"),
|
|
132
|
+
data=raw.get("data", {}),
|
|
133
|
+
created_at=raw.get("created_at", ""),
|
|
134
|
+
last_used=raw.get("last_used", ""),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def remove_session(domain: str) -> bool:
|
|
139
|
+
"""Remove stored session."""
|
|
140
|
+
return delete_session(domain)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_session(domain: str) -> dict:
|
|
144
|
+
"""Return session metadata (no secrets). For --status display."""
|
|
145
|
+
if not session_exists(domain):
|
|
146
|
+
return {"exists": False}
|
|
147
|
+
|
|
148
|
+
raw = load_session(domain)
|
|
149
|
+
if raw is None:
|
|
150
|
+
return {"exists": False, "error": "corrupt or unreadable"}
|
|
151
|
+
|
|
152
|
+
info = {
|
|
153
|
+
"exists": True,
|
|
154
|
+
"auth_type": raw.get("auth_type", "unknown"),
|
|
155
|
+
"created_at": raw.get("created_at", ""),
|
|
156
|
+
"last_used": raw.get("last_used", ""),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Show cookie key names (not values) for verification
|
|
160
|
+
data = raw.get("data", {})
|
|
161
|
+
if "cookies" in data:
|
|
162
|
+
info["cookie_keys"] = list(data["cookies"].keys())
|
|
163
|
+
if "token" in data:
|
|
164
|
+
info["has_token"] = True
|
|
165
|
+
|
|
166
|
+
return info
|
web2cli/auth/store.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Encrypted session storage.
|
|
2
|
+
|
|
3
|
+
Sessions are stored at ~/.web2cli/sessions/<domain>.json.enc
|
|
4
|
+
using Fernet symmetric encryption derived from machine identity.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
14
|
+
|
|
15
|
+
SESSIONS_DIR = Path.home() / ".web2cli" / "sessions"
|
|
16
|
+
FIXED_SALT = b"web2cli-session-store-v1"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_encryption_key() -> bytes:
|
|
20
|
+
"""Derive a Fernet key from hostname + username + fixed string.
|
|
21
|
+
|
|
22
|
+
This is NOT a high-security secret vault — it's obfuscation
|
|
23
|
+
to prevent casual reading of cookie values on disk.
|
|
24
|
+
"""
|
|
25
|
+
identity = f"{os.uname().nodename}:{os.getlogin()}:web2cli"
|
|
26
|
+
dk = hashlib.pbkdf2_hmac("sha256", identity.encode(), FIXED_SALT, 100_000)
|
|
27
|
+
return base64.urlsafe_b64encode(dk)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ensure_dir() -> None:
|
|
31
|
+
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_session(domain: str, data: dict) -> None:
|
|
35
|
+
"""Encrypt and persist session data for a domain."""
|
|
36
|
+
_ensure_dir()
|
|
37
|
+
fernet = Fernet(_get_encryption_key())
|
|
38
|
+
encrypted = fernet.encrypt(json.dumps(data).encode())
|
|
39
|
+
path = SESSIONS_DIR / f"{domain}.json.enc"
|
|
40
|
+
path.write_bytes(encrypted)
|
|
41
|
+
path.chmod(0o600)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_session(domain: str) -> dict | None:
|
|
45
|
+
"""Load and decrypt session for a domain. Returns None if missing/corrupt."""
|
|
46
|
+
path = SESSIONS_DIR / f"{domain}.json.enc"
|
|
47
|
+
if not path.exists():
|
|
48
|
+
return None
|
|
49
|
+
try:
|
|
50
|
+
fernet = Fernet(_get_encryption_key())
|
|
51
|
+
decrypted = fernet.decrypt(path.read_bytes())
|
|
52
|
+
return json.loads(decrypted)
|
|
53
|
+
except (InvalidToken, json.JSONDecodeError, OSError):
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def delete_session(domain: str) -> bool:
|
|
58
|
+
"""Remove session file. Returns True if deleted."""
|
|
59
|
+
path = SESSIONS_DIR / f"{domain}.json.enc"
|
|
60
|
+
if path.exists():
|
|
61
|
+
path.unlink()
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def session_exists(domain: str) -> bool:
|
|
67
|
+
"""Check if a session file exists for a domain."""
|
|
68
|
+
return (SESSIONS_DIR / f"{domain}.json.enc").exists()
|