from __future__ import annotations import argparse import json import secrets from pathlib import Path from relay_gateway.keys import hash_token, CHANNELS_FOR_LEVEL def _read(path: Path) -> dict: try: return json.loads(Path(path).read_text(encoding="utf-8")) except (FileNotFoundError, json.JSONDecodeError): return {} def _write(path: Path, body: dict) -> None: Path(path).write_text(json.dumps(body, indent=2), encoding="utf-8") def add_key(path: Path, *, name: str, level: str) -> str: if level not in CHANNELS_FOR_LEVEL: raise ValueError(f"level must be one of {sorted(CHANNELS_FOR_LEVEL)}") token = secrets.token_urlsafe(32) body = _read(path) body[hash_token(token)] = {"name": name, "level": level} _write(path, body) return token def list_keys(path: Path) -> list[dict]: return [{"hash": h, **meta} for h, meta in _read(path).items()] def revoke(path: Path, name: str) -> int: body = _read(path) to_remove = [h for h, meta in body.items() if meta.get("name") == name] for h in to_remove: del body[h] _write(path, body) return len(to_remove) def main() -> None: ap = argparse.ArgumentParser(description="Manage relay gateway keys") ap.add_argument("--file", required=True, type=Path) sub = ap.add_subparsers(dest="cmd", required=True) a = sub.add_parser("add"); a.add_argument("--name", required=True); a.add_argument("--level", required=True) sub.add_parser("list") r = sub.add_parser("revoke"); r.add_argument("--name", required=True) args = ap.parse_args() if args.cmd == "add": token = add_key(args.file, name=args.name, level=args.level) print(f"Token for {args.name!r} (level={args.level}) — store it now, shown once:\n{token}") elif args.cmd == "list": for e in list_keys(args.file): print(f"{e['name']:20s} {e['level']:4s} {e['hash'][:12]}…") elif args.cmd == "revoke": print(f"Removed {revoke(args.file, args.name)} key(s) named {args.name!r}") if __name__ == "__main__": main()