"""KEZ identifiers: always ``system:identifier`` (Spec ยง3).""" from __future__ import annotations import re from . import bech32 _HEX64 = re.compile(r"^[0-9a-f]{64}$") class IdentityError(ValueError): pass class Identity: """A canonical ``system:identifier`` string.""" __slots__ = ("_raw",) def __init__(self, raw: str) -> None: self._raw = raw @classmethod def parse(cls, raw: str) -> "Identity": trimmed = raw.strip() if not trimmed: raise IdentityError("empty identity") # CLI ergonomics: a bare npub normalizes to nostr:npub... if trimmed.startswith("npub1"): _validate_npub(trimmed) return cls(f"nostr:{trimmed}") colon = trimmed.find(":") if colon <= 0 or colon == len(trimmed) - 1: raise IdentityError(f"invalid identity (need scheme:value): {raw!r}") scheme = trimmed[:colon] rest = trimmed[colon + 1 :] if scheme == "nostr": _validate_npub(rest) elif scheme == "ed25519": _validate_ed25519_hex(rest) return cls(f"{scheme}:{rest}") @property def scheme(self) -> str: return self._raw.split(":", 1)[0] @property def value(self) -> str: return self._raw.split(":", 1)[1] def __str__(self) -> str: return self._raw def __repr__(self) -> str: return f"Identity({self._raw!r})" def __eq__(self, other: object) -> bool: return isinstance(other, Identity) and other._raw == self._raw def __hash__(self) -> int: return hash(self._raw) def _validate_npub(value: str) -> None: if not value.startswith("npub1"): raise IdentityError(f"invalid nostr identifier (expected npub1...): {value!r}") raw = bech32.decode("npub", value) if len(raw) != 32: raise IdentityError("invalid npub: expected 32-byte key") def _validate_ed25519_hex(value: str) -> None: if not _HEX64.match(value): raise IdentityError("invalid ed25519 identifier: expected 64 lowercase hex chars")