Vintendopower commited on
Commit
a38ca42
·
verified ·
1 Parent(s): 2ec5fd0

Upload 9 files

Browse files
beta_keys_schema.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VTX beta key schema, migration, and validation helpers.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import secrets
7
+ import string
8
+ from datetime import date
9
+ from typing import Any
10
+
11
+ VALID_STATUSES = frozenset({"ACTIVE", "REVOKED", "EXPIRED", "BANNED", "INVALID"})
12
+ KEY_CHARS = string.ascii_uppercase + string.digits
13
+ NEW_KEY_PREFIX = "VTX-"
14
+
15
+
16
+ def today_iso() -> str:
17
+ return date.today().isoformat()
18
+
19
+
20
+ def _random_key_segment(length: int = 6) -> str:
21
+ return "".join(secrets.choice(KEY_CHARS) for _ in range(length))
22
+
23
+
24
+ def generate_unique_key(existing: dict[str, Any]) -> str:
25
+ for _ in range(5000):
26
+ candidate = f"{NEW_KEY_PREFIX}{_random_key_segment(6)}"
27
+ if candidate not in existing:
28
+ return candidate
29
+ raise RuntimeError("Unable to generate a unique beta key.")
30
+
31
+
32
+ def normalize_record(key: str, record: dict[str, Any] | None) -> dict[str, Any]:
33
+ data = dict(record or {})
34
+ key_text = str(data.get("key") or key or "").strip()
35
+ active = bool(data.get("active"))
36
+ status = str(data.get("status") or "").strip().upper()
37
+ if not status:
38
+ status = "ACTIVE" if active else "REVOKED"
39
+ if status not in VALID_STATUSES:
40
+ status = "ACTIVE" if active else "REVOKED"
41
+ if status != "ACTIVE":
42
+ active = False
43
+ created = str(data.get("created") or data.get("created_date") or "").strip()
44
+ last_activation = data.get("last_activation")
45
+ if last_activation is not None and last_activation != "":
46
+ last_activation = str(last_activation).strip()
47
+ else:
48
+ last_activation = None
49
+ return {
50
+ "key": key_text,
51
+ "name": str(data.get("name") or data.get("owner") or "").strip(),
52
+ "notes": str(data.get("notes") or "").strip(),
53
+ "created": created,
54
+ "last_activation": last_activation,
55
+ "active": active,
56
+ "status": status,
57
+ }
58
+
59
+
60
+ def migrate_keys_payload(payload: Any) -> dict[str, dict[str, Any]]:
61
+ if payload is None:
62
+ return {}
63
+ if isinstance(payload, list):
64
+ migrated: dict[str, dict[str, Any]] = {}
65
+ for item in payload:
66
+ if not isinstance(item, dict):
67
+ continue
68
+ key = str(item.get("key") or "").strip()
69
+ if not key:
70
+ continue
71
+ migrated[key] = normalize_record(key, item)
72
+ return migrated
73
+ if not isinstance(payload, dict):
74
+ return {}
75
+
76
+ if "keys" in payload and isinstance(payload.get("keys"), list):
77
+ return migrate_keys_payload(payload["keys"])
78
+
79
+ migrated: dict[str, dict[str, Any]] = {}
80
+ for raw_key, raw_record in payload.items():
81
+ key = str(raw_key or "").strip()
82
+ if not key:
83
+ continue
84
+ if isinstance(raw_record, str):
85
+ migrated[key] = normalize_record(
86
+ key,
87
+ {"key": key, "active": True, "status": "ACTIVE", "notes": raw_record},
88
+ )
89
+ continue
90
+ if not isinstance(raw_record, dict):
91
+ continue
92
+ migrated[key] = normalize_record(key, raw_record)
93
+ return migrated
94
+
95
+
96
+ def validation_result(record: dict[str, Any] | None) -> dict[str, Any]:
97
+ if not record:
98
+ return {"valid": False, "active": False, "status": "INVALID"}
99
+ normalized = normalize_record(str(record.get("key") or ""), record)
100
+ active = bool(normalized.get("active"))
101
+ status = str(normalized.get("status") or "").strip().upper()
102
+ if status == "ACTIVE" and active:
103
+ return {"valid": True, "active": True, "status": "ACTIVE"}
104
+ if status == "REVOKED" or (not active and status == "ACTIVE"):
105
+ return {"valid": True, "active": False, "status": "REVOKED"}
106
+ if status in {"EXPIRED", "BANNED"}:
107
+ return {"valid": True, "active": False, "status": status}
108
+ return {"valid": True, "active": active, "status": status}
109
+
110
+
111
+ def new_record(name: str, notes: str = "") -> dict[str, Any]:
112
+ return {
113
+ "name": str(name or "").strip(),
114
+ "notes": str(notes or "").strip(),
115
+ "created": today_iso(),
116
+ "last_activation": None,
117
+ "active": True,
118
+ "status": "ACTIVE",
119
+ }
beta_keys_store.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VTX beta keys — private Hugging Face dataset storage with local cache.
3
+
4
+ Environment:
5
+ VTX_KEYS_SOURCE=local|dataset (default: dataset)
6
+ VTX_KEYS_PATH=path/to/keys.json (local mode / cache file override)
7
+ VTX_BETA_KEYS_DATASET=org/repo (default: ManChildTechnologies/VTX-BetaKeys)
8
+ VTX_BETA_KEYS_FILENAME=keys.json
9
+ VTX_BETA_KEYS_REVISION=main
10
+ VTX_KEYS_REFRESH_SECONDS=30
11
+ HF_TOKEN / HUGGING_FACE_HUB_TOKEN
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import io
16
+ import json
17
+ import os
18
+ import threading
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from beta_keys_schema import migrate_keys_payload, normalize_record
24
+
25
+ APP_DIR = Path(__file__).resolve().parent
26
+ DEFAULT_KEYS_PATH = APP_DIR / "keys.json"
27
+ DEFAULT_CACHE_PATH = APP_DIR / "keys_cache.json"
28
+ DEFAULT_DATASET_REPO = "ManChildTechnologies/VTX-BetaKeys"
29
+ DEFAULT_DATASET_FILENAME = "keys.json"
30
+
31
+ _store_lock = threading.RLock()
32
+ _keys_cache: dict[str, Any] = {
33
+ "source": None,
34
+ "signature": None,
35
+ "fetched_at": 0.0,
36
+ "data": {},
37
+ "error": "",
38
+ "using_stale_cache": False,
39
+ }
40
+
41
+
42
+ def keys_source() -> str:
43
+ raw = str(os.environ.get("VTX_KEYS_SOURCE", "dataset") or "dataset").strip().lower()
44
+ return "local" if raw == "local" else "dataset"
45
+
46
+
47
+ def keys_path() -> Path:
48
+ custom = str(os.environ.get("VTX_KEYS_PATH", "") or "").strip()
49
+ if custom:
50
+ return Path(custom)
51
+ return DEFAULT_KEYS_PATH
52
+
53
+
54
+ def cache_path() -> Path:
55
+ custom = str(os.environ.get("VTX_KEYS_CACHE_PATH", "") or "").strip()
56
+ if custom:
57
+ return Path(custom)
58
+ return DEFAULT_CACHE_PATH
59
+
60
+
61
+ def dataset_repo() -> str:
62
+ return str(os.environ.get("VTX_BETA_KEYS_DATASET", DEFAULT_DATASET_REPO) or DEFAULT_DATASET_REPO).strip()
63
+
64
+
65
+ def dataset_filename() -> str:
66
+ return str(os.environ.get("VTX_BETA_KEYS_FILENAME", DEFAULT_DATASET_FILENAME) or DEFAULT_DATASET_FILENAME).strip()
67
+
68
+
69
+ def dataset_revision() -> str:
70
+ return str(os.environ.get("VTX_BETA_KEYS_REVISION", "main") or "main").strip() or "main"
71
+
72
+
73
+ def refresh_seconds() -> int:
74
+ try:
75
+ return max(5, int(os.environ.get("VTX_KEYS_REFRESH_SECONDS", "30") or 30))
76
+ except Exception:
77
+ return 30
78
+
79
+
80
+ def hf_token() -> str:
81
+ for key in ("HF_TOKEN", "HUGGING_FACE_HUB_TOKEN", "HUGGINGFACE_HUB_TOKEN"):
82
+ value = str(os.environ.get(key, "") or "").strip()
83
+ if value:
84
+ return value
85
+ return ""
86
+
87
+
88
+ def _require_hf_token() -> str:
89
+ token = hf_token()
90
+ if not token:
91
+ raise RuntimeError(
92
+ "HF_TOKEN is required for VTX beta key dataset access. "
93
+ "Set HF_TOKEN as an environment variable or Hugging Face Space secret."
94
+ )
95
+ return token
96
+
97
+
98
+ def _get_hf_api():
99
+ try:
100
+ from huggingface_hub import HfApi
101
+ except ImportError as exc:
102
+ raise RuntimeError("huggingface_hub is required for dataset key storage.") from exc
103
+ return HfApi(token=_require_hf_token())
104
+
105
+
106
+ def _serialize_keys(keys: dict[str, dict[str, Any]]) -> str:
107
+ ordered = {
108
+ key: normalize_record(key, record)
109
+ for key, record in sorted(keys.items(), key=lambda item: item[0])
110
+ }
111
+ return json.dumps(ordered, indent=2) + "\n"
112
+
113
+
114
+ def _read_json_file(path: Path) -> dict[str, dict[str, Any]]:
115
+ if not path.is_file():
116
+ return {}
117
+ try:
118
+ payload = json.loads(path.read_text(encoding="utf-8"))
119
+ except Exception:
120
+ return {}
121
+ return migrate_keys_payload(payload)
122
+
123
+
124
+ def _write_json_file(path: Path, keys: dict[str, dict[str, Any]]) -> None:
125
+ path.parent.mkdir(parents=True, exist_ok=True)
126
+ path.write_text(_serialize_keys(keys), encoding="utf-8")
127
+
128
+
129
+ def _local_signature(path: Path) -> str | None:
130
+ try:
131
+ if not path.is_file():
132
+ return None
133
+ stat = path.stat()
134
+ return f"local:{path}:{stat.st_mtime_ns}:{stat.st_size}"
135
+ except OSError:
136
+ return None
137
+
138
+
139
+ def _fetch_dataset_keys() -> tuple[dict[str, dict[str, Any]], str]:
140
+ token = _require_hf_token()
141
+ try:
142
+ from huggingface_hub import hf_hub_download
143
+ except ImportError as exc:
144
+ raise RuntimeError("huggingface_hub is required for dataset key loading.") from exc
145
+
146
+ repo_id = dataset_repo()
147
+ filename = dataset_filename()
148
+ revision = dataset_revision()
149
+ cached_path = hf_hub_download(
150
+ repo_id=repo_id,
151
+ repo_type="dataset",
152
+ filename=filename,
153
+ revision=revision,
154
+ token=token,
155
+ force_download=True,
156
+ )
157
+ path = Path(cached_path)
158
+ data = _read_json_file(path)
159
+ signature = f"dataset:{repo_id}:{revision}:{filename}:{path.stat().st_mtime_ns}:{path.stat().st_size}"
160
+ _write_json_file(cache_path(), data)
161
+ return data, signature
162
+
163
+
164
+ def push_keys_to_dataset(keys: dict[str, dict[str, Any]], commit_message: str) -> None:
165
+ token = _require_hf_token()
166
+ api = _get_hf_api()
167
+ payload = _serialize_keys(keys).encode("utf-8")
168
+ api.upload_file(
169
+ path_or_fileobj=io.BytesIO(payload),
170
+ path_in_repo=dataset_filename(),
171
+ repo_id=dataset_repo(),
172
+ repo_type="dataset",
173
+ revision=dataset_revision(),
174
+ commit_message=commit_message,
175
+ token=token,
176
+ )
177
+ _write_json_file(cache_path(), keys)
178
+ global _keys_cache
179
+ _keys_cache = {
180
+ "source": "dataset",
181
+ "signature": f"pushed:{time.time_ns()}",
182
+ "fetched_at": time.time(),
183
+ "data": dict(keys),
184
+ "error": "",
185
+ "using_stale_cache": False,
186
+ }
187
+
188
+
189
+ def load_keys(force: bool = False) -> dict[str, dict[str, Any]]:
190
+ global _keys_cache
191
+
192
+ with _store_lock:
193
+ source = keys_source()
194
+ now = time.time()
195
+ ttl = refresh_seconds()
196
+
197
+ if source == "local":
198
+ path = keys_path()
199
+ signature = _local_signature(path)
200
+ if (
201
+ not force
202
+ and _keys_cache.get("source") == "local"
203
+ and signature == _keys_cache.get("signature")
204
+ and (now - float(_keys_cache.get("fetched_at") or 0.0)) < ttl
205
+ ):
206
+ return dict(_keys_cache.get("data") or {})
207
+
208
+ data = _read_json_file(path)
209
+ _keys_cache = {
210
+ "source": "local",
211
+ "signature": signature,
212
+ "fetched_at": now,
213
+ "data": data,
214
+ "error": "",
215
+ "using_stale_cache": False,
216
+ }
217
+ return dict(data)
218
+
219
+ if (
220
+ not force
221
+ and _keys_cache.get("source") == "dataset"
222
+ and (now - float(_keys_cache.get("fetched_at") or 0.0)) < ttl
223
+ and _keys_cache.get("data") is not None
224
+ ):
225
+ return dict(_keys_cache.get("data") or {})
226
+
227
+ try:
228
+ data, signature = _fetch_dataset_keys()
229
+ _keys_cache = {
230
+ "source": "dataset",
231
+ "signature": signature,
232
+ "fetched_at": now,
233
+ "data": data,
234
+ "error": "",
235
+ "using_stale_cache": False,
236
+ }
237
+ return dict(data)
238
+ except Exception as exc:
239
+ cached_file = _read_json_file(cache_path())
240
+ if cached_file:
241
+ _keys_cache = {
242
+ "source": "dataset",
243
+ "signature": _keys_cache.get("signature"),
244
+ "fetched_at": now,
245
+ "data": cached_file,
246
+ "error": str(exc),
247
+ "using_stale_cache": True,
248
+ }
249
+ return dict(cached_file)
250
+ _keys_cache = {
251
+ "source": "dataset",
252
+ "signature": None,
253
+ "fetched_at": now,
254
+ "data": {},
255
+ "error": str(exc),
256
+ "using_stale_cache": False,
257
+ }
258
+ raise
259
+
260
+
261
+ def save_keys(keys: dict[str, dict[str, Any]], commit_message: str = "Update VTX beta keys") -> None:
262
+ normalized = {
263
+ key: normalize_record(key, record)
264
+ for key, record in (keys or {}).items()
265
+ if str(key or "").strip()
266
+ }
267
+ with _store_lock:
268
+ if keys_source() == "local":
269
+ _write_json_file(keys_path(), normalized)
270
+ global _keys_cache
271
+ _keys_cache = {
272
+ "source": "local",
273
+ "signature": _local_signature(keys_path()),
274
+ "fetched_at": time.time(),
275
+ "data": dict(normalized),
276
+ "error": "",
277
+ "using_stale_cache": False,
278
+ }
279
+ return
280
+ push_keys_to_dataset(normalized, commit_message)
281
+
282
+
283
+ def update_key_record(
284
+ beta_key: str,
285
+ updater,
286
+ commit_message: str,
287
+ ) -> dict[str, dict[str, Any]]:
288
+ key = str(beta_key or "").strip()
289
+ if not key:
290
+ raise ValueError("Beta key is required.")
291
+
292
+ with _store_lock:
293
+ keys = load_keys(force=True)
294
+ record = dict(keys.get(key) or normalize_record(key, {"key": key}))
295
+ record = normalize_record(key, updater(record))
296
+ record["key"] = key
297
+ keys[key] = record
298
+ save_keys(keys, commit_message=commit_message)
299
+ return record
300
+
301
+
302
+ def keys_status() -> dict[str, Any]:
303
+ source = keys_source()
304
+ status = {
305
+ "source": source,
306
+ "keys_loaded": len(_keys_cache.get("data") or {}),
307
+ "last_error": str(_keys_cache.get("error") or ""),
308
+ "cache_age_seconds": max(0, int(time.time() - float(_keys_cache.get("fetched_at") or 0.0))),
309
+ "using_stale_cache": bool(_keys_cache.get("using_stale_cache")),
310
+ }
311
+ if source == "local":
312
+ path = keys_path()
313
+ status["keys_path"] = str(path)
314
+ status["keys_exists"] = path.is_file()
315
+ else:
316
+ status["dataset_repo"] = dataset_repo()
317
+ status["dataset_filename"] = dataset_filename()
318
+ status["dataset_revision"] = dataset_revision()
319
+ status["hf_token_configured"] = bool(hf_token())
320
+ status["cache_path"] = str(cache_path())
321
+ return status
generate_beta_key.py CHANGED
@@ -1,116 +1,113 @@
1
  """
2
  VTX Studio beta key administration utility.
3
 
4
- Manage keys.json locally, then redeploy the Hugging Face Space to publish changes.
5
- No accounts, billing, subscriptions, machine locking, or activation limits.
6
  """
