cartograph-cli 0.1.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.
cartograph/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Cartograph - widget library manager for AI agents."""
2
+
3
+ from importlib.metadata import version as _pkg_version
4
+
5
+ from .engine import Cartograph, LIBRARY_PATH
6
+
7
+ try:
8
+ __version__ = _pkg_version("cartograph-cli")
9
+ except Exception:
10
+ __version__ = "0.0.0"
11
+ __all__ = ["Cartograph", "LIBRARY_PATH", "__version__"]
cartograph/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m cartograph"""
2
+
3
+ from .cli import main
4
+
5
+ main()
cartograph/auth.py ADDED
@@ -0,0 +1,196 @@
1
+ """
2
+ Auth credential storage for the Cartograph cloud registry.
3
+
4
+ Credentials are stored in the platform user-config directory with 0o600
5
+ permissions so only the current user can read them. Nothing in this module
6
+ makes network calls except the token refresh path.
7
+
8
+ Stored format:
9
+ {"id_token": "...", "refresh_token": "...", "signing_key": "..."}
10
+
11
+ Environment overrides (useful for CI/scripts):
12
+ CARTOGRAPH_TOKEN — skip file storage entirely (raw ID token)
13
+ CARTOGRAPH_REGISTRY_URL — override the default registry endpoint
14
+ """
15
+
16
+ import base64
17
+ import json
18
+ import logging
19
+ import os
20
+ import stat
21
+ import time
22
+ import urllib.error
23
+ import urllib.parse
24
+ import urllib.request
25
+
26
+ from platformdirs import user_config_dir
27
+
28
+ log = logging.getLogger("cartograph")
29
+
30
+ _DEFAULT_REGISTRY_URL = "https://cartograph-api-562154372671.us-central1.run.app"
31
+ _CREDENTIALS_FILE = os.path.join(user_config_dir("cartograph"), "credentials.json")
32
+ _GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
33
+ def _google_client_id() -> str:
34
+ env = os.environ.get("GOOGLE_CLIENT_ID", "")
35
+ if env:
36
+ return env
37
+ return _read_credentials().get("client_id", "")
38
+
39
+
40
+ def _google_client_secret() -> str:
41
+ env = os.environ.get("GOOGLE_CLIENT_SECRET", "")
42
+ if env:
43
+ return env
44
+ return _read_credentials().get("client_secret", "")
45
+
46
+
47
+ def get_registry_url() -> str:
48
+ return os.environ.get("CARTOGRAPH_REGISTRY_URL", _DEFAULT_REGISTRY_URL)
49
+
50
+
51
+ def _read_credentials() -> dict:
52
+ """Read the credentials file, or return empty dict."""
53
+ try:
54
+ with open(_CREDENTIALS_FILE) as f:
55
+ return json.load(f)
56
+ except Exception:
57
+ return {}
58
+
59
+
60
+ def _decode_jwt_payload(token: str) -> dict:
61
+ """Decode the payload of a JWT without verification (client-side only)."""
62
+ try:
63
+ parts = token.split(".")
64
+ if len(parts) != 3:
65
+ return {}
66
+ # Add padding
67
+ payload = parts[1]
68
+ payload += "=" * (4 - len(payload) % 4)
69
+ return json.loads(base64.urlsafe_b64decode(payload))
70
+ except Exception:
71
+ return {}
72
+
73
+
74
+ def _is_token_expired(token: str) -> bool:
75
+ """Check if a JWT ID token is expired (with 5min buffer)."""
76
+ payload = _decode_jwt_payload(token)
77
+ exp = payload.get("exp")
78
+ if not exp:
79
+ return True
80
+ return time.time() > (exp - 300) # 5 minute buffer
81
+
82
+
83
+ def _refresh_id_token(refresh_token: str) -> str | None:
84
+ """Use a refresh token to get a new ID token from Google.
85
+
86
+ Returns the new id_token, or None on failure.
87
+ """
88
+ if not refresh_token:
89
+ return None
90
+
91
+ # Need client_id and client_secret for refresh
92
+ client_id = _google_client_id()
93
+ client_secret = _google_client_secret()
94
+ if not client_id or not client_secret:
95
+ log.debug("Cannot refresh token: GOOGLE_CLIENT_ID/SECRET not set")
96
+ return None
97
+
98
+ data = urllib.parse.urlencode({
99
+ "grant_type": "refresh_token",
100
+ "refresh_token": refresh_token,
101
+ "client_id": client_id,
102
+ "client_secret": client_secret,
103
+ }).encode()
104
+
105
+ req = urllib.request.Request(_GOOGLE_TOKEN_URL, data=data, method="POST")
106
+ try:
107
+ with urllib.request.urlopen(req, timeout=10) as resp:
108
+ result = json.loads(resp.read())
109
+ new_id_token = result.get("id_token")
110
+ if new_id_token:
111
+ # Update stored credentials with new id_token
112
+ creds = _read_credentials()
113
+ creds["id_token"] = new_id_token
114
+ _write_credentials(creds)
115
+ log.debug("ID token refreshed successfully")
116
+ return new_id_token
117
+ except Exception as e:
118
+ log.debug("Token refresh failed: %s", e)
119
+ return None
120
+
121
+
122
+
123
+ def get_token() -> str | None:
124
+ """Return a valid ID token, or None if not authenticated.
125
+
126
+ Checks expiration and auto-refreshes if needed.
127
+ """
128
+ env = os.environ.get("CARTOGRAPH_TOKEN")
129
+ if env:
130
+ return env
131
+
132
+ creds = _read_credentials()
133
+ id_token = creds.get("id_token")
134
+ if not id_token:
135
+ return None
136
+
137
+ # Check if expired and try to refresh
138
+ if _is_token_expired(id_token):
139
+ refresh_token = creds.get("refresh_token", "")
140
+ refreshed = _refresh_id_token(refresh_token)
141
+ if refreshed:
142
+ return refreshed
143
+ # Refresh failed — token is expired
144
+ log.debug("ID token expired and refresh failed")
145
+ return None
146
+
147
+ return id_token
148
+
149
+
150
+ def get_signing_key() -> str | None:
151
+ """Return the user's signing key from stored credentials."""
152
+ env = os.environ.get("CARTOGRAPH_SIGNING_KEY")
153
+ if env:
154
+ return env
155
+ creds = _read_credentials()
156
+ return creds.get("signing_key") or None
157
+
158
+
159
+ def is_authenticated() -> bool:
160
+ return get_token() is not None
161
+
162
+
163
+ def _write_credentials(creds: dict) -> None:
164
+ """Write credentials dict to disk with restricted permissions."""
165
+ os.makedirs(os.path.dirname(_CREDENTIALS_FILE), exist_ok=True)
166
+ with open(_CREDENTIALS_FILE, "w") as f:
167
+ json.dump(creds, f)
168
+ try:
169
+ os.chmod(_CREDENTIALS_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0o600
170
+ except OSError as e:
171
+ log.debug("Could not set credentials file permissions: %s", e)
172
+
173
+
174
+ def save_credentials(id_token: str, refresh_token: str, signing_key: str,
175
+ client_id: str = "", client_secret: str = "") -> None:
176
+ """Persist OAuth credentials to disk."""
177
+ creds = {
178
+ "id_token": id_token,
179
+ "refresh_token": refresh_token,
180
+ "signing_key": signing_key,
181
+ }
182
+ if client_id:
183
+ creds["client_id"] = client_id
184
+ if client_secret:
185
+ creds["client_secret"] = client_secret
186
+ _write_credentials(creds)
187
+
188
+
189
+ def clear_token() -> None:
190
+ """Remove stored credentials."""
191
+ try:
192
+ os.remove(_CREDENTIALS_FILE)
193
+ except FileNotFoundError:
194
+ pass
195
+ except OSError as e:
196
+ log.debug("Could not remove credentials file: %s", e)