From f6f1d25e250f4341896e68a7d941845f37295139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20H=C3=B8iby?= Date: Wed, 29 Jul 2026 13:09:16 +0200 Subject: [PATCH] feat: add SourceHandle TypedDict for foreign-namespace user numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit peck-overlay-schema PR #66 added source_handle to /v1/feed and /v1/thread rows for app == 'zanaadu' — a source-scoped alias read from Zanaadu's on-chain user-number registry, additive to (never a replacement for) author. PeckRow stays an open dict by design, so this adds a typed SourceHandle (mirrors the TypeScript SDK's interface one-to-one) that may appear at row["source_handle"]: mandatory namespace, optional numbers[] for multi-number owners, and membership_proof: "none" spelling out that the value is read from the registry, not Merkle-verified. Bumps to 0.3.0 (additive, non-breaking). Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 13 ++++++++++ overlay_social/__init__.py | 4 ++- overlay_social/models.py | 50 ++++++++++++++++++++++++++++++++++++-- pyproject.toml | 2 +- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9c4d5..5ff0a88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to `overlay-social` (Python). Mirrors `@overlay-social/sdk` (TypeScript) one-to-one. +## 0.3.0 — 2026-07-29 + +### Added +- `SourceHandle` (TypedDict) — a source-scoped alias for a `PeckRow`'s + `author`, present at `row["source_handle"]` on `app == "zanaadu"` rows + that own an on-chain "user number" (`{namespace: "zanaadu", value: "@14", + number: 14, kind: "user_number", membership_proof: "none"}`, plus + `numbers` when a key owns more than one). Additive: comes alongside + `author`, never replaces it, and is absent (not `None`) when the author + has no number. `membership_proof: "none"` is explicit that the value is + read from Zanaadu's registry, not verified against their Merkle tree. + See `peck-overlay-schema/ZANAADU_POSTANCHOR_FORMAT.md` §13. + ## 0.2.0 — 2026-06-12 ### Added diff --git a/overlay_social/__init__.py b/overlay_social/__init__.py index 74b016e..9985ec4 100644 --- a/overlay_social/__init__.py +++ b/overlay_social/__init__.py @@ -39,12 +39,13 @@ ProfileRow, ProfileState, ResolvedIdentity, + SourceHandle, ThreadResponse, TopicAnchor, TopicState, ) -__version__ = "0.2.0" +__version__ = "0.3.0" __all__ = [ "__version__", @@ -69,4 +70,5 @@ "FeedResponse", "ThreadResponse", "PeckRow", + "SourceHandle", ] diff --git a/overlay_social/models.py b/overlay_social/models.py index aff920b..716134e 100644 --- a/overlay_social/models.py +++ b/overlay_social/models.py @@ -5,19 +5,65 @@ (``displayName``, ``avatarRef``, ``stateRoot``) and snake_case (``minted_at``); the ``from_dict`` helpers preserve that verbatim rather than silently normalizing a real contract difference. Open-ended rows (``PeckRow``) stay -plain dicts because the indexer rides extra MAP keys. +plain dicts because the indexer rides extra MAP keys — the one typed +exception is ``SourceHandle``, documented separately below because it is a +nested value worth a real shape, not a top-level response. """ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any +from typing import Any, Literal, TypedDict # A peck row (/v1/feed, /v1/post/:txid) is intentionally an open dict — the # indexer carries arbitrary extra MAP keys, so we do not lock its shape. PeckRow = dict[str, Any] +class _SourceHandleRequired(TypedDict): + namespace: Literal["zanaadu"] + value: str + number: int + kind: Literal["user_number"] + membership_proof: Literal["none"] + + +class SourceHandle(_SourceHandleRequired, total=False): + """Feed shape of one foreign-namespace alias (mirrors `SourceHandle` in + ``@overlay-social/sdk`` one-to-one). + + May appear at ``PeckRow["source_handle"]``. Today the only source is + Zanaadu: it sells on-chain "user numbers" (an on-chain collectibles + registry), and rows with ``app == "zanaadu"`` carry the holder's number + here. See ``peck-overlay-schema/ZANAADU_POSTANCHOR_FORMAT.md`` §13 for + the full derivation and its proof gap. + + ``namespace`` is mandatory and comes first so the value can never be + shown bare and mistaken for a peck handle — render it qualified, e.g. + ``@14 · zanaadu``. + + Rules (normative): + + - Comes IN ADDITION TO ``author`` (the key). Never replaces it. + - Omitted from the row entirely (never ``None``, never ``""``) when the + author has no alias at the source — absence is a valid, measured + state there, not a gap to fill with a guess. + - ``membership_proof: "none"`` means the value was READ from the + source's own state (here: a registry counter, cross-checked 7/7 + against Zanaadu's own UI) but the source's stronger commitment + (their sparse Merkle tree) is NOT verified by us — the root is + legible, not reproducible. It is not a claim that the number itself + is unreliable. + - ``numbers`` (all numbers this key owns at the source, ascending) is + only present when there is more than one; ``value``/``number`` are + then the lowest. Tie-break for "which number is primary" when a key + owns several is NOT proven on-chain (§13.7) — lowest was chosen for + stability over time, not because it is confirmed canonical. + """ + + numbers: list[int] + + @dataclass(frozen=True) class ResolvedIdentity: """One entry from POST /v1/identities/resolve, keyed by the exact input.""" diff --git a/pyproject.toml b/pyproject.toml index dd5e23e..22242ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "overlay-social" -version = "0.2.0" +version = "0.3.0" description = "Read-only Python client for overlay.peck.to — identity resolution, profiles, feed and overlay state over the canonical BSV/BRC-100 social overlay." readme = "README.md" requires-python = ">=3.10"