gbp-stack 1.2.3__tar.gz → 1.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gbp-stack
3
- Version: 1.2.3
3
+ Version: 1.4.0
4
4
  Summary: Python bindings for the Group Protocol Stack: a layered, end-to-end encrypted group-messaging protocol family built on top of MLS (RFC 9420).
5
5
  Author: Group Protocol Stack contributors
6
6
  License: Apache-2.0
@@ -61,11 +61,23 @@ Beyond the protocol clients, the package ships ready-made helpers:
61
61
  * `SFrameSession` + `SFrameEncryptor` — SFrame (draft-ietf-sframe-enc) E2EE
62
62
  for GAP audio frames; per-sender AES-GCM keys derived from MLS exporter,
63
63
  1024-entry sliding-window replay protection.
64
+ * `encode_gbp_frame` — low-level helper to construct a raw CBOR GBP frame.
65
+ * `lookup_error` — return the CBOR `ErrorObject` for a known error code.
66
+
67
+ ### Coordinator events
68
+
69
+ `NodeEvent` surfaces three new event kinds for coordinator election:
70
+
71
+ | `kind` | Extra fields | Meaning |
72
+ |--------|-------------|---------|
73
+ | `coordinator_election_needed` | — | The local node should initiate GSP `COORDINATOR_CLAIM` |
74
+ | `became_coordinator` | — | This node won the election |
75
+ | `coordinator_claim` | `claimant` | A peer sent `COORDINATOR_CLAIM` with this member id |
64
76
 
65
77
  ## Install
66
78
 
67
79
  ```sh
68
- pip install gbp-stack==1.2.3
80
+ pip install gbp-stack==1.4.0
69
81
  ```
70
82
 
71
83
  ## Quick start
@@ -77,7 +89,7 @@ with MlsContext.create("alice") as alice_mls, \
77
89
  MlsContext.create("bob") as bob_mls:
78
90
 
79
91
  bob_kp = bob_mls.export_key_package()
80
- welcome = alice_mls.invite(bob_kp)
92
+ welcome = alice_mls.invite(bob_kp) # alice auto-finalizes; epoch advances to 1
81
93
  bob_mls.accept_welcome(welcome)
82
94
 
83
95
  group_id = alice_mls.group_id
@@ -92,8 +104,67 @@ with MlsContext.create("alice") as alice_mls, \
92
104
  frame = gtp_alice.send(alice, alice_mls, target=2,
93
105
  message_id=0xCAFE_F00D, text="hello")
94
106
  for ev in bob.on_wire(bob_mls, frame.wire):
