session

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package session is the client-side engine: it dials the relay, runs the create/join handshake, performs the PAKE + group-key exchange, and moves encrypted service envelopes. It compiles natively (tests, headless tools) and to WASM (the browser core) — no syscall/js here, ever.

Index

Constants

View Source
const DefaultProtocol = "parley/v1"

DefaultProtocol is the domain-separation label used when no WithProtocol option is given. Applications with their own deployed protocol pass their label instead; the label feeds both session-ID derivation and the PAKE key schedule, so all ends of a session must agree on it.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is one end of a live session.

func Host

func Host(ctx context.Context, relayURL string, opts ...Option) (*Client, string, error)

Host creates a new session on the relay and returns a keyed client plus the freshly generated code phrase. Joiner roles are assigned by the configured RolePolicy (see WithRolePolicy); the default seats observers as RoleObserver and everyone else as RoleMember.

func Join

func Join(ctx context.Context, relayURL, phraseText string, opts ...Option) (*Client, error)

Join connects to an existing session with its phrase. It returns once the handshake completes and the client is keyed; a wrong phrase surfaces as crypto.ErrUnwrap. A joiner that wants to watch rather than participate passes WithObserver.

func (*Client) BecomeHost

func (c *Client) BecomeHost()

BecomeHost promotes this client to session host in place after a host migration: role→RoleHost, hostID→self. The group key and keyed flag are untouched — the successor already holds the key and wraps that same key to future joiners (no re-key). Called on the routing goroutine during promotion.

func (*Client) Broadcast

func (c *Client) Broadcast(serviceID string, body []byte) error

Broadcast seals one service message to every other participant.

func (*Client) ClaimHost

func (c *Client) ClaimHost() error

ClaimHost tells the relay this client is the new host, so it routes future joiners' handshake here. Safe from any goroutine (writeFrame holds writeMu).

func (*Client) Close

func (c *Client) Close() error

Close tears the connection down gracefully (a normal-closure "bye"); the relay treats this as leaving for good — no grace, no reconnect.

func (*Client) CloseNow

func (c *Client) CloseNow() error

CloseNow drops the connection abruptly, with no close handshake — as a real network loss does. The relay classifies this as unexpected and holds the slot for its grace window, so a subsequent Reconnect can reclaim it. The read loop emits Closed("connection lost") and exits.

func (*Client) Events

func (c *Client) Events() <-chan Event

Events delivers session events. The channel closes after Closed.

func (*Client) HostID

func (c *Client) HostID() wire.ParticipantID

HostID returns the session host's participant ID. Locked because host migration can reassign it from another goroutine (see SetHostID/BecomeHost).

func (*Client) Reconnect

func (c *Client) Reconnect(ctx context.Context) error

Reconnect re-dials the relay and reclaims this client's slot after an unexpected drop, preserving the participant id, role, group key, and per- service send sequence — so peers see an uninterrupted sender and no re-key is needed. It returns once keyed traffic can flow again; the caller then rebinds its routing layer onto the client. A rejected reclaim (grace expired, relay restarted, bad token) returns an error the caller should treat as terminal.

Precondition: the previous readLoop has ended (the events channel closed), which is exactly the state after a Closed event — so no goroutine is touching the client when Reconnect runs.

func (*Client) RekeyWithNewHost

func (c *Client) RekeyWithNewHost() error

RekeyWithNewHost is called on a surviving non-host member after a host migration: the promoted host shares no pairwise key with it, so it re-runs the PAKE (over the phrase everyone still holds) to establish one; the host replies (KindRekeyPake2) and then delivers the rotated group key (KindRekey). No-op if we are the host or not keyed.

func (*Client) Role

func (c *Client) Role() Role

Role returns this client's role (RoleHost, or as assigned by the host).

func (*Client) RotateForMigration

func (c *Client) RotateForMigration()

RotateForMigration is called on the promoted host immediately after BecomeHost: it mints a fresh group key the departed host does NOT hold, so once survivors re-fetch it (see RekeyWithNewHost) the departed host is locked out of subsequent traffic. The old key is retained in the ring so in-flight frames still open, and the pairwise map is reset (a promoted host holds none) to refill as survivors re-PAKE. Runs on the routing goroutine, before any survivor's re-PAKE can traverse the network, so the fresh key is in place when the re-PAKE responder wraps it.