7
  from __future__ import annotations
8
 
9
  import argparse
10
  import json
11
- import secrets
12
- import string
13
  from pathlib import Path
14
  from typing import Any
15
 
 
 
 
16
  APP_DIR = Path(__file__).resolve().parent
17
  DEFAULT_KEYS_PATH = APP_DIR / "keys.json"
18
- KEY_PREFIX = "VTX-BETA"
19
- KEY_CHARS = string.ascii_uppercase + string.digits
20
-
21
-
22
- def keys_path(path: str | Path | None = None) -> Path:
23
- if path:
24
- return Path(path)
25
- env = __import__("os").environ.get("VTX_KEYS_PATH", "").strip()
26
- if env:
27
- return Path(env)
28
- return DEFAULT_KEYS_PATH
29
-
30
-
31
- def load_keys(path: str | Path | None = None) -> dict[str, dict[str, Any]]:
32
- file_path = keys_path(path)
33
- if not file_path.is_file():
34
- return {}
35
- data = json.loads(file_path.read_text(encoding="utf-8"))
36
- if not isinstance(data, dict):
37
- raise ValueError(f"keys file must be a JSON object: {file_path}")
38
- return data
39
 
40
 
41
- def save_keys(keys: dict[str, dict[str, Any]], path: str | Path | None = None) -> Path:
42
- file_path = keys_path(path)
43
- file_path.parent.mkdir(parents=True, exist_ok=True)
44
- file_path.write_text(json.dumps(keys, indent=2) + "\n", encoding="utf-8")
45
- return file_path
46
-
47
-
48
- def _random_segment(length: int = 4) -> str:
49
- return "".join(secrets.choice(KEY_CHARS) for _ in range(length))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
 