95
- if ev.kind == "payload_received" and ev.stream_type == 2:
96
- print(gtp_bob.accept(ev.plaintext).text)
107
+ if ev.kind == "payload_received" and ev.stream_type == 2: # StreamType.Text
108
+ result = gtp_bob.accept(ev.plaintext, bob_mls.epoch)
109
+ print(result.text) # → "hello"
110
+ # result.status is "new" (first message from this sender)
111
+ # subsequent messages → "new"; duplicates → "duplicate"
112
+ ```
113
+
114
+ ## GSP signals with per-signal arguments
115
+
116
+ Signals that target a specific member or resource require CBOR-encoded `args`.
117
+ The `send` method accepts an optional `args: bytes` keyword argument.
118
+
119
+ ```python
120
+ import struct
121
+ from gbp_stack import GspClient, SignalType
122
+
123
+ # Minimal CBOR helpers
124
+ def cbor_uint(n: int) -> bytes:
125
+ if n <= 23: return bytes([n])
126
+ if n <= 0xFF: return bytes([0x18, n])
127
+ if n <= 0xFFFF: return bytes([0x19, n >> 8, n & 0xFF])
128
+ return bytes([0x1A, (n>>24)&0xFF, (n>>16)&0xFF, (n>>8)&0xFF, n&0xFF])
129
+
130
+ def cbor_map1(k: int, v: int) -> bytes:
131
+ return bytes([0xA1]) + cbor_uint(k) + cbor_uint(v)
132
+
133
+ def cbor_map2(k0: int, v0: int, k1: int, v1: int) -> bytes:
134
+ return bytes([0xA2]) + cbor_uint(k0) + cbor_uint(v0) + cbor_uint(k1) + cbor_uint(v1)
135
+
136
+ # Signal-specific args schemas:
137
+ # MUTE / UNMUTE → {0: target_member_id}
138
+ # ROLE_CHANGE → {0: target_member_id, 1: new_role_id}
139
+ # STREAM_START / STREAM_STOP → {0: stream_type}
140
+ # CODEC_UPDATE → {0: codec_id}
141
+ # JOIN / LEAVE → no args required
142
+
143
+ with GspClient.create() as gsp_alice:
144
+ # Mute member 3 (no role_claim needed for self-moderation)
145
+ frame = gsp_alice.send(
146
+ alice_node, alice_mls,
147
+ target=0, # 0 = broadcast
148
+ signal=SignalType.MUTE,
149
+ role_claim=0,
150
+ request_id=1,
151
+ args=cbor_map1(0, 3), # {0: target_member_id=3}
152
+ )
153
+ ```
154
+
155
+ ## MLS multi-member group pattern
156
+
157
+ When inviting a member to an **existing** group (not the first invite), use
158
+ `invite_full` so that existing members can process the commit:
159
+
160
+ ```python
161
+ # Alice adds Carol to an alice+bob group
162
+ commit, welcome = alice_mls.invite_full(carol_mls.export_key_package())
163
+ alice_mls.finalize_commit() # alice's epoch advances
164
+ bob_mls.process_message(commit) # bob stages the commit
165
+ bob_mls.finalize_commit() # bob's epoch advances to match alice
166
+ carol_mls.accept_welcome(welcome) # carol joins
167
+ assert alice_mls.epoch == bob_mls.epoch == carol_mls.epoch
97
168
  ```
98
169
 
99
170
  ## License
@@ -37,11 +37,23 @@ Beyond the protocol clients, the package ships ready-made helpers:
37
37
  * `SFrameSession` + `SFrameEncryptor` — SFrame (draft-ietf-sframe-enc) E2EE
38
38
  for GAP audio frames; per-sender AES-GCM keys derived from MLS exporter,
39
39
  1024-entry sliding-window replay protection.
40
+ * `encode_gbp_frame` — low-level helper to construct a raw CBOR GBP frame.
41
+ * `lookup_error` — return the CBOR `ErrorObject` for a known error code.
42
+
43
+ ### Coordinator events
44
+
45
+ `NodeEvent` surfaces three new event kinds for coordinator election:
46
+
47
+ | `kind` | Extra fields | Meaning |
48
+ |--------|-------------|---------|
49
+ | `coordinator_election_needed` | — | The local node should initiate GSP `COORDINATOR_CLAIM` |
50
+ | `became_coordinator` | — | This node won the election |
51
+ | `coordinator_claim` | `claimant` | A peer sent `COORDINATOR_CLAIM` with this member id |
40
52
 
41
53
  ## Install
42
54
 
43
55
  ```sh
