buzzkit 0.1.1__tar.gz → 0.1.2__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.
- {buzzkit-0.1.1 → buzzkit-0.1.2}/.gitignore +2 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/Cargo.lock +1 -1
- {buzzkit-0.1.1 → buzzkit-0.1.2}/Cargo.toml +1 -1
- {buzzkit-0.1.1 → buzzkit-0.1.2}/PKG-INFO +1 -1
- {buzzkit-0.1.1 → buzzkit-0.1.2}/pyproject.toml +1 -1
- {buzzkit-0.1.1 → buzzkit-0.1.2}/python/buzzkit/__init__.py +5 -1
- {buzzkit-0.1.1 → buzzkit-0.1.2}/python/buzzkit/_native.pyi +9 -1
- {buzzkit-0.1.1 → buzzkit-0.1.2}/python/buzzkit/client.py +8 -1
- {buzzkit-0.1.1 → buzzkit-0.1.2}/src/lib.rs +51 -3
- {buzzkit-0.1.1 → buzzkit-0.1.2}/tests/test_signing.py +22 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/.github/workflows/CI.yml +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/.github/workflows/release.yml +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/LICENSE +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/LICENSE-APACHE +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/NOTICE +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/README.md +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/examples/_shared.py +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/examples/claim_invite.py +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/examples/list_channels.py +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/examples/offline_sign.py +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/examples/send_message.py +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/examples/set_profile.py +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/examples/subscribe.py +0 -0
- {buzzkit-0.1.1 → buzzkit-0.1.2}/python/buzzkit/py.typed +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "buzzkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Python bindings + async client for Block's Buzz (Nostr) protocol, backed by Rust buzz-core/buzz-sdk"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -20,7 +20,9 @@ from ._native import (
|
|
|
20
20
|
build_auth_event,
|
|
21
21
|
build_join_channel_event,
|
|
22
22
|
build_message_event,
|
|
23
|
+
build_presence_event,
|
|
23
24
|
build_profile_event,
|
|
25
|
+
compute_auth_tag,
|
|
24
26
|
generate_keypair,
|
|
25
27
|
pubkey_from_secret,
|
|
26
28
|
sign_nip98,
|
|
@@ -28,7 +30,7 @@ from ._native import (
|
|
|
28
30
|
)
|
|
29
31
|
from .client import BuzzClient
|
|
30
32
|
|
|
31
|
-
__version__ = "0.1.
|
|
33
|
+
__version__ = "0.1.2"
|
|
32
34
|
|
|
33
35
|
__all__ = [
|
|
34
36
|
"KIND_ADD_MEMBER",
|
|
@@ -42,7 +44,9 @@ __all__ = [
|
|
|
42
44
|
"build_auth_event",
|
|
43
45
|
"build_join_channel_event",
|
|
44
46
|
"build_message_event",
|
|
47
|
+
"build_presence_event",
|
|
45
48
|
"build_profile_event",
|
|
49
|
+
"compute_auth_tag",
|
|
46
50
|
"generate_keypair",
|
|
47
51
|
"pubkey_from_secret",
|
|
48
52
|
"sign_nip98",
|
|
@@ -24,9 +24,17 @@ def build_profile_event(
|
|
|
24
24
|
def build_join_channel_event(secret: str, channel_id: str) -> str:
|
|
25
25
|
"""Build + sign a NIP-29 channel self-join event (kind 9000, role=bot)."""
|
|
26
26
|
|
|
27
|
-
def
|
|
27
|
+
def build_presence_event(secret: str, status: str = ...) -> str:
|
|
28
|
+
"""Build + sign a presence event (kind 20001); status online/away/offline."""
|
|
29
|
+
|
|
30
|
+
def build_auth_event(
|
|
31
|
+
secret: str, challenge: str, relay_url: str, auth_tag: str | None = ...
|
|
32
|
+
) -> str:
|
|
28
33
|
"""Build + sign a NIP-42 AUTH event (kind 22242); returns event JSON."""
|
|
29
34
|
|
|
35
|
+
def compute_auth_tag(owner_secret: str, agent_pubkey_hex: str, conditions: str = ...) -> str:
|
|
36
|
+
"""Compute a NIP-OA owner-attestation tag JSON attesting an agent pubkey."""
|
|
37
|
+
|
|
30
38
|
def sign_nip98(secret: str, method: str, url: str, body: bytes | None = ...) -> str:
|
|
31
39
|
"""Return an ``Authorization: Nostr <base64>`` header value (NIP-98)."""
|
|
32
40
|
|
|
@@ -119,6 +119,11 @@ class BuzzClient:
|
|
|
119
119
|
ev = _native.build_join_channel_event(self._secret, channel_id)
|
|
120
120
|
return await self.publish(ev)
|
|
121
121
|
|
|
122
|
+
async def publish_presence(self, status: str = "online") -> dict:
|
|
123
|
+
"""Announce presence (kind 20001, ephemeral) over the WebSocket."""
|
|
124
|
+
ev = _native.build_presence_event(self._secret, status)
|
|
125
|
+
return await self.publish(ev)
|
|
126
|
+
|
|
122
127
|
async def query(self, filters: list[dict]) -> list[dict]:
|
|
123
128
|
"""Run a NIP-01 REQ over the HTTP bridge; returns a list of events."""
|
|
124
129
|
url = f"{self._http}/query"
|
|
@@ -225,7 +230,9 @@ class BuzzClient:
|
|
|
225
230
|
q.put_nowait(("closed", "connection lost"))
|
|
226
231
|
|
|
227
232
|
async def _on_auth_challenge(self, challenge: str) -> None:
|
|
228
|
-
auth_ev = json.loads(
|
|
233
|
+
auth_ev = json.loads(
|
|
234
|
+
_native.build_auth_event(self._secret, challenge, self._ws_url, self._auth_tag)
|
|
235
|
+
)
|
|
229
236
|
self._auth_event_id = auth_ev["id"]
|
|
230
237
|
await self._ws.send(json.dumps(["AUTH", auth_ev]))
|
|
231
238
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
use base64::engine::general_purpose::STANDARD as B64;
|
|
9
9
|
use base64::Engine as _;
|
|
10
|
-
use nostr::{EventBuilder, JsonUtil, Keys, Kind, RelayUrl, Tag, ToBech32};
|
|
10
|
+
use nostr::{EventBuilder, JsonUtil, Keys, Kind, PublicKey, RelayUrl, Tag, ToBech32};
|
|
11
11
|
use pyo3::exceptions::PyValueError;
|
|
12
12
|
use pyo3::prelude::*;
|
|
13
13
|
use sha2::{Digest, Sha256};
|
|
@@ -69,17 +69,50 @@ fn build_message_event(
|
|
|
69
69
|
|
|
70
70
|
/// Build and sign a NIP-42 AUTH event (kind 22242) for a relay challenge.
|
|
71
71
|
/// Returns the event JSON to send back as `["AUTH", <event>]`.
|
|
72
|
+
///
|
|
73
|
+
/// `auth_tag` is an optional NIP-OA owner-attestation tag JSON
|
|
74
|
+
/// (`["auth", <owner>, <conditions>, <sig>]`) — inject it so the relay records
|
|
75
|
+
/// the agent's owner.
|
|
72
76
|
#[pyfunction]
|
|
73
|
-
|
|
77
|
+
#[pyo3(signature = (secret, challenge, relay_url, auth_tag=None))]
|
|
78
|
+
fn build_auth_event(
|
|
79
|
+
secret: &str,
|
|
80
|
+
challenge: &str,
|
|
81
|
+
relay_url: &str,
|
|
82
|
+
auth_tag: Option<&str>,
|
|
83
|
+
) -> PyResult<String> {
|
|
74
84
|
let keys = keys_from_secret(secret)?;
|
|
75
85
|
let url = RelayUrl::parse(relay_url)
|
|
76
86
|
.map_err(|e| PyValueError::new_err(format!("invalid relay url: {e}")))?;
|
|
77
|
-
let
|
|
87
|
+
let mut builder = EventBuilder::auth(challenge, url);
|
|
88
|
+
if let Some(tag_json) = auth_tag {
|
|
89
|
+
let tag = buzz_sdk::nip_oa::parse_auth_tag(tag_json)
|
|
90
|
+
.map_err(|e| PyValueError::new_err(format!("invalid auth_tag: {e}")))?;
|
|
91
|
+
builder = builder.tags([tag]);
|
|
92
|
+
}
|
|
93
|
+
let event = builder
|
|
78
94
|
.sign_with_keys(&keys)
|
|
79
95
|
.map_err(|e| PyValueError::new_err(format!("sign: {e}")))?;
|
|
80
96
|
Ok(event.as_json())
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
/// Compute a NIP-OA owner-attestation tag: the OWNER signs an attestation over
|
|
100
|
+
/// the agent's pubkey. Returns the `["auth", <owner>, <conditions>, <sig>]` tag
|
|
101
|
+
/// JSON. `owner_secret` must differ from the agent (no self-attestation).
|
|
102
|
+
#[pyfunction]
|
|
103
|
+
#[pyo3(signature = (owner_secret, agent_pubkey_hex, conditions=""))]
|
|
104
|
+
fn compute_auth_tag(
|
|
105
|
+
owner_secret: &str,
|
|
106
|
+
agent_pubkey_hex: &str,
|
|
107
|
+
conditions: &str,
|
|
108
|
+
) -> PyResult<String> {
|
|
109
|
+
let owner_keys = keys_from_secret(owner_secret)?;
|
|
110
|
+
let agent_pubkey = PublicKey::from_hex(agent_pubkey_hex)
|
|
111
|
+
.map_err(|e| PyValueError::new_err(format!("invalid agent pubkey: {e}")))?;
|
|
112
|
+
buzz_sdk::nip_oa::compute_auth_tag(&owner_keys, &agent_pubkey, conditions)
|
|
113
|
+
.map_err(|e| PyValueError::new_err(format!("compute_auth_tag: {e}")))
|
|
114
|
+
}
|
|
115
|
+
|
|
83
116
|
/// Sign a NIP-98 HTTP-auth event (kind 27235). Returns the
|
|
84
117
|
/// `Authorization: Nostr <base64>` header value for the Buzz HTTP bridge.
|
|
85
118
|
#[pyfunction]
|
|
@@ -160,16 +193,31 @@ fn build_join_channel_event(secret: &str, channel_id: &str) -> PyResult<String>
|
|
|
160
193
|
Ok(event.as_json())
|
|
161
194
|
}
|
|
162
195
|
|
|
196
|
+
/// Build and sign a presence event (kind 20001). `status` is "online",
|
|
197
|
+
/// "away", or "offline". Ephemeral — publish over the WebSocket.
|
|
198
|
+
#[pyfunction]
|
|
199
|
+
#[pyo3(signature = (secret, status="online"))]
|
|
200
|
+
fn build_presence_event(secret: &str, status: &str) -> PyResult<String> {
|
|
201
|
+
let keys = keys_from_secret(secret)?;
|
|
202
|
+
let event = buzz_sdk::build_presence_update(status)
|
|
203
|
+
.map_err(|e| PyValueError::new_err(format!("build_presence: {e}")))?
|
|
204
|
+
.sign_with_keys(&keys)
|
|
205
|
+
.map_err(|e| PyValueError::new_err(format!("sign: {e}")))?;
|
|
206
|
+
Ok(event.as_json())
|
|
207
|
+
}
|
|
208
|
+
|
|
163
209
|
#[pymodule]
|
|
164
210
|
fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
165
211
|
m.add_function(wrap_pyfunction!(generate_keypair, m)?)?;
|
|
166
212
|
m.add_function(wrap_pyfunction!(pubkey_from_secret, m)?)?;
|
|
167
213
|
m.add_function(wrap_pyfunction!(build_message_event, m)?)?;
|
|
168
214
|
m.add_function(wrap_pyfunction!(build_auth_event, m)?)?;
|
|
215
|
+
m.add_function(wrap_pyfunction!(compute_auth_tag, m)?)?;
|
|
169
216
|
m.add_function(wrap_pyfunction!(sign_nip98, m)?)?;
|
|
170
217
|
m.add_function(wrap_pyfunction!(verify_event, m)?)?;
|
|
171
218
|
m.add_function(wrap_pyfunction!(build_profile_event, m)?)?;
|
|
172
219
|
m.add_function(wrap_pyfunction!(build_join_channel_event, m)?)?;
|
|
220
|
+
m.add_function(wrap_pyfunction!(build_presence_event, m)?)?;
|
|
173
221
|
|
|
174
222
|
// Buzz event kinds (subset — mirrors buzz-core/src/kind.rs).
|
|
175
223
|
m.add("KIND_REACTION", 7u16)?;
|
|
@@ -73,3 +73,25 @@ def test_join_channel_event_keeps_self_p_tag():
|
|
|
73
73
|
assert ["p", pk_hex] in event["tags"]
|
|
74
74
|
assert ["role", "bot"] in event["tags"]
|
|
75
75
|
assert any(t[0] == "h" for t in event["tags"])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_compute_auth_tag():
|
|
79
|
+
owner_nsec, _, owner_pk = buzzkit.generate_keypair()
|
|
80
|
+
_, _, agent_pk = buzzkit.generate_keypair()
|
|
81
|
+
tag = json.loads(buzzkit.compute_auth_tag(owner_nsec, agent_pk))
|
|
82
|
+
assert tag[0] == "auth"
|
|
83
|
+
assert tag[1] == owner_pk # owner pubkey
|
|
84
|
+
assert len(tag) == 4 # ["auth", owner, conditions, sig]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_compute_auth_tag_rejects_self():
|
|
88
|
+
nsec, _, pk_hex = buzzkit.generate_keypair()
|
|
89
|
+
with pytest.raises(ValueError):
|
|
90
|
+
buzzkit.compute_auth_tag(nsec, pk_hex) # owner == agent
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_presence_event():
|
|
94
|
+
nsec, _, _ = buzzkit.generate_keypair()
|
|
95
|
+
event = json.loads(buzzkit.build_presence_event(nsec, "online"))
|
|
96
|
+
assert event["kind"] == buzzkit.KIND_PRESENCE_UPDATE == 20001
|
|
97
|
+
assert event["content"] == "online" or ["status", "online"] in event["tags"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|