// 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 }