44
- pip install gbp-stack==1.2.3
56
+ pip install gbp-stack==1.4.0
45
57
  ```
46
58
 
47
59
  ## Quick start
@@ -53,7 +65,7 @@ with MlsContext.create("alice") as alice_mls, \
53
65
  MlsContext.create("bob") as bob_mls:
54
66
 
55
67
  bob_kp = bob_mls.export_key_package()
56
- welcome = alice_mls.invite(bob_kp)
68
+ welcome = alice_mls.invite(bob_kp) # alice auto-finalizes; epoch advances to 1
57
69
  bob_mls.accept_welcome(welcome)
58
70
 
59
71
  group_id = alice_mls.group_id
@@ -68,8 +80,67 @@ with MlsContext.create("alice") as alice_mls, \
68
80
  frame = gtp_alice.send(alice, alice_mls, target=2,
69
81
  message_id=0xCAFE_F00D, text="hello")
70
82
  for ev in bob.on_wire(bob_mls, frame.wire):
71
- if ev.kind == "payload_received" and ev.stream_type == 2:
72
- print(gtp_bob.accept(ev.plaintext).text)
83
+ if ev.kind == "payload_received" and ev.stream_type == 2: # StreamType.Text
84
+ result = gtp_bob.accept(ev.plaintext, bob_mls.epoch)
85
+ print(result.text) # → "hello"
86
+ # result.status is "new" (first message from this sender)
87
+ # subsequent messages → "new"; duplicates → "duplicate"
88
+ ```
89
+
90
+ ## GSP signals with per-signal arguments
91
+
92
+ Signals that target a specific member or resource require CBOR-encoded `args`.
93
+ The `send` method accepts an optional `args: bytes` keyword argument.
94
+
95
+ ```python
96
+ import struct
97
+ from gbp_stack import GspClient, SignalType
98
+
99
+ # Minimal CBOR helpers
100
+ def cbor_uint(n: int) -> bytes:
101
+ if n <= 23: return bytes([n])
102
+ if n <= 0xFF: return bytes([0x18, n])
103
+ if n <= 0xFFFF: return bytes([0x19, n >> 8, n & 0xFF])
104
+ return bytes([0x1A, (n>>24)&0xFF, (n>>16)&0xFF, (n>>8)&0xFF, n&0xFF])
105
+
106
+ def cbor_map1(k: int, v: int) -> bytes:
107
+ return bytes([0xA1]) + cbor_uint(k) + cbor_uint(v)
108
+
109
+ def cbor_map2(k0: int, v0: int, k1: int, v1: int) -> bytes:
110
+ return bytes([0xA2]) + cbor_uint(k0) + cbor_uint(v0) + cbor_uint(k1) + cbor_uint(v1)
111
+
112
+ # Signal-specific args schemas:
113
+ # MUTE / UNMUTE → {0: target_member_id}
114
+ # ROLE_CHANGE → {0: target_member_id, 1: new_role_id}
115
+ # STREAM_START / STREAM_STOP → {0: stream_type}
116
+ # CODEC_UPDATE → {0: codec_id}
117
+ # JOIN / LEAVE → no args required
118
+
119
+ with GspClient.create() as gsp_alice:
120
+ # Mute member 3 (no role_claim needed for self-moderation)
121
+ frame = gsp_alice.send(
122
+ alice_node, alice_mls,
123
+ target=0, # 0 = broadcast
124
+ signal=SignalType.MUTE,
125
+ role_claim=0,
126
+ request_id=1,
127
+ args=cbor_map1(0, 3), # {0: target_member_id=3}
128
+ )
129
+ ```
130
+
131
+ ## MLS multi-member group pattern
132
+
133
+ When inviting a member to an **existing** group (not the first invite), use
134
+ `invite_full` so that existing members can process the commit:
135
+
136
+ ```python
137
+ # Alice adds Carol to an alice+bob group
138
+ commit, welcome = alice_mls.invite_full(carol_mls.export_key_package())
139
+ alice_mls.finalize_commit() # alice's epoch advances
140
+ bob_mls.process_message(commit) # bob stages the commit
141
+ bob_mls.finalize_commit() # bob's epoch advances to match alice
142
+ carol_mls.accept_welcome(welcome) # carol joins
143
+ assert alice_mls.epoch == bob_mls.epoch == carol_mls.epoch
73
144
  ```
74
145
 
75
146
  ## License
@@ -20,7 +20,7 @@ README for a worked example.
20
20
  from ._native import last_error, version
21
21
  from .capabilities import CapabilitiesNegotiator
22
22
  from .gap_client import GapAcceptResult, GapClient
23
- from .gbp_node import GroupNode, NodeEvent, NodeState, OutboundFrame, StreamType
23
+ from .gbp_node import GroupNode, NodeEvent, NodeState, OutboundFrame, StreamType, encode_gbp_frame, lookup_error
24
24
  from .gsp_client import GspAcceptResult, GspClient, SignalType
25
25
  from .gtp_client import GtpAcceptResult, GtpClient
26
26
  from .history import MessageEntry, MessageHistory, Watermark
@@ -66,8 +66,10 @@ __all__ = [
66
66
  "SignalType",
67
67
  "StreamType",
68
68
  "Watermark",
69
+ "encode_gbp_frame",
69
70
  "last_error",
71
+ "lookup_error",
70
72
  "version",
71
73
  ]
72
74
 
73
- __version__ = "1.2.3"
75
+ __version__ = "1.4.0"
@@ -153,6 +153,11 @@ gsp_client_send = _bind(
153
153
  GbpBuffer,
154
154
  [c_int32, c_int32, c_int32, c_uint32, c_uint32, c_uint32, c_uint32],
155
155
  )
156
+ gsp_client_send_with_args = _bind(
157
+ "gsp_client_send_with_args",
158
+ GbpBuffer,
159
+ [c_int32, c_int32, c_int32, c_uint32, c_uint32, c_uint32, c_uint32, c_void_p, c_size_t],
160
+ )
156
161
  gsp_client_accept = _bind("gsp_client_accept", c_void_p, [c_int32, c_uint64, c_void_p, c_size_t])
157
162
 
158
163
  # ── SFrame ────────────────────────────────────────────────────────────────────
@@ -175,6 +180,25 @@ gbp_sframe_decrypt = _bind(
175
180
  [c_int32, c_void_p, c_size_t, c_void_p, c_size_t, ctypes.POINTER(c_uint32)],
176
181
  )
177
182
 
183
+ gbp_frame_encode_v = _bind(
184
+ "gbp_frame_encode_v",
185
+ GbpBuffer,
186
+ [
187
+ c_uint8, # version
188
+ c_void_p, # group_id_16
189
+ c_uint64, # epoch
190
+ c_uint32, # transition_id
191
+ c_uint32, # stream_type
192
+ c_uint32, # stream_id
193
+ c_uint16, # flags
194
+ c_uint32, # sequence_no
195
+ c_void_p, # payload_ptr
196
+ c_size_t, # payload_len
197
+ ],
198
+ )
199
+
200
+ gbp_error_lookup = _bind("gbp_error_lookup", GbpBuffer, [c_uint16])
201
+
178
202
 
179
203
  def take_buffer(buf: GbpBuffer) -> bytes:
180
204
  """Copy a returned :class:`GbpBuffer` into a ``bytes`` object and free it."""
@@ -69,7 +69,12 @@ class NodeEvent:
69
69
  * ``control`` — populates ``sender``, ``opcode`` and ``transition_id``;
70
70
  * ``error`` — populates ``code``, ``code_hex``, ``class_``, ``retryable``,
71
71
  ``fatal`` and ``reason``;
72
- * ``epoch_advanced`` — populates ``epoch`` and ``transition_id``.
72
+ * ``epoch_advanced`` — populates ``epoch`` and ``transition_id``;
73
+ * ``coordinator_election_needed`` — no extra fields; the local node should
74
+ start the coordinator-election handshake (GSP ``COORDINATOR_CLAIM``);
75
+ * ``became_coordinator`` — no extra fields; this node won the election;
76
+ * ``coordinator_claim`` — populates ``claimant`` (member id of the peer
77
+ that sent a ``COORDINATOR_CLAIM`` signal).
73
78
  """
74
79
 
75
80
  kind: str
@@ -92,6 +97,7 @@ class NodeEvent:
92
97
  fatal: Optional[bool] = None
93
98
  reason: Optional[str] = None
94
99
  epoch: Optional[int] = None
100
+ claimant: Optional[int] = None
95
101
 
96
102
  @classmethod
97
103
  def _from_dict(cls, d: dict) -> "NodeEvent":
@@ -118,6 +124,7 @@ class NodeEvent:
118
124
  fatal=d.get("fatal"),
119
125
  reason=d.get("reason"),
120
126
  epoch=d.get("epoch"),
127
+ claimant=d.get("claimant"),
121
128
  )
122
129
 
123
130
 
@@ -137,6 +144,51 @@ def _unpack(buf: _n.GbpBuffer, what: str) -> OutboundFrame:
137
144
  return OutboundFrame(target=target, wire=raw[4:])
138
145
 
139
146
 
147
+ def encode_gbp_frame(
148
+ version: int,
149
+ group_id: bytes,
150
+ epoch: int,
151
+ transition_id: int,
152
+ stream_type: int,
153
+ stream_id: int,
154
+ flags: int,
155
+ sequence_no: int,
156
+ payload: bytes,
157
+ ) -> bytes:
158
+ """Encode a raw GBP frame to CBOR bytes.
159
+
160
+ Low-level helper — most callers should use :meth:`GroupNode.send_control`
161
+ or the sub-protocol ``send`` methods instead.
162
+ """
163
+ if len(group_id) != 16:
164
+ raise ValueError("group_id must be 16 bytes")
165
+ gid = (ctypes.c_uint8 * 16).from_buffer_copy(group_id)
166
+
167
+ def call(ptr, length):
168
+ return _n.gbp_frame_encode_v(
169
+ version,
170
+ ctypes.cast(gid, ctypes.c_void_p),
171
+ epoch,
172
+ transition_id,
173
+ stream_type,
174
+ stream_id,
175
+ flags,
176
+ sequence_no,
177
+ ptr,
178
+ length,
179
+ )
180
+
181
+ buf = _n.call_with_bytes(payload, call)
182
+ return _n.take_buffer(buf)
183
+
184
+
185
+ def lookup_error(code: int) -> Optional[bytes]:
186
+ """Return the CBOR-encoded ``ErrorObject`` for *code*, or ``None`` if unknown."""
187
+ buf = _n.gbp_error_lookup(code)
188
+ data = _n.take_buffer(buf)
189
+ return data if data else None
190
+
191
+
140
192
  class GroupNode:
141
193
  """GBP-layer group node.
