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.
@@ -6,3 +6,5 @@ __pycache__/
6
6
  *.egg-info/
7
7
  .python-version
8
8
  agent.secret
9
+ /dist
10
+ owner.secret
@@ -168,7 +168,7 @@ dependencies = [
168
168
 
169
169
  [[package]]
170
170
  name = "buzzkit"
171
- version = "0.1.1"
171
+ version = "0.1.2"
172
172
  dependencies = [
173
173
  "base64",
174
174
  "buzz-core",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "buzzkit"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  edition = "2021"
5
5
  publish = false
6
6
  description = "PyO3 bindings over Block's Buzz zero-I/O crates (buzz-core / buzz-sdk)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: buzzkit
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "buzzkit"
7
- version = "0.1.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.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 build_auth_event(secret: str, challenge: str, relay_url: str) -> str:
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(_native.build_auth_event(self._secret, challenge, self._ws_url))
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
- fn build_auth_event(secret: &str, challenge: &str, relay_url: &str) -> PyResult<String> {
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 event = EventBuilder::auth(challenge, url)
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