@voluntas-progressus/gbp-stack
v1.5.4
Published
Node.js bindings for the Group Protocol Stack: a layered, end-to-end encrypted group-messaging protocol family built on top of MLS (RFC 9420).
Maintainers
Readme
@voluntas-progressus/gbp-stack — Node.js bindings for the Group Protocol Stack
Node.js bindings for the Group Protocol Stack: a layered, end-to-end encrypted group-messaging protocol family built on top of MLS (RFC 9420).
The package wraps the native gbp_stack shared library through
koffi. Pre-built native binaries
for every supported runtime identifier (win-x64, win-arm64,
linux-x64, linux-arm64, osx-x64, osx-arm64) are bundled in the
package — npm install @voluntas-progressus/gbp-stack works out of the box.
Layers
┌── application ──────────────────────────────────────────────────────┐
│ GtpClient · GapClient · GspClient (TCP / UDP / SCTP-like) │
├─────────────────────────────────────────────────────────────────────┤
│ GroupNode (GBP — IP-like base) │
├─────────────────────────────────────────────────────────────────────┤
│ MlsContext (RFC 9420) │
└─────────────────────────────────────────────────────────────────────┘Payload codec
Each sub-protocol payload can be encoded as CBOR (default), Protobuf,
or FlatBuffers. Pass PayloadCodec to send and accept; the chosen
codec is surfaced in ev.codec on payload_received events.
import { PayloadCodec } from "@voluntas-progressus/gbp-stack";
// FlatBuffers for low-latency audio, Protobuf for text archival, CBOR default
const frame = gtpAlice.send(alice, aliceMls, 2, 0xCAFEn, "hi", PayloadCodec.FlatBuffers);
for (const ev of bob.onWire(bobMls, frame.wire)) {
if (ev.kind === "payload_received") {
const r = gtpBob.accept(ev.plaintext!, bobMls.epoch, ev.codec ?? PayloadCodec.Cbor);
console.log(r.text);
}
}| Value | Name | Description |
|-------|------|-------------|
| 0 | PayloadCodec.Cbor | Default; pf field omitted from wire |
| 1 | PayloadCodec.Protobuf | Protobuf via gbp-proto |
| 2 | PayloadCodec.FlatBuffers | FlatBuffers via gbp-flat; lowest latency |
Sub-protocol toolkits
Beyond the protocol clients, the package ships ready-made helpers:
MessageHistory+Watermark— bounded GTP message log + per-sender high-water mark for serving and consuming resync requests.JitterBuffer— bounded GAP reorder window keyed bymediaSourceId, withpush,popInOrder,popForceand late-frame detection.RoleRegistry+Permissions— bind numeric role ids to permission bit-masks and check them withrequire/has.CapabilitiesNegotiator— track per-member advertisements and query theintersection(),union(),groupSupports()andmissing()views.SFrameSession+SFrameEncryptor— SFrame (draft-ietf-sframe-enc) E2EE for GAP audio frames; per-sender AES-GCM keys derived from MLS exporter, 1024-entry sliding-window replay protection.encodeGbpFrame— low-level helper to construct a raw CBOR GBP frame.lookupError— return the CBORErrorObjectfor a known error code.
Coordinator events
NodeEvent surfaces three new event kinds for coordinator election:
| kind | Extra fields | Meaning |
|--------|-------------|---------|
| coordinator_election_needed | — | The local node should initiate GSP COORDINATOR_CLAIM |
| became_coordinator | — | This node won the election |
| coordinator_claim | claimant | A peer sent COORDINATOR_CLAIM with this member id |
Install
npm install @voluntas-progressus/[email protected]Quick start
import { MlsContext, GroupNode, GtpClient, StreamType } from "@voluntas-progressus/gbp-stack";
const aliceMls = MlsContext.create("alice");
const bobMls = MlsContext.create("bob");
const bobKp = bobMls.exportKeyPackage();
const welcome = aliceMls.invite(bobKp); // alice auto-finalizes; epoch advances to 1n
bobMls.acceptWelcome(welcome);
const gid = aliceMls.groupId;
const alice = GroupNode.create(1, gid);
const bob = GroupNode.create(2, gid);
alice.bootstrapAsCreator(aliceMls.epoch);
bob.bootstrapAsJoiner(bobMls.epoch);
const gtpAlice = GtpClient.create();
const gtpBob = GtpClient.create();
const frame = gtpAlice.send(alice, aliceMls, 2, 0xCAFEF00Dn, "hello");
for (const ev of bob.onWire(bobMls, frame.wire)) {
if (ev.kind === "payload_received" && ev.streamType === StreamType.Text) {
const r = gtpBob.accept(ev.plaintext!, bobMls.epoch);
console.log(r.text); // → "hello"
// r.status is "new" (first message from this sender)
// subsequent messages → "new"; duplicates → "duplicate"
}
}GSP signals with per-signal arguments
Signals that target a specific member or resource require CBOR-encoded arguments.
Use GspClient.sendWithArgs for these signals:
import { GspClient, SignalType } from "@voluntas-progressus/gbp-stack";
// Minimal CBOR helpers
function cborUint(n: number): number[] {
if (n <= 23) return [n];
if (n <= 0xFF) return [0x18, n];
if (n <= 0xFFFF) return [0x19, (n >> 8) & 0xFF, n & 0xFF];
return [0x1A, (n>>24)&0xFF, (n>>16)&0xFF, (n>>8)&0xFF, n&0xFF];
}
function cborMap1(k: number, v: number): Buffer {
return Buffer.from([0xA1, ...cborUint(k), ...cborUint(v)]);
}
// Signal-specific args schemas:
// MUTE / Unmute → {0: target_member_id}
// RoleChange → {0: target_member_id, 1: new_role_id}
// StreamStart / StreamStop → {0: stream_type}
// CodecUpdate → {0: codec_id}
// Join / Leave → no args; use GspClient.send
const gsp = GspClient.create();
// Mute member 3
const frame = gsp.sendWithArgs(
aliceNode, aliceMls,
0, // target: 0 = broadcast
SignalType.Mute,
0, // roleClaim
1, // requestId
cborMap1(0, 3), // args: {0: target_member_id=3}
);MLS multi-member group pattern
When inviting a member to an existing group (not the first invite), use
inviteFull so that existing members can process the commit:
// Alice adds Carol to an alice+bob group
const { commit, welcome } = aliceMls.inviteFull(carolMls.exportKeyPackage());
aliceMls.finalizeCommit(); // alice's epoch advances
bobMls.processMessage(commit); // bob stages the commit
bobMls.finalizeCommit(); // bob's epoch advances to match alice
carolMls.acceptWelcome(welcome); // carol joins
console.assert(aliceMls.epoch === bobMls.epoch && bobMls.epoch === carolMls.epoch);License
Licensed under Apache License, Version 2.0.