142
194
 
@@ -85,12 +85,20 @@ class GspClient:
85
85
  signal: SignalType,
86
86
  role_claim: int,
87
87
  request_id: int,
88
+ args: bytes = b"",
88
89
  ) -> OutboundFrame:
89
- """Send a signal."""
90
- buf = _n.gsp_client_send(
91
- self._handle, node.handle, mls.handle, target,
92
- int(signal), role_claim, request_id,
93
- )
90
+ """Send a signal.
91
+
92
+ ``args`` carries opcode-specific CBOR-encoded arguments required by
93
+ signals such as MUTE/UNMUTE (``{0: target_member_id}``),
94
+ ROLE_CHANGE (``{0: target_member_id, 1: new_role_id}``), etc.
95
+ """
96
+ def do_send(ptr, length):
97
+ return _n.gsp_client_send_with_args(
98
+ self._handle, node.handle, mls.handle, target,
99
+ int(signal), role_claim, request_id, ptr, length,
100
+ )
101
+ buf = _n.call_with_bytes(args, do_send)
94
102
  return _unpack(buf, "gsp_client_send")
95
103
 
96
104
  def accept(self, plaintext: bytes, current_epoch: int) -> GspAcceptResult:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gbp-stack
3
- Version: 1.2.3
3
+ Version: 1.4.0
4
4
  Summary: Python bindings for the Group Protocol Stack: a layered, end-to-end encrypted group-messaging protocol family built on top of MLS (RFC 9420).
5
5
  Author: Group Protocol Stack contributors
6
6
  License: Apache-2.0
@@ -61,11 +61,23 @@ Beyond the protocol clients, the package ships ready-made helpers:
61
61
  * `SFrameSession` + `SFrameEncryptor` — SFrame (draft-ietf-sframe-enc) E2EE
62
62
  for GAP audio frames; per-sender AES-GCM keys derived from MLS exporter,
63
63
  1024-entry sliding-window replay protection.
64
+ * `encode_gbp_frame` — low-level helper to construct a raw CBOR GBP frame.
65
+ * `lookup_error` — return the CBOR `ErrorObject` for a known error code.
66
+
67
+ ### Coordinator events
68
+
69
+ `NodeEvent` surfaces three new event kinds for coordinator election:
70
+
71
+ | `kind` | Extra fields | Meaning |
72
+ |--------|-------------|---------|
73
+ | `coordinator_election_needed` | — | The local node should initiate GSP `COORDINATOR_CLAIM` |
74
+ | `became_coordinator` | — | This node won the election |
75
+ | `coordinator_claim` | `claimant` | A peer sent `COORDINATOR_CLAIM` with this member id |
64
76
 
65
77
  ## Install
66
78
 
67
79
  ```sh
68
- pip install gbp-stack==1.2.3
80
+ pip install gbp-stack==1.4.0
69
81
  ```
70
82
 
71
83
  ## Quick start
@@ -77,7 +89,7 @@ with MlsContext.create("alice") as alice_mls, \
77
89
  MlsContext.create("bob") as bob_mls:
78
90
 
79
91
  bob_kp = bob_mls.export_key_package()
80
- welcome = alice_mls.invite(bob_kp)
92
+ welcome = alice_mls.invite(bob_kp) # alice auto-finalizes; epoch advances to 1
81
93
  bob_mls.accept_welcome(welcome)
82
94
 
83
95
  group_id = alice_mls.group_id