func (*Client) Self

func (c *Client) Self() wire.ParticipantID

Self returns this client's participant ID (valid after construction).

func (*Client) SendTo

func (c *Client) SendTo(to wire.ParticipantID, serviceID string, body []byte) error

SendTo seals one service message to a single participant.

func (*Client) SetHostID

func (c *Client) SetHostID(id wire.ParticipantID)

SetHostID re-points a non-host survivor at the newly elected host so its ctl accepts the new host's announces and its services address the right authority.

type Closed

type Closed struct{ Reason string }

Closed fires last: the session is over.

type Event

type Event any

Event is anything the session surfaces to the layer above (a service mux or UI bridge): MemberJoined, MemberKeyed, MemberLeft, Frame, Closed.

type Frame

type Frame struct {
	From     wire.ParticipantID
	Envelope wire.Envelope
}

Frame is one decrypted service envelope from a peer.

type MemberJoined

type MemberJoined struct{ ID wire.ParticipantID }

MemberJoined fires when the relay announces a new participant. For the host it fires before that member is keyed.

type MemberKeyed

type MemberKeyed struct {
	ID   wire.ParticipantID
	Role Role
}

MemberKeyed fires on the host once a joiner completes the handshake.

type MemberLeft

type MemberLeft struct{ ID wire.ParticipantID }

MemberLeft fires when a participant disconnects.

type Option

type Option func(*config)

Option configures Host and Join.

func WithObserver added in v0.2.0

func WithObserver() Option

WithObserver marks a Join as an observer: the joiner asks the host to seat it with an observer role rather than a member seat. The intent rides the first handshake flight; the host's RolePolicy decides what to do with it. No effect on Host.

func WithProtocol

func WithProtocol(label string) Option

WithProtocol sets the application's domain-separation label (for example "myapp/v1"). Changing an application's label is a protocol version bump: clients with different labels derive different session IDs and keys and cannot talk to each other.

func WithRolePolicy added in v0.2.0

func WithRolePolicy(p RolePolicy) Option

WithRolePolicy installs the policy used to assign roles to joiners when this end is the session host. Pass it to Join as well as Host: after a host migration the promoted joiner becomes the role assigner.

type Role

type Role uint8

Role is assigned by the host when it wraps the group key for a joiner. The zero value means "not keyed yet". The byte value rides inside the encrypted handshake, so it is application protocol: every end and every version of an application must agree on the numbering. RoleNone and RoleHost are reserved by the library; values 2..255 belong to the application's RolePolicy (RoleMember and RoleObserver are the built-in default vocabulary).

const (
	RoleNone     Role = 0 // not keyed yet; in a rekey wrap: "keep your existing role"
	RoleHost     Role = 1 // reserved: the key authority
	RoleMember   Role = 2 // default policy: an ordinary joiner
	RoleObserver Role = 3 // default policy: a joiner that asked to observe
)

func DefaultRolePolicy added in v0.2.0

func DefaultRolePolicy(_ wire.ParticipantID, observer bool, _ map[wire.ParticipantID]Role) Role

DefaultRolePolicy seats observers as RoleObserver and everyone else as RoleMember.

type RolePolicy added in v0.2.0

type RolePolicy func(joiner wire.ParticipantID, observer bool, assigned map[wire.ParticipantID]Role) Role

RolePolicy decides the role for a newly keyed joiner. The host calls it once per completed handshake, on the session's read goroutine, with the joiner's participant ID, whether the joiner asked to observe (see WithObserver), and a snapshot of the roles this host has already assigned to its joiners (the host itself is not in the map). Implementations must be fast, must not block, and must not call back into the Client — a stalled policy stalls the whole session end. RoleNone and RoleHost are reserved: a policy returning either is clamped to RoleMember. Any other value (2..255) is delivered verbatim, so applications may define their own role vocabulary on top of Role.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL