chore: rename quicproquo → quicprochat in Rust workspace

Rename all crate directories, package names, binary names, proto
package/module paths, ALPN strings, env var prefixes, config filenames,
mDNS service names, and plugin ABI symbols from quicproquo/qpq to
quicprochat/qpc.
This commit is contained in:
2026-03-07 18:24:52 +01:00
parent d8c1392587
commit a710037dde
212 changed files with 609 additions and 609 deletions

377
sdks/go/qpc/client.go Normal file
View File

@@ -0,0 +1,377 @@
// Package qpq provides the high-level Go API for interacting with a quicproquo server.
//
// It wraps the generated Cap'n Proto types and transport layer into an
// ergonomic client that handles authentication, key management, and messaging.
package qpq
import (
"context"
"fmt"
"quicproquo.dev/sdk/go/proto/node"
"quicproquo.dev/sdk/go/transport"
)
// Options configures the connection to a quicproquo server.
type Options struct {
// Addr is the host:port of the server (e.g. "127.0.0.1:5001").
Addr string
// InsecureSkipVerify disables TLS certificate verification (dev mode only).
InsecureSkipVerify bool
// CACertPath is the path to a PEM-encoded CA certificate for production use.
CACertPath string
}
// Message represents a received message envelope.
type Message struct {
Seq uint64
Data []byte
}
// Client is the high-level quicproquo client.
type Client struct {
conn *transport.Connection
token []byte // session token from OPAQUE login
deviceID []byte // optional device ID
}
// Connect establishes a connection to a qpq server.
func Connect(ctx context.Context, opts Options) (*Client, error) {
conn, err := transport.Connect(ctx, transport.ConnectOptions{
Addr: opts.Addr,
InsecureSkipVerify: opts.InsecureSkipVerify,
CACertPath: opts.CACertPath,
})
if err != nil {
return nil, fmt.Errorf("qpq: connect: %w", err)
}
return &Client{conn: conn}, nil
}
// Close disconnects from the server.
func (c *Client) Close() error {
return c.conn.Close()
}
// SetSessionToken sets a pre-existing session token for authentication.
// Use this when you have already performed OPAQUE login externally.
func (c *Client) SetSessionToken(token []byte) {
c.token = token
}
// SetDeviceID sets the device ID sent with each authenticated RPC call.
func (c *Client) SetDeviceID(id []byte) {
c.deviceID = id
}
// setAuth populates an Auth struct on an RPC params message.
func (c *Client) setAuth(auth node.Auth) error {
auth.SetVersion(1)
if err := auth.SetAccessToken(c.token); err != nil {
return fmt.Errorf("set access token: %w", err)
}
if len(c.deviceID) > 0 {
if err := auth.SetDeviceId(c.deviceID); err != nil {
return fmt.Errorf("set device id: %w", err)
}
}
return nil
}
// Health checks server health and returns the status string.
func (c *Client) Health(ctx context.Context) (string, error) {
future, release := c.conn.Client().Health(ctx, nil)
defer release()
res, err := future.Struct()
if err != nil {
return "", fmt.Errorf("qpq: health: %w", err)
}
status, err := res.Status()
if err != nil {
return "", fmt.Errorf("qpq: health: read status: %w", err)
}
return status, nil
}
// ResolveUser looks up a username and returns their identity key.
func (c *Client) ResolveUser(ctx context.Context, username string) (identityKey []byte, err error) {
future, release := c.conn.Client().ResolveUser(ctx, func(p node.NodeService_resolveUser_Params) error {
if err := p.SetUsername(username); err != nil {
return err
}
auth, err := p.NewAuth()
if err != nil {
return err
}
return c.setAuth(auth)
})
defer release()
res, err := future.Struct()
if err != nil {
return nil, fmt.Errorf("qpq: resolve user %q: %w", username, err)
}
key, err := res.IdentityKey()
if err != nil {
return nil, fmt.Errorf("qpq: resolve user %q: read identity key: %w", username, err)
}
// Copy the key out of the capnp message buffer.
out := make([]byte, len(key))
copy(out, key)
return out, nil
}
// CreateChannel creates a 1:1 DM channel with a peer identified by their identity key.
func (c *Client) CreateChannel(ctx context.Context, peerKey []byte) (channelID []byte, wasNew bool, err error) {
future, release := c.conn.Client().CreateChannel(ctx, func(p node.NodeService_createChannel_Params) error {
if err := p.SetPeerKey(peerKey); err != nil {
return err
}
auth, err := p.NewAuth()
if err != nil {
return err
}
return c.setAuth(auth)
})
defer release()
res, err := future.Struct()
if err != nil {
return nil, false, fmt.Errorf("qpq: create channel: %w", err)
}
chID, err := res.ChannelId()
if err != nil {
return nil, false, fmt.Errorf("qpq: create channel: read channel id: %w", err)
}
out := make([]byte, len(chID))
copy(out, chID)
return out, res.WasNew(), nil
}
// Send enqueues a message payload to a recipient identified by their identity key.
func (c *Client) Send(ctx context.Context, recipientKey, payload []byte) (seq uint64, err error) {
return c.sendInternal(ctx, recipientKey, payload, 0)
}
// SendWithTTL enqueues a disappearing message with a time-to-live in seconds.
func (c *Client) SendWithTTL(ctx context.Context, recipientKey, payload []byte, ttlSecs uint32) (seq uint64, err error) {
return c.sendInternal(ctx, recipientKey, payload, ttlSecs)
}
func (c *Client) sendInternal(ctx context.Context, recipientKey, payload []byte, ttlSecs uint32) (uint64, error) {
future, release := c.conn.Client().Enqueue(ctx, func(p node.NodeService_enqueue_Params) error {
if err := p.SetRecipientKey(recipientKey); err != nil {
return err
}
if err := p.SetPayload(payload); err != nil {
return err
}
p.SetVersion(1)
if ttlSecs > 0 {
p.SetTtlSecs(ttlSecs)
}
auth, err := p.NewAuth()
if err != nil {
return err
}
return c.setAuth(auth)
})
defer release()
res, err := future.Struct()
if err != nil {
return 0, fmt.Errorf("qpq: send: %w", err)
}
return res.Seq(), nil
}
// Receive fetches queued messages for the given recipient key.
func (c *Client) Receive(ctx context.Context, recipientKey []byte) ([]Message, error) {
future, release := c.conn.Client().Fetch(ctx, func(p node.NodeService_fetch_Params) error {
if err := p.SetRecipientKey(recipientKey); err != nil {
return err
}
p.SetVersion(1)
auth, err := p.NewAuth()
if err != nil {
return err
}
return c.setAuth(auth)
})
defer release()
res, err := future.Struct()
if err != nil {
return nil, fmt.Errorf("qpq: receive: %w", err)
}
return extractMessages(res.Payloads())
}
// ReceiveWait long-polls for messages with a timeout in milliseconds.
func (c *Client) ReceiveWait(ctx context.Context, recipientKey []byte, timeoutMs uint64) ([]Message, error) {
future, release := c.conn.Client().FetchWait(ctx, func(p node.NodeService_fetchWait_Params) error {
if err := p.SetRecipientKey(recipientKey); err != nil {
return err
}
p.SetVersion(1)
p.SetTimeoutMs(timeoutMs)
auth, err := p.NewAuth()
if err != nil {
return err
}
return c.setAuth(auth)
})
defer release()
res, err := future.Struct()
if err != nil {
return nil, fmt.Errorf("qpq: receive wait: %w", err)
}
return extractMessages(res.Payloads())
}
// DeleteAccount permanently deletes the authenticated user's account.
func (c *Client) DeleteAccount(ctx context.Context) error {
future, release := c.conn.Client().DeleteAccount(ctx, func(p node.NodeService_deleteAccount_Params) error {
auth, err := p.NewAuth()
if err != nil {
return err
}
return c.setAuth(auth)
})
defer release()
res, err := future.Struct()
if err != nil {
return fmt.Errorf("qpq: delete account: %w", err)
}
if !res.Success() {
return fmt.Errorf("qpq: delete account: server returned success=false")
}
return nil
}
// RegisterStart initiates OPAQUE registration and returns the server's response bytes.
//
// The OPAQUE protocol requires client-side cryptographic operations.
// The request parameter must be the serialized OPAQUE RegistrationRequest
// generated by an OPAQUE client library (e.g. github.com/cloudflare/circl/opaque).
// Process the returned server response with your OPAQUE library to produce
// the upload bytes for RegisterFinish.
func (c *Client) RegisterStart(ctx context.Context, username string, request []byte) (serverResponse []byte, err error) {
future, release := c.conn.Client().OpaqueRegisterStart(ctx, func(p node.NodeService_opaqueRegisterStart_Params) error {
if err := p.SetUsername(username); err != nil {
return err
}
return p.SetRequest(request)
})
defer release()
res, err := future.Struct()
if err != nil {
return nil, fmt.Errorf("qpq: register start: %w", err)
}
resp, err := res.Response()
if err != nil {
return nil, fmt.Errorf("qpq: register start: read response: %w", err)
}
out := make([]byte, len(resp))
copy(out, resp)
return out, nil
}
// RegisterFinish completes OPAQUE registration with the upload and identity key.
func (c *Client) RegisterFinish(ctx context.Context, username string, upload, identityKey []byte) error {
future, release := c.conn.Client().OpaqueRegisterFinish(ctx, func(p node.NodeService_opaqueRegisterFinish_Params) error {
if err := p.SetUsername(username); err != nil {
return err
}
if err := p.SetUpload(upload); err != nil {
return err
}
return p.SetIdentityKey(identityKey)
})
defer release()
_, err := future.Struct()
if err != nil {
return fmt.Errorf("qpq: register finish: %w", err)
}
return nil
}
// LoginStart initiates OPAQUE login and returns the server's response bytes.
func (c *Client) LoginStart(ctx context.Context, username string, request []byte) (serverResponse []byte, err error) {
future, release := c.conn.Client().OpaqueLoginStart(ctx, func(p node.NodeService_opaqueLoginStart_Params) error {
if err := p.SetUsername(username); err != nil {
return err
}
return p.SetRequest(request)
})
defer release()
res, err := future.Struct()
if err != nil {
return nil, fmt.Errorf("qpq: login start: %w", err)
}
resp, err := res.Response()
if err != nil {
return nil, fmt.Errorf("qpq: login start: read response: %w", err)
}
out := make([]byte, len(resp))
copy(out, resp)
return out, nil
}
// LoginFinish completes OPAQUE login and stores the session token.
// The finalization parameter is the OPAQUE finalization message from
// your OPAQUE client library. The identityKey is your public identity key.
func (c *Client) LoginFinish(ctx context.Context, username string, finalization, identityKey []byte) error {
future, release := c.conn.Client().OpaqueLoginFinish(ctx, func(p node.NodeService_opaqueLoginFinish_Params) error {
if err := p.SetUsername(username); err != nil {
return err
}
if err := p.SetFinalization(finalization); err != nil {
return err
}
return p.SetIdentityKey(identityKey)
})
defer release()
res, err := future.Struct()
if err != nil {
return fmt.Errorf("qpq: login finish: %w", err)
}
token, err := res.SessionToken()
if err != nil {
return fmt.Errorf("qpq: login finish: read session token: %w", err)
}
c.token = make([]byte, len(token))
copy(c.token, token)
return nil
}
// extractMessages converts a Cap'n Proto Envelope_List into a slice of Message.
func extractMessages(envList node.Envelope_List, listErr error) ([]Message, error) {
if listErr != nil {
return nil, listErr
}
msgs := make([]Message, envList.Len())
for i := range msgs {
env := envList.At(i)
data, err := env.Data()
if err != nil {
return nil, fmt.Errorf("read message %d data: %w", i, err)
}
dataCopy := make([]byte, len(data))
copy(dataCopy, data)
msgs[i] = Message{
Seq: env.Seq(),
Data: dataCopy,
}
}
return msgs, nil
}

View File

@@ -0,0 +1,58 @@
package qpq
import (
"testing"
)
func TestSetSessionToken(t *testing.T) {
c := &Client{}
token := []byte("test-session-token-abc123")
c.SetSessionToken(token)
if string(c.token) != string(token) {
t.Errorf("expected token %q, got %q", token, c.token)
}
}
func TestSetDeviceID(t *testing.T) {
c := &Client{}
id := []byte{0x01, 0x02, 0x03, 0x04}
c.SetDeviceID(id)
if len(c.deviceID) != 4 {
t.Fatalf("expected 4-byte device ID, got %d bytes", len(c.deviceID))
}
for i, b := range id {
if c.deviceID[i] != b {
t.Errorf("deviceID[%d]: expected %d, got %d", i, b, c.deviceID[i])
}
}
}
func TestMessageStruct(t *testing.T) {
m := Message{Seq: 42, Data: []byte("hello")}
if m.Seq != 42 {
t.Errorf("expected Seq 42, got %d", m.Seq)
}
if string(m.Data) != "hello" {
t.Errorf("expected Data %q, got %q", "hello", m.Data)
}
}
func TestOptionsDefaults(t *testing.T) {
opts := Options{
Addr: "127.0.0.1:5001",
InsecureSkipVerify: true,
}
if opts.Addr != "127.0.0.1:5001" {
t.Errorf("unexpected addr: %s", opts.Addr)
}
if !opts.InsecureSkipVerify {
t.Error("expected InsecureSkipVerify to be true")
}
if opts.CACertPath != "" {
t.Error("expected empty CACertPath")
}
}