@@ -92,8 +104,67 @@ with MlsContext.create("alice") as alice_mls, \
92
104
  frame = gtp_alice.send(alice, alice_mls, target=2,
93
105
  message_id=0xCAFE_F00D, text="hello")
94
106
  for ev in bob.on_wire(bob_mls, frame.wire):
95
- if ev.kind == "payload_received" and ev.stream_type == 2:
96
- print(gtp_bob.accept(ev.plaintext).text)
107
+ if ev.kind == "payload_received" and ev.stream_type == 2: # StreamType.Text
108
+ result = gtp_bob.accept(ev.plaintext, bob_mls.epoch)
109
+ print(result.text) # → "hello"
110
+ # result.status is "new" (first message from this sender)
111
+ # subsequent messages → "new"; duplicates → "duplicate"
112
+ ```
113
+
114
+ ## GSP signals with per-signal arguments
115
+
116
+ Signals that target a specific member or resource require CBOR-encoded `args`.
117
+ The `send` method accepts an optional `args: bytes` keyword argument.
118
+
119
+ ```python
120
+ import struct
121
+ from gbp_stack import GspClient, SignalType
122
+
123
+ # Minimal CBOR helpers
124
+ def cbor_uint(n: int) -> bytes:
125
+ if n <= 23: return bytes([n])
126
+ if n <= 0xFF: return bytes([0x18, n])
127
+ if n <= 0xFFFF: return bytes([0x19, n >> 8, n & 0xFF])
128
+ return bytes([0x1A, (n>>24)&0xFF, (n>>16)&0xFF, (n>>8)&0xFF, n&0xFF])
129
+
130
+ def cbor_map1(k: int, v: int) -> bytes:
131
+ return bytes([0xA1]) + cbor_uint(k) + cbor_uint(v)
132
+
133
+ def cbor_map2(k0: int, v0: int, k1: int, v1: int) -> bytes:
134
+ return bytes([0xA2]) + cbor_uint(k0) + cbor_uint(v0) + cbor_uint(k1) + cbor_uint(v1)
135
+
136
+ # Signal-specific args schemas:
137
+ # MUTE / UNMUTE → {0: target_member_id}
138
+ # ROLE_CHANGE → {0: target_member_id, 1: new_role_id}
139
+ # STREAM_START / STREAM_STOP → {0: stream_type}
140
+ # CODEC_UPDATE → {0: codec_id}
141
+ # JOIN / LEAVE → no args required
142
+
143
+ with GspClient.create() as gsp_alice:
144
+ # Mute member 3 (no role_claim needed for self-moderation)
145
+ frame = gsp_alice.send(
146
+ alice_node, alice_mls,
147
+ target=0, # 0 = broadcast
148
+ signal=SignalType.MUTE,
149
+ role_claim=0,
150
+ request_id=1,
151
+ args=cbor_map1(0, 3), # {0: target_member_id=3}
152
+ )
153
+ ```
154
+
155
+ ## MLS multi-member group pattern
156
+
157
+ When inviting a member to an **existing** group (not the first invite), use
158
+ `invite_full` so that existing members can process the commit:
159
+
160
+ ```python
161
+ # Alice adds Carol to an alice+bob group
162
+ commit, welcome = alice_mls.invite_full(carol_mls.export_key_package())
163
+ alice_mls.finalize_commit() # alice's epoch advances
164
+ bob_mls.process_message(commit) # bob stages the commit
165
+ bob_mls.finalize_commit() # bob's epoch advances to match alice
166
+ carol_mls.accept_welcome(welcome) # carol joins
167
+ assert alice_mls.epoch == bob_mls.epoch == carol_mls.epoch
97
168
  ```
98
169
 
99
170
  ## License
@@ -16,4 +16,5 @@ gbp_stack/sframe_session.py
16
16
  gbp_stack.egg-info/PKG-INFO
17
17
  gbp_stack.egg-info/SOURCES.txt
18
18
  gbp_stack.egg-info/dependency_links.txt
19
- gbp_stack.egg-info/top_level.txt
19
+ gbp_stack.egg-info/top_level.txt
20
+ tests/test_integration.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gbp-stack"
7
- version = "1.2.3"
7
+ version = "1.4.0"
8
8
  description = "Python bindings for the Group Protocol Stack: a layered, end-to-end encrypted group-messaging protocol family built on top of MLS (RFC 9420)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"