52
- def _new_key(existing: dict[str, Any]) -> str:
53
- for _ in range(1000):
54
- candidate = f"{KEY_PREFIX}-{_random_segment()}-{_random_segment()}"
55
- if candidate not in existing:
56
- return candidate
57
- raise RuntimeError("Unable to generate a unique beta key.")
58
 
 
 
 
59
 
60
- def generate_key(path: str | Path | None = None) -> str:
61
- """Generate a new ACTIVE beta key and persist it to keys.json."""
62
- keys = load_keys(path)
63
- beta_key = _new_key(keys)
64
- keys[beta_key] = {"active": True, "status": "ACTIVE"}
65
- save_keys(keys, path)
66
- return beta_key
67
 
68
 
69
- def revoke_key(beta_key: str, path: str | Path | None = None) -> dict[str, Any]:
70
- """Revoke an existing beta key."""
71
  key = str(beta_key or "").strip()
72
  if not key:
73
  raise ValueError("Beta key is required.")
74
 
75
- keys = load_keys(path)
76
  if key not in keys:
77
  raise KeyError(f"Beta key not found: {key}")
78
 
79
- keys[key] = {"active": False, "status": "REVOKED"}
80
- save_keys(keys, path)
81
- return keys[key]
 
 
 
 
82
 
83
 
84
- def reactivate_key(beta_key: str, path: str | Path | None = None) -> dict[str, Any]:
85
- """Reactivate a previously revoked beta key."""
86
  key = str(beta_key or "").strip()
87
  if not key:
88
  raise ValueError("Beta key is required.")
89
 
90
- keys = load_keys(path)
91
  if key not in keys:
92
  raise KeyError(f"Beta key not found: {key}")
 
 
 
93
 
94
- keys[key] = {"active": True, "status": "ACTIVE"}
95
- save_keys(keys, path)
96
- return keys[key]
97
 
98
-
99
- def export_json(path: str | Path | None = None, output: str | Path | None = None) -> Path:
100
- """Export keys.json to stdout path (defaults to keys.json itself)."""
101
- keys = load_keys(path)
102
- out = Path(output) if output else keys_path(path)
103
  out.parent.mkdir(parents=True, exist_ok=True)
104
- out.write_text(json.dumps(keys, indent=2) + "\n", encoding="utf-8")
 
105
  return out
106
 
107
 
108
  def main() -> int:
109
  parser = argparse.ArgumentParser(description="VTX Studio beta key manager")
110
- parser.add_argument("--keys", default=str(DEFAULT_KEYS_PATH), help="Path to keys.json")
111
  sub = parser.add_subparsers(dest="command", required=True)
112
 
113
- sub.add_parser("generate", help="Generate a new ACTIVE beta key")
 
 
114
 
115
  revoke = sub.add_parser("revoke", help="Revoke an existing beta key")
116
  revoke.add_argument("beta_key", help="Beta key to revoke")
@@ -118,34 +115,36 @@ def main() -> int:
118
  reactivate = sub.add_parser("reactivate", help="Reactivate a revoked beta key")
119
  reactivate.add_argument("beta_key", help="Beta key to reactivate")
120
 
121
- export = sub.add_parser("export", help="Export keys.json")
122
- export.add_argument(
123
- "-o",
124
- "--output",
125
- default="",
126
- help="Optional output path (defaults to keys file)",
127
- )
128
 
129
  args = parser.parse_args()
130
- keys_file = args.keys
131
 
132
  if args.command == "generate":
133
- beta_key = generate_key(keys_file)
134
  print(beta_key)
135
  return 0
136
 
137
  if args.command == "revoke":
138
- record = revoke_key(args.beta_key, keys_file)
139
- print(json.dumps({args.beta_key: record}, indent=2))
140
  return 0
141
 
142
  if args.command == "reactivate":
143
- record = reactivate_key(args.beta_key, keys_file)
144
- print(json.dumps({args.beta_key: record}, indent=2))
 
 
 
 
 
145
  return 0
146
 
147
  if args.command == "export":
148
- out = export_json(keys_file, args.output or None)
149
  print(str(out))
150
  return 0
151
 
 
1
  """
2
  VTX Studio beta key administration utility.
3
 
4
+ Keys are stored in the private ManChildTechnologies/VTX-BetaKeys dataset.
 
5
  """
6
  from __future__ import annotations
7
 
8
  import argparse
9
  import json
 
 
10
  from pathlib import Path
11
  from typing import Any
12
 
13
+ from beta_keys_schema import generate_unique_key, new_record, normalize_record
14
+ from beta_keys_store import keys_path, load_keys, save_keys
15
+
16
  APP_DIR = Path(__file__).resolve().parent
17
  DEFAULT_KEYS_PATH = APP_DIR / "keys.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
 
20
+ def generate_key(
21
+ *,
22
+ name: str,
23
+ notes: str = "",
24
+ path: str | Path | None = None,
25
+ push: bool = True,
26
+ ) -> str:
27
+ """Generate a new ACTIVE beta key and persist it to the dataset."""
28
+ if not str(name or "").strip():
29
+ raise ValueError("Tester name is required.")
30
+
31
+ keys = load_keys(force=True)
32
+ beta_key = generate_unique_key(keys)
33
+ record = new_record(name=name, notes=notes)
34
+ record["key"] = beta_key
35
+ keys[beta_key] = normalize_record(beta_key, record)
36
+ if push:
37
+ save_keys(keys, commit_message=f"Generate beta key for {name.strip()}")
38
+ elif path:
39
+ from beta_keys_store import _write_json_file
40
+
41
+ _write_json_file(Path(path), keys)
42
+ return beta_key
43
 
44
 
45
+ def revoke_key(beta_key: str, path: str | Path | None = None, push: bool = True) -> dict[str, Any]:
46
+ key = str(beta_key or "").strip()
47
+ if not key:
48
+ raise ValueError("Beta key is required.")
 
 
49
 
50
+ keys = load_keys(force=True)
51
+ if key not in keys:
52
+ raise KeyError(f"Beta key not found: {key}")
53
 
54
+ record = normalize_record(key, keys[key])
55
+ record["active"] = False
56
+ record["status"] = "REVOKED"
57
+ keys[key] = record
58
+ if push:
59
+ save_keys(keys, commit_message=f"Revoke beta key {key}")
60
+ return record
61
 
62
 
63
+ def reactivate_key(beta_key: str, path: str | Path | None = None, push: bool = True) -> dict[str, Any]:
 
64
  key = str(beta_key or "").strip()
65
  if not key:
66
  raise ValueError("Beta key is required.")
67
 
68
+ keys = load_keys(force=True)
69
  if key not in keys:
70
  raise KeyError(f"Beta key not found: {key}")
71
 
72
+ record = normalize_record(key, keys[key])
73
+ record["active"] = True
74
+ record["status"] = "ACTIVE"
75
+ keys[key] = record
76
+ if push:
77
+ save_keys(keys, commit_message=f"Reactivate beta key {key}")
78
+ return record
79
 
80
 
81
+ def delete_key(beta_key: str, push: bool = True) -> None:
 
82
  key = str(beta_key or "").strip()
83
  if not key:
84
  raise ValueError("Beta key is required.")
85
 
86
+ keys = load_keys(force=True)
87
  if key not in keys:
88
  raise KeyError(f"Beta key not found: {key}")
89
+ del keys[key]
90
+ if push:
91
+ save_keys(keys, commit_message=f"Delete beta key {key}")
92
 
 
 
 
93
 
94
+ def export_json(output: str | Path | None = None) -> Path:
95
+ keys = load_keys(force=True)
96
+ out = Path(output) if output else keys_path()
 
 
97
  out.parent.mkdir(parents=True, exist_ok=True)
98
+ ordered = {key: normalize_record(key, record) for key, record in sorted(keys.items())}
99
+ out.write_text(json.dumps(ordered, indent=2) + "\n", encoding="utf-8")
100
  return out
101
 
102
 
103
  def main() -> int:
104
  parser = argparse.ArgumentParser(description="VTX Studio beta key manager")
105
+ parser.add_argument("--keys", default="", help="Optional local export path")
106
  sub = parser.add_subparsers(dest="command", required=True)
107
 
108
+ generate = sub.add_parser("generate", help="Generate a new ACTIVE beta key")
109
+ generate.add_argument("--name", required=True, help="Tester name")
110
+ generate.add_argument("--notes", default="", help="Optional notes")
111
 
112
  revoke = sub.add_parser("revoke", help="Revoke an existing beta key")
113
  revoke.add_argument("beta_key", help="Beta key to revoke")
 
115
  reactivate = sub.add_parser("reactivate", help="Reactivate a revoked beta key")
116
  reactivate.add_argument("beta_key", help="Beta key to reactivate")
117
 
118
+ delete = sub.add_parser("delete", help="Delete an existing beta key")
119
+ delete.add_argument("beta_key", help="Beta key to delete")
120
+
121
+ export = sub.add_parser("export", help="Export keys.json snapshot")
122
+ export.add_argument("-o", "--output", default="", help="Optional output path")
 
 
123
 
124
  args = parser.parse_args()
 
125
 
126
  if args.command == "generate":
127
+ beta_key = generate_key(name=args.name, notes=args.notes)
128
  print(beta_key)
129
  return 0
130
 
131
  if args.command == "revoke":
132
+ record = revoke_key(args.beta_key)
133
+ print(json.dumps(record, indent=2))
134
  return 0
135
 
136
  if args.command == "reactivate":
137
+ record = reactivate_key(args.beta_key)
138
+ print(json.dumps(record, indent=2))
139
+ return 0
140
+
141
+ if args.command == "delete":
142
+ delete_key(args.beta_key)
143
+ print("deleted")
144
  return 0
145
 
146
  if args.command == "export":
147
+ out = export_json(args.output or None)
148
  print(str(out))
149
  return 0
150
 
requirements.txt CHANGED
@@ -1,2 +1,3 @@
1
  fastapi>=0.110.0
2
- uvicorn>=0.27.0
 
 
1
  fastapi>=0.110.0
