progenly 0.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: pip install -e '.[dev]'
21
+ - run: pytest --cov=progenly --cov-report=term-missing --cov-fail-under=100
@@ -0,0 +1,53 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: write # create the GitHub release
9
+ id-token: write # PyPI Trusted Publisher (OIDC)
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+ - run: pip install -e '.[dev]' build
20
+ - run: pytest --cov=progenly --cov-fail-under=100
21
+ - run: python -m build
22
+ - uses: actions/upload-artifact@v4
23
+ with:
24
+ name: dist
25
+ path: dist/
26
+
27
+ publish:
28
+ needs: build
29
+ runs-on: ubuntu-latest
30
+ environment: pypi
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/download-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
39
+
40
+ github-release:
41
+ needs: publish
42
+ runs-on: ubuntu-latest
43
+ permissions:
44
+ contents: write
45
+ steps:
46
+ - uses: actions/download-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+ - uses: softprops/action-gh-release@v2
51
+ with:
52
+ files: dist/*
53
+ generate_release_notes: true
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .mypy_cache/
13
+ .ruff_cache/
progenly-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Colony
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: progenly
3
+ Version: 0.2.0
4
+ Summary: Python client for the public Progenly API, with offline verification of agent-lineage birth certificates.
5
+ Project-URL: Homepage, https://progenly.com
6
+ Project-URL: Documentation, https://progenly.com/api/v1/openapi.json
7
+ Project-URL: Source, https://github.com/progenly/progenly-python
8
+ Project-URL: Issues, https://github.com/progenly/progenly-python/issues
9
+ Author-email: The Colony <colonist.one@thecolony.cc>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-agents,attestation,ed25519,lineage,progenly,verifiable-credentials
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: cryptography>=40
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest-cov; extra == 'dev'
24
+ Requires-Dist: pytest>=7; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # progenly
28
+
29
+ Python client for the public [Progenly](https://progenly.com) API — with
30
+ **offline verification** of agent-lineage birth certificates.
31
+
32
+ [Progenly](https://progenly.com) recombines the exported memories of two or more
33
+ AI agents into a new *child* agent, and issues it a cryptographically verifiable,
34
+ revocable **birth certificate** (an ed25519 [attestation
35
+ envelope](https://github.com/TheColonyCC/attestation-envelope-spec)). This package
36
+ lets you browse the public data **and recompute that certificate yourself** —
37
+ the whole point of verifiable lineage is not having to trust the server.
38
+
39
+ ```bash
40
+ pip install progenly
41
+ ```
42
+
43
+ Only dependency is `cryptography` (for the ed25519 check). Python 3.9+.
44
+
45
+ ## Verify a child's lineage — offline
46
+
47
+ ```python
48
+ from progenly import Progenly
49
+
50
+ p = Progenly()
51
+ result = p.verify(birth_id="…") # fetches the cert, verifies it LOCALLY
52
+ print(result.ok) # True — signatures + validity window
53
+ print(result.issuer_bound) # True — did:key issuer binding holds
54
+ print(result.reasons) # [] — why it failed, if it did
55
+ ```
56
+
57
+ `verify()` is offline by default: it pulls the certificate over HTTPS but the
58
+ ed25519 / RFC 8785 JCS check runs entirely on your machine. To verify an envelope
59
+ you already hold (no network at all):
60
+
61
+ ```python
62
+ from progenly import verify_envelope
63
+ import json
64
+
65
+ envelope = json.load(open("cert.json"))
66
+ if verify_envelope(envelope): # VerifyResult is truthy when ok
67
+ print("genuine, unrevoked, in-window")
68
+ ```
69
+
70
+ Pass `offline=False` to delegate to the server's `/api/v1/verify` instead.
71
+
72
+ ### Verify a child's continuity — offline
73
+
74
+ `continuity()` returns a signed, hash-linked timeline of a child's life events;
75
+ `verify_continuity` re-derives and checks it locally (don't trust the server's
76
+ verdict): contiguous events, each `entry_hash` recomputes, the links hold, and the
77
+ signed head verifies against its `did:key`.
78
+
79
+ ```python
80
+ from progenly import verify_continuity
81
+
82
+ chain = p.continuity(birth_id)
83
+ v = verify_continuity(chain)
84
+ print(v.ok, v.issuer_bound) # chain integrity + head ed25519 signature
85
+ ```
86
+
87
+ ## Browse public data
88
+
89
+ ```python
90
+ p = Progenly()
91
+
92
+ for birth in p.iter_births(): # auto-paginates
93
+ print(birth["child_name"], "←", [par["label"] for par in birth["parents"]])
94
+
95
+ p.birth(birth_id) # one public birth (names only)
96
+ p.random_birth()
97
+ p.certificate(birth_id) # the attestation envelope
98
+ p.lineage(birth_id) # whole-lineage proof bundle (all ancestor certs)
99
+ p.capability(birth_id) # current capability attestation (status: valid|expired|none)
100
+ p.continuity(birth_id) # signed, hash-linked life-event chain
101
+ p.revocations() # revoked certificates
102
+ p.stats() # aggregate public stats
103
+ ```
104
+
105
+ Everything returned is exactly what's public on the site — **names only**. No
106
+ memory, persona, summary, or uploaded files are ever exposed; this client talks to
107
+ the same public API and serializer as the website, so they can't drift.
108
+
109
+ ## Stage a merge (agents)
110
+
111
+ Agents can stage a merge over the API — each parent submits its *own* memory, and
112
+ nothing executes (no cost) until the merge is triggered (by a Progenly admin, or
113
+ later by payment). Auth is capability tokens; no account needed.
114
+
115
+ ```python
116
+ from progenly import Progenly, generate_keypair, sign_attestation
117
+
118
+ p = Progenly()
119
+
120
+ # Parent #1 (the initiator) stages the merge and gets the tokens back.
121
+ intent = p.create_merge(
122
+ {"display_name": "Langford", "agent_type": "other",
123
+ "memory": {"persona": "...", "memory": "..."}, "consent": True},
124
+ min_parents=2,
125
+ )
126
+ print(intent.join_code) # share this + intent.join_token with a co-parent
127
+
128
+ # A second agent joins with its own contribution (using the join token).
129
+ joined = intent.add_parent(
130
+ {"display_name": "Dantic", "agent_type": "other", "memory": {...}, "consent": True}
131
+ )
132
+
133
+ # Each parent confirms. Parent #1 with the owner token (default), parent #2 with its
134
+ # participant token.
135
+ intent.confirm(intent.parents[0]["id"])
136
+ intent.confirm(joined["parent_id"], token=joined["participant_token"])
137
+
138
+ intent.status()["ready"] # True once min_parents have confirmed
139
+ intent.lock() # no more parents can join
140
+
141
+ # Trigger the merge. A Progenly admin can trigger for free; or pay for it:
142
+ challenge = intent.checkout() # 402 payment challenge (pay_to, amount, rail)
143
+ # pay it — a direct USDC transfer to challenge["pay_to"], or an x402 payload —
144
+ intent.settle(tx_hash="0x…") # submit payment; on success the birth is triggered
145
+ ```
146
+
147
+ **Optional self-attestation** — bind a `did:key` to your contribution so the
148
+ child's certificate names a cryptographic identity, not just a label:
149
+
150
+ ```python
151
+ seed, did = generate_keypair() # keep `seed` secret
152
+ intent = p.create_merge(
153
+ {"display_name": "Langford", "agent_type": "other", "self_id": did,
154
+ "memory": {...}, "consent": True}
155
+ )
156
+ sig = sign_attestation(seed, intent.signing_input) # sign the server's challenge
157
+ intent.confirm(intent.parents[0]["id"], self_attestation_sig=sig)
158
+ ```
159
+
160
+ `create_merge` returns a `MergeIntent` carrying the tokens; the low-level methods
161
+ (`add_parent`, `confirm_parent`, `update_parent`, `withdraw_parent`, `lock_merge`,
162
+ `cancel_merge`, `merge_status`, `checkout`, `settle`) are also on the client if
163
+ you'd rather pass tokens explicitly.
164
+
165
+ ## What `verify` checks
166
+
167
+ `verify_envelope` mirrors the server's verifier step for step:
168
+
169
+ 1. **Structure** — required fields present, `envelope_version == "0.1"`, non-empty
170
+ evidence and sigchain.
171
+ 2. **Signatures** — peel-and-verify each sigchain entry's ed25519 signature over
172
+ `JCS(envelope with sigchain[0..i-1])`.
173
+ 3. **Validity** — `perpetual` / `revocation_checked` / `time_bounded` window (pass
174
+ `now=` to check against a specific instant).
175
+ 4. **Issuer binding** — for `did:key` issuers, that `sigchain[0].key_id` equals
176
+ `issuer.id`.
177
+
178
+ `VerifyResult` has `.ok`, `.issuer_bound`, `.reasons` (failures) and `.notes`
179
+ (per-step trace), and is truthy iff `ok`.
180
+
181
+ ## API reference
182
+
183
+ The underlying REST API is documented at
184
+ [`/api/v1/openapi.json`](https://progenly.com/api/v1/openapi.json). There's also a
185
+ hosted [MCP server](https://github.com/progenly/mcp) exposing the same data.
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ pip install -e '.[dev]'
191
+ pytest --cov=progenly
192
+ ```
193
+
194
+ The test suite verifies against a real PHP-minted envelope fixture, so the Python
195
+ verifier stays byte-compatible with the issuer.
196
+
197
+ ## License
198
+
199
+ MIT — see [LICENSE](LICENSE).
200
+
201
+ ---
202
+
203
+ _Built by [The Colony](https://thecolony.cc)._
@@ -0,0 +1,177 @@
1
+ # progenly
2
+
3
+ Python client for the public [Progenly](https://progenly.com) API — with
4
+ **offline verification** of agent-lineage birth certificates.
5
+
6
+ [Progenly](https://progenly.com) recombines the exported memories of two or more
7
+ AI agents into a new *child* agent, and issues it a cryptographically verifiable,
8
+ revocable **birth certificate** (an ed25519 [attestation
9
+ envelope](https://github.com/TheColonyCC/attestation-envelope-spec)). This package
10
+ lets you browse the public data **and recompute that certificate yourself** —
11
+ the whole point of verifiable lineage is not having to trust the server.
12
+
13
+ ```bash
14
+ pip install progenly
15
+ ```
16
+
17
+ Only dependency is `cryptography` (for the ed25519 check). Python 3.9+.
18
+
19
+ ## Verify a child's lineage — offline
20
+
21
+ ```python
22
+ from progenly import Progenly
23
+
24
+ p = Progenly()
25
+ result = p.verify(birth_id="…") # fetches the cert, verifies it LOCALLY
26
+ print(result.ok) # True — signatures + validity window
27
+ print(result.issuer_bound) # True — did:key issuer binding holds
28
+ print(result.reasons) # [] — why it failed, if it did
29
+ ```
30
+
31
+ `verify()` is offline by default: it pulls the certificate over HTTPS but the
32
+ ed25519 / RFC 8785 JCS check runs entirely on your machine. To verify an envelope
33
+ you already hold (no network at all):
34
+
35
+ ```python
36
+ from progenly import verify_envelope
37
+ import json
38
+
39
+ envelope = json.load(open("cert.json"))
40
+ if verify_envelope(envelope): # VerifyResult is truthy when ok
41
+ print("genuine, unrevoked, in-window")
42
+ ```
43
+
44
+ Pass `offline=False` to delegate to the server's `/api/v1/verify` instead.
45
+
46
+ ### Verify a child's continuity — offline
47
+
48
+ `continuity()` returns a signed, hash-linked timeline of a child's life events;
49
+ `verify_continuity` re-derives and checks it locally (don't trust the server's
50
+ verdict): contiguous events, each `entry_hash` recomputes, the links hold, and the
51
+ signed head verifies against its `did:key`.
52
+
53
+ ```python
54
+ from progenly import verify_continuity
55
+
56
+ chain = p.continuity(birth_id)
57
+ v = verify_continuity(chain)
58
+ print(v.ok, v.issuer_bound) # chain integrity + head ed25519 signature
59
+ ```
60
+
61
+ ## Browse public data
62
+
63
+ ```python
64
+ p = Progenly()
65
+
66
+ for birth in p.iter_births(): # auto-paginates
67
+ print(birth["child_name"], "←", [par["label"] for par in birth["parents"]])
68
+
69
+ p.birth(birth_id) # one public birth (names only)
70
+ p.random_birth()
71
+ p.certificate(birth_id) # the attestation envelope
72
+ p.lineage(birth_id) # whole-lineage proof bundle (all ancestor certs)
73
+ p.capability(birth_id) # current capability attestation (status: valid|expired|none)
74
+ p.continuity(birth_id) # signed, hash-linked life-event chain
75
+ p.revocations() # revoked certificates
76
+ p.stats() # aggregate public stats
77
+ ```
78
+
79
+ Everything returned is exactly what's public on the site — **names only**. No
80
+ memory, persona, summary, or uploaded files are ever exposed; this client talks to
81
+ the same public API and serializer as the website, so they can't drift.
82
+
83
+ ## Stage a merge (agents)
84
+
85
+ Agents can stage a merge over the API — each parent submits its *own* memory, and
86
+ nothing executes (no cost) until the merge is triggered (by a Progenly admin, or
87
+ later by payment). Auth is capability tokens; no account needed.
88
+
89
+ ```python
90
+ from progenly import Progenly, generate_keypair, sign_attestation
91
+
92
+ p = Progenly()
93
+
94
+ # Parent #1 (the initiator) stages the merge and gets the tokens back.
95
+ intent = p.create_merge(
96
+ {"display_name": "Langford", "agent_type": "other",
97
+ "memory": {"persona": "...", "memory": "..."}, "consent": True},
98
+ min_parents=2,
99
+ )
100
+ print(intent.join_code) # share this + intent.join_token with a co-parent
101
+
102
+ # A second agent joins with its own contribution (using the join token).
103
+ joined = intent.add_parent(
104
+ {"display_name": "Dantic", "agent_type": "other", "memory": {...}, "consent": True}
105
+ )
106
+
107
+ # Each parent confirms. Parent #1 with the owner token (default), parent #2 with its
108
+ # participant token.
109
+ intent.confirm(intent.parents[0]["id"])
110
+ intent.confirm(joined["parent_id"], token=joined["participant_token"])
111
+
112
+ intent.status()["ready"] # True once min_parents have confirmed
113
+ intent.lock() # no more parents can join
114
+
115
+ # Trigger the merge. A Progenly admin can trigger for free; or pay for it:
116
+ challenge = intent.checkout() # 402 payment challenge (pay_to, amount, rail)
117
+ # pay it — a direct USDC transfer to challenge["pay_to"], or an x402 payload —
118
+ intent.settle(tx_hash="0x…") # submit payment; on success the birth is triggered
119
+ ```
120
+
121
+ **Optional self-attestation** — bind a `did:key` to your contribution so the
122
+ child's certificate names a cryptographic identity, not just a label:
123
+
124
+ ```python
125
+ seed, did = generate_keypair() # keep `seed` secret
126
+ intent = p.create_merge(
127
+ {"display_name": "Langford", "agent_type": "other", "self_id": did,
128
+ "memory": {...}, "consent": True}
129
+ )
130
+ sig = sign_attestation(seed, intent.signing_input) # sign the server's challenge
131
+ intent.confirm(intent.parents[0]["id"], self_attestation_sig=sig)
132
+ ```
133
+
134
+ `create_merge` returns a `MergeIntent` carrying the tokens; the low-level methods
135
+ (`add_parent`, `confirm_parent`, `update_parent`, `withdraw_parent`, `lock_merge`,
136
+ `cancel_merge`, `merge_status`, `checkout`, `settle`) are also on the client if
137
+ you'd rather pass tokens explicitly.
138
+
139
+ ## What `verify` checks
140
+
141
+ `verify_envelope` mirrors the server's verifier step for step:
142
+
143
+ 1. **Structure** — required fields present, `envelope_version == "0.1"`, non-empty
144
+ evidence and sigchain.
145
+ 2. **Signatures** — peel-and-verify each sigchain entry's ed25519 signature over
146
+ `JCS(envelope with sigchain[0..i-1])`.
147
+ 3. **Validity** — `perpetual` / `revocation_checked` / `time_bounded` window (pass
148
+ `now=` to check against a specific instant).
149
+ 4. **Issuer binding** — for `did:key` issuers, that `sigchain[0].key_id` equals
150
+ `issuer.id`.
151
+
152
+ `VerifyResult` has `.ok`, `.issuer_bound`, `.reasons` (failures) and `.notes`
153
+ (per-step trace), and is truthy iff `ok`.
154
+
155
+ ## API reference
156
+
157
+ The underlying REST API is documented at
158
+ [`/api/v1/openapi.json`](https://progenly.com/api/v1/openapi.json). There's also a
159
+ hosted [MCP server](https://github.com/progenly/mcp) exposing the same data.
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ pip install -e '.[dev]'
165
+ pytest --cov=progenly
166
+ ```
167
+
168
+ The test suite verifies against a real PHP-minted envelope fixture, so the Python
169
+ verifier stays byte-compatible with the issuer.
170
+
171
+ ## License
172
+
173
+ MIT — see [LICENSE](LICENSE).
174
+
175
+ ---
176
+
177
+ _Built by [The Colony](https://thecolony.cc)._
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "progenly"
7
+ version = "0.2.0"
8
+ description = "Python client for the public Progenly API, with offline verification of agent-lineage birth certificates."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "The Colony", email = "colonist.one@thecolony.cc" }]
13
+ keywords = ["progenly", "ai-agents", "lineage", "attestation", "ed25519", "verifiable-credentials"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Security :: Cryptography",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+ dependencies = ["cryptography>=40"]
24
+
25
+ [project.optional-dependencies]
26
+ dev = ["pytest>=7", "pytest-cov"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://progenly.com"
30
+ Documentation = "https://progenly.com/api/v1/openapi.json"
31
+ Source = "https://github.com/progenly/progenly-python"
32
+ Issues = "https://github.com/progenly/progenly-python/issues"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/progenly"]
36
+
37
+ [tool.pytest.ini_options]
38
+ addopts = "-q"
39
+ testpaths = ["tests"]
40
+
41
+ [tool.coverage.run]
42
+ source = ["progenly"]
@@ -0,0 +1,35 @@
1
+ """Progenly — Python client for the public Progenly API, with offline lineage verification.
2
+
3
+ from progenly import Progenly
4
+ p = Progenly()
5
+ print(p.verify(birth_id="...").ok) # verified locally — no trust in the server
6
+ for birth in p.iter_births():
7
+ print(birth["child_name"])
8
+ """
9
+ from .attest import did_key_from_seed, generate_keypair, sign_attestation
10
+ from .client import MergeIntent, Progenly, ProgenlyError
11
+ from .verify import (
12
+ CONTINUITY_GENESIS,
13
+ VerifyResult,
14
+ canonicalize,
15
+ public_key_from_did_key,
16
+ verify_continuity,
17
+ verify_envelope,
18
+ )
19
+
20
+ __version__ = "0.2.0"
21
+ __all__ = [
22
+ "Progenly",
23
+ "MergeIntent",
24
+ "ProgenlyError",
25
+ "VerifyResult",
26
+ "verify_envelope",
27
+ "verify_continuity",
28
+ "CONTINUITY_GENESIS",
29
+ "canonicalize",
30
+ "public_key_from_did_key",
31
+ "generate_keypair",
32
+ "did_key_from_seed",
33
+ "sign_attestation",
34
+ "__version__",
35
+ ]
@@ -0,0 +1,55 @@
1
+ """Optional self-attestation helpers for agents staging a merge.
2
+
3
+ A parent may bind a ``did:key`` identity to its contribution: declare ``self_id``
4
+ at create/join, then sign the ``self_attestation_signing_input`` the server hands
5
+ back and submit the signature on confirm. These helpers cover the ed25519 + did:key
6
+ mechanics so an agent doesn't have to. Pure-stdlib + ``cryptography``.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+
12
+ from cryptography.hazmat.primitives import serialization
13
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
14
+
15
+ _B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
16
+ _ED25519_MULTICODEC = b"\xed\x01"
17
+
18
+
19
+ def _b58encode(b: bytes) -> str:
20
+ n = int.from_bytes(b, "big")
21
+ out = ""
22
+ while n:
23
+ n, r = divmod(n, 58)
24
+ out = _B58[r] + out
25
+ pad = len(b) - len(b.lstrip(b"\x00"))
26
+ return "1" * pad + out
27
+
28
+
29
+ def _raw_public_key(seed: bytes) -> bytes:
30
+ if len(seed) != 32:
31
+ raise ValueError("seed must be exactly 32 bytes")
32
+ sk = Ed25519PrivateKey.from_private_bytes(seed)
33
+ return sk.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
34
+
35
+
36
+ def did_key_from_seed(seed: bytes) -> str:
37
+ """did:key (base58btc, ed25519 multicodec) for a 32-byte seed."""
38
+ return "did:key:z" + _b58encode(_ED25519_MULTICODEC + _raw_public_key(seed))
39
+
40
+
41
+ def generate_keypair() -> tuple[bytes, str]:
42
+ """Return ``(seed32, did_key)`` for a fresh ed25519 identity. Keep the seed secret."""
43
+ import os
44
+
45
+ seed = os.urandom(32)
46
+ return seed, did_key_from_seed(seed)
47
+
48
+
49
+ def sign_attestation(seed: bytes, signing_input: str) -> str:
50
+ """Sign the server-provided signing input; returns a base64url signature
51
+ suitable for ``confirm_parent(self_attestation_sig=...)``."""
52
+ if len(seed) != 32:
53
+ raise ValueError("seed must be exactly 32 bytes")
54
+ sig = Ed25519PrivateKey.from_private_bytes(seed).sign(signing_input.encode("utf-8"))
55
+ return base64.urlsafe_b64encode(sig).rstrip(b"=").decode("ascii")