2
+ uvicorn>=0.27.0
3
+ huggingface_hub>=0.23.0
tools/migrate_keys_to_dataset.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """One-time migration: local keys.json -> private VTX-BetaKeys dataset."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
9
+
10
+ from beta_keys_schema import migrate_keys_payload
11
+ from beta_keys_store import cache_path, dataset_repo, hf_token, push_keys_to_dataset
12
+
13
+
14
+ def main() -> int:
15
+ parser = argparse.ArgumentParser(description="Migrate local keys.json to the private HF dataset")
16
+ parser.add_argument(
17
+ "--input",
18
+ default=str(Path(__file__).resolve().parents[1] / "keys.json"),
19
+ help="Path to legacy keys.json",
20
+ )
21
+ parser.add_argument(
22
+ "--dry-run",
23
+ action="store_true",
24
+ help="Print migrated key count without uploading",
25
+ )
26
+ args = parser.parse_args()
27
+
28
+ if not hf_token():
29
+ print("HF_TOKEN is required.", file=sys.stderr)
30
+ return 1
31
+
32
+ source = Path(args.input)
33
+ if not source.is_file():
34
+ print(f"Missing input file: {source}", file=sys.stderr)
35
+ return 1
36
+
37
+ import json
38
+
39
+ payload = json.loads(source.read_text(encoding="utf-8"))
40
+ migrated = migrate_keys_payload(payload)
41
+ print(f"Migrated {len(migrated)} keys from {source}")
42
+ print(f"Target dataset: {dataset_repo()}")
43
+
44
+ if args.dry_run:
45
+ for key, record in sorted(migrated.items()):
46
+ print(f" {key}: {record.get('status')} name={record.get('name')!r}")
47
+ return 0
48
+
49
+ push_keys_to_dataset(migrated, commit_message="Migrate legacy keys.json to private dataset")
50
+ print(f"Uploaded keys.json to {dataset_repo()}")
51
+ print(f"Local cache written to {cache_path()}")
52
+ return 0
53
+
54
+
55
+ if __name__ == "__main__":
56
+ raise SystemExit(main())
tools/test_gui_import.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
5
+
6
+ from beta_keys_schema import normalize_record
7
+ import vtx_beta_key_manager as gui
8
+
9
+ record = normalize_record("VTX-TEST01", {"active": True, "status": "ACTIVE"})
10
+ assert record["status"] == "ACTIVE"
11
+ assert gui.APP_NAME == "VTX Beta Key Manager"
12
+ assert gui.APP_VERSION == "1.1.0"
13
+ print("OK")
tools/test_openapi.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Verify OpenAPI documents ValidateRequest for Swagger."""
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
6
+
7
+ from app import app
8
+
9
+ schema = app.openapi()
10
+ request_schema = (
11
+ schema["paths"]["/api/validate"]["post"]
12
+ .get("requestBody", {})
13
+ .get("content", {})
14
+ .get("application/json", {})
15
+ .get("schema", {})
16
+ )
17
+
18
+ ref = request_schema.get("$ref", "")
19
+ assert ref.endswith("/ValidateRequest"), ref
20
+
21
+ model = schema["components"]["schemas"]["ValidateRequest"]
22
+ props = model.get("properties", {})
23
+ assert "beta_key" in props, props
24
+
25
+ examples = props["beta_key"].get("examples") or []
26
+ if not examples:
27
+ example = props["beta_key"].get("example")
28
+ assert example == "VTX-BETA-XXXX-XXXX", props["beta_key"]
29
+ else:
30
+ assert "VTX-BETA-XXXX-XXXX" in examples, examples
31
+
32
+ print("OK")
tools/test_schema.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Schema migration and validation tests."""
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
6
+
7
+ from beta_keys_schema import migrate_keys_payload, normalize_record, validation_result
8
+
9
+ legacy = {
10
+ "VTX-BETA-7K4P-X92M": {"active": True, "status": "ACTIVE"},
11
+ "VTX-BETA-R8JL-Q4WT": {"active": False, "status": "REVOKED"},
12
+ }
13
+ migrated = migrate_keys_payload(legacy)
14
+ assert "VTX-BETA-7K4P-X92M" in migrated
15
+ record = migrated["VTX-BETA-7K4P-X92M"]
16
+ assert record["key"] == "VTX-BETA-7K4P-X92M"
17
+ assert record["name"] == ""
18
+ assert record["notes"] == ""
19
+ assert record["created"] == ""
20
+ assert record["last_activation"] is None
21
+ assert record["active"] is True
22
+ assert validation_result(record)["status"] == "ACTIVE"
23
+ assert validation_result(migrated["VTX-BETA-R8JL-Q4WT"])["status"] == "REVOKED"
24
+ assert validation_result(None)["status"] == "INVALID"
25
+
26
+ modern = normalize_record(
27
+ "VTX-4D8A9B",
28
+ {
29
+ "key": "VTX-4D8A9B",
30
+ "name": "Ramon",
31
+ "notes": "Beta Tester",
32
+ "created": "2026-06-12",
33
+ "last_activation": None,
34
+ "active": True,
35
+ "status": "ACTIVE",
36
+ },
37
+ )
38
+ assert modern["name"] == "Ramon"
39
+ assert modern["notes"] == "Beta Tester"
40
+
41
+ print("OK")
tools/test_validate.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Smoke test for validate_beta_key logic (local mode, no HF required)."""
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
7
+
8
+ os.environ["VTX_KEYS_SOURCE"] = "local"
9
+ os.environ["VTX_KEYS_PATH"] = str(Path(__file__).resolve().parents[1] / "keys.json")
10
+
11
+ from app import validate_beta_key
12
+
13
+ active = validate_beta_key("VTX-BETA-7K4P-X92M", track_activation=False)
14
+ revoked = validate_beta_key("VTX-BETA-R8JL-Q4WT", track_activation=False)
15
+ unknown = validate_beta_key("VTX-BETA-NOT-REAL", track_activation=False)
16
+ empty = validate_beta_key("", track_activation=False)
17
+
18
+ assert active == {"valid": True, "active": True, "status": "ACTIVE"}, active
19
+ assert revoked == {"valid": True, "active": False, "status": "REVOKED"}, revoked
20
+ assert unknown == {"valid": False, "active": False, "status": "INVALID"}, unknown
21
+ assert empty == {"valid": False, "active": False, "status": "INVALID"}, empty
22
+
23
+ print("OK")