hyperspace

package module
v1.2.2 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 14 Imported by: 0

README

Hyperspace

Hyperspace is a Go library that provides P2P connectivity using the Holepunch ecosystem's Hyperbeam. It enables two parties with X25519 keypairs to establish direct connections through NAT traversal.

Overview

Hyperspace derives a passcode from X25519 keys and uses it with Hyperbeam for P2P connections. Both parties compute the same passcode from their X25519 shared secret, enabling them to find each other via the DHT and establish an encrypted connection.

Alice                              Bob
  │                                  │
  │ X25519(keyA_secret, keyB_public)│ X25519(keyB_secret, keyA_public)
  │         = shared_secret          │         = shared_secret
  │                                  │
  │ SHA-256(shared_secret ||         │ SHA-256(shared_secret ||
  │          "hyperspace-v1")         │          "hyperspace-v1")
  │         = passcode               │         = passcode
  │                                  │
  │     z-base-32(passcode)          │     z-base-32(passcode)
  │                                  │
  │       hyperbeam <passcode>       │       hyperbeam <passcode>
  │                                  │
  └──────────── DHT ─────────────────┘
                    │
              P2P Connection

Installation

go get codeberg.org/OperatorFoundation/hyperspace

Prerequisites

Hyperbeam must be installed:

npm install -g hyperbeam

Or use the library's installation function:

if err := hyperspace.InstallHyperbeam(); err != nil {
    log.Fatal(err)
}

Quick Start

package main

import (
    "fmt"
    "log"

    "codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
    // Your X25519 private key (32 bytes)
    myPrivateKey := make([]byte, 32)
    // ... load from secure storage ...

    // Peer's X25519 public key (32 bytes)
    theirPublicKey := make([]byte, 32)
    // ... received from peer ...

    // Derive passcode
    passcode, err := hyperspace.DerivePasscodeFromKeys(myPrivateKey, theirPublicKey)
    if err != nil {
        log.Fatal(err)
    }

    // Convert to z-base-32 (format expected by Hyperbeam)
    z32 := hyperspace.PasscodeToZ32(passcode)
    fmt.Printf("Passcode: %s\n", z32)

    // Connect via Hyperbeam
    conn, err := hyperspace.Connect(z32)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // Use the connection (implements io.ReadWriteCloser)
    conn.Write([]byte("Hello, peer!"))

    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Received: %s\n", buf[:n])
}

API Reference

Passcode Derivation
func ComputeSharedSecret(privateKey, peerPublicKey []byte) ([]byte, error)

Performs X25519 key exchange between a private key and a peer's public key.

sharedSecret, err := hyperspace.ComputeSharedSecret(myPrivateKey, theirPublicKey)
  • privateKey: Your X25519 private key (32 bytes)
  • peerPublicKey: Peer's X25519 public key (32 bytes)
  • Returns: The shared secret (32 bytes)
func DerivePasscode(sharedSecret []byte) ([]byte, error)

Derives a 32-byte passcode from an X25519 shared secret using SHA-256.

passcode, err := hyperspace.DerivePasscode(sharedSecret)
  • sharedSecret: The X25519 shared secret (32 bytes)
  • Returns: The derived passcode (32 bytes)

The passcode is computed as: SHA-256(sharedSecret || "hyperspace-v1")

func DerivePasscodeFromKeys(myPrivateKey, theirPublicKey []byte) ([]byte, error)

Convenience function that combines ComputeSharedSecret and DerivePasscode.

passcode, err := hyperspace.DerivePasscodeFromKeys(myPrivateKey, theirPublicKey)
Encoding Functions
func PasscodeToZ32(passcode []byte) string

Encodes a passcode to z-base-32 format. This is the format expected by Hyperbeam.

z32 := hyperspace.PasscodeToZ32(passcode)
// Example: "ybxwmkpqnbfdtpjyquvfrk3q5zgjmqse9r4k6hyno81gdmwz4sco"
func Z32ToPasscode(z32 string) ([]byte, error)

Decodes a z-base-32 string back to a passcode.

passcode, err := hyperspace.Z32ToPasscode(z32)
func PasscodeToHex(passcode []byte) string

Converts a passcode to hexadecimal format (useful for debugging).

hex := hyperspace.PasscodeToHex(passcode)
// Example: "a1b2c3d4..."
Hyperbeam Management
func IsHyperbeamInstalled() bool

Returns true if the hyperbeam executable is available.

if hyperspace.IsHyperbeamInstalled() {
    // hyperbeam is available
}
func InstallHyperbeam() error

Installs hyperbeam globally via npm. Returns an error if npm is not available or installation fails.

if err := hyperspace.InstallHyperbeam(); err != nil {
    log.Fatal(err)
}
func EnsureHyperbeam() error

Ensures hyperbeam is installed, installing if necessary.

if err := hyperspace.EnsureHyperbeam(); err != nil {
    log.Fatal(err)
}
func HyperbeamPath() (string, error)

Returns the path to the hyperbeam executable.

path, err := hyperspace.HyperbeamPath()
Presence Detection

There are two approaches to check if a peer is online:

Quick Check (Best Effort)
// Check if a peer is announced on the DHT
result := hyperspace.CheckPresence(z32, 5*time.Second)
if result.Found {
    fmt.Println("Peer is online")
} else {
    fmt.Println("Peer not found")
}

// Or use the simpler boolean version
if hyperspace.PingPeer(z32, 5*time.Second) {
    fmt.Println("Peer is online")
}

Note: Presence detection is best-effort. A peer may be:

  • Announced but not currently reachable (network issues)
  • Not announced but available (DHT propagation delay)
  • Behind restrictive NAT that prevents holepunching
Full Connection (More Reliable)
// Try to connect with timeout
conn, err := hyperspace.Connect(z32)
if err != nil {
    log.Fatal(err)
}

// Set a read deadline
time.AfterFunc(10*time.Second, func() {
    conn.Close()
})

// Wait for actual data
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Connected! Received %d bytes\n", n)

Connection

func Connect(passcode string) (*Connection, error)

Establishes a P2P connection via Hyperbeam using the given passcode (z-base-32 format).

conn, err := hyperspace.Connect("ybxwmkpqnbfdtpjyquvfrk3q5zgjmqse9r4k6hyno81gdmwz4sco")
if err != nil {
    log.Fatal(err)
}
defer conn.Close()
func ConnectWithKeys(myPrivateKey, theirPublicKey []byte) (*Connection, error)

Convenience function that derives the passcode from keys and connects.

conn, err := hyperspace.ConnectWithKeys(myPrivateKey, theirPublicKey)
if err != nil {
    log.Fatal(err)
}
defer conn.Close()
type Connection

Represents an active Hyperbeam connection. Implements io.ReadWriteCloser.

type Connection struct {
    // Has unexported fields.
}

func (c *Connection) Read(p []byte) (n int, err error)
func (c *Connection) Write(p []byte) (n int, err error)
func (c *Connection) Close() error
func (c *Connection) Stderr() io.Reader

Integration with Velada

Hyperspace is designed to integrate with Velada, which provides Nahoft-compatible key management via the keychain library.

Basic Integration
import (
    "codeberg.org/OperatorFoundation/hyperspace"
    "codeberg.org/OperatorFoundation/keychain"
)

func ConnectToContact(store *keychain.Store, contactID string) (*hyperspace.Connection, error) {
    // Get the default identity's private key
    identity, err := store.GetDefaultIdentity()
    if err != nil {
        return nil, fmt.Errorf("get identity: %w", err)
    }

    // Load the private key (protected memory)
    privBuf, _, err := store.LoadSecret(identity.X25519KeyID)
    if err != nil {
        return nil, fmt.Errorf("load private key: %w", err)
    }
    defer privBuf.Destroy()

    // Get the contact's public key
    addressBook := keychain.NewAddressBook(store)
    pubKeys, err := addressBook.GetContactPublicKeys(contactID)
    if err != nil {
        return nil, fmt.Errorf("get contact public key: %w", err)
    }

    // Derive passcode
    passcode, err := hyperspace.DerivePasscodeFromKeys(privBuf.Bytes(), pubKeys.X25519PublicKey)
    if err != nil {
        return nil, fmt.Errorf("derive passcode: %w", err)
    }

    // Connect
    return hyperspace.Connect(hyperspace.PasscodeToZ32(passcode))
}
With CryptoManager (Velada's wrapper)
import (
    "codeberg.org/OperatorFoundation/hyperspace"
    "codeberg.org/OperatorFoundation/velada"
)

// Velada's CryptoManager provides higher-level access
func ConnectWithCryptoManager(cm *velada.CryptoManager, contactID string) (*hyperspace.Connection, error) {
    // Get your public key
    myPublicKey, err := cm.GetPublicKey()
    if err != nil {
        return nil, err
    }

    // Get contact's public key
    theirPublicKey, err := cm.GetContactPublicKey(contactID)
    if err != nil {
        return nil, err
    }

    // Note: You'll need access to the private key from the keychain.
    // This may require extending Velada's CryptoManager to expose
    // a method for passcode derivation, or exposing the underlying Store.

    // ... derive passcode and connect ...
}

For Velada, consider adding a helper function:

// In velada/crypto.go (proposed addition)
func (cm *CryptoManager) DeriveHyperspacePasscode(contactID string) ([]byte, error) {
    // Get identity's X25519 key ID
    identity, err := cm.store.GetDefaultIdentity()
    if err != nil {
        return nil, err
    }

    // Load private key
    privBuf, _, err := cm.store.LoadSecret(identity.X25519KeyID)
    if err != nil {
        return nil, err
    }
    defer privBuf.Destroy()

    // Get contact's public key
    pubKeys, err := cm.addressBook.GetContactPublicKeys(contactID)
    if err != nil {
        return nil, err
    }

    // Derive passcode
    return hyperspace.DerivePasscodeFromKeys(privBuf.Bytes(), pubKeys.X25519PublicKey)
}

Security Properties

Property Implementation
Authentication Only parties with the correct private key can derive the shared secret
Deterministic Same keypair produces the same passcode every time
Domain separation Uses "hyperspace-v1" context to prevent cross-protocol attacks
Forward secrecy Not provided by this layer - use ephemeral keys if needed
Defense in depth Application should layer additional encryption (e.g., NaCl box)

Connection Pool

For managing multiple connections with automatic retries and state monitoring, use the Pool:

pool := hyperspace.NewPool(nil)
defer pool.Close()

// Add connections
err := pool.Connect(ctx, "peer-1", "passcode-1")

// Query connection state
info, err := pool.Get("peer-1")
fmt.Printf("State: %s\n", info.State)

// List connections by state
active := pool.ListByState(hyperspace.StateActive)
Pool Configuration
config := &hyperspace.PoolConfig{
    MaxRetries:     5,              // Default: 3
    RetryDelay:     10 * time.Second, // Default: 5s
    ConnectTimeout: 60 * time.Second, // Default: 30s
}
pool := hyperspace.NewPool(config)
Connection States
  • StatePending: Connection is being established
  • StateActive: Connection is ready for use
  • StateFailed: Connection failed after all retries
  • StateCancelled: Connection was explicitly cancelled
  • StateClosed: Connection was closed

Subscription API

Use the subscription API to receive real-time updates when connection states change:

pool := hyperspace.NewPool(nil)
defer pool.Close()

// Subscribe to all connection state changes
sub := pool.Subscribe()
defer sub.Unsubscribe()

// Handle events in a goroutine
go func() {
    for event := range sub.Events {
        fmt.Printf("Connection %s: %s -> %s\n",
            event.ID, event.OldState, event.NewState)
        
        switch event.NewState {
        case hyperspace.StateActive:
            // Connection ready - can now send/receive
            fmt.Printf("Connection %s is active!\n", event.ID)
        case hyperspace.StateFailed:
            // Connection failed
            fmt.Printf("Connection %s failed: %v\n", event.ID, event.Error)
        }
    }
}()

// Start connections - events will be received asynchronously
pool.Connect(ctx, "peer-1", "passcode-1")
pool.Connect(ctx, "peer-2", "passcode-2")
ConnectionEvent
type ConnectionEvent struct {
    ID        string           // Connection identifier
    OldState  ConnectionState // Previous state
    NewState  ConnectionState // New state
    Error     error            // Error if state is failed
    Timestamp time.Time        // When the event occurred
}
Subscription Methods
// Subscribe to all connection events
sub := pool.Subscribe()

// Events channel (buffered, size 100)
events := sub.Events

// Unsubscribe when done
sub.Unsubscribe()
Monitoring Multiple Connections
// Start multiple connections
pool.Connect(ctx, "peer-1", "passcode-1")
pool.Connect(ctx, "peer-2", "passcode-2")

// Monitor all connections
for event := range sub.Events {
    // Check if specific connection became active
    if event.ID == "peer-1" && event.NewState == hyperspace.StateActive {
        stream, _ := pool.Stream("peer-1")
        // Use the stream...
    }
    
    // Check if all connections are done
    active := pool.ListByState(hyperspace.StateActive)
    if len(active) == 2 {
        break
    }
}

How It Works

  1. X25519 Key Exchange: Both parties have X25519 keypairs. Each computes the shared secret using their private key and the peer's public key.

  2. Passcode Derivation: The shared secret is hashed with a context string:

    passcode = SHA-256(sharedSecret || "hyperspace-v1")
    
  3. z-base-32 Encoding: The passcode is encoded in z-base-32, a human-friendly encoding used by the Holepunch ecosystem.

  4. Hyperbeam Connection: The passcode is passed to hyperbeam, which uses it to derive a keypair for DHT announcement and encrypted connection establishment.

  5. NAT Traversal: Hyperbeam uses the DHT for peer discovery and UDP hole-punching for NAT traversal.

Constants

const (
    // PasscodeContext is the domain separation string
    PasscodeContext = "hyperspace-v1"

    // PasscodeSize is the size of the derived passcode in bytes
    PasscodeSize = 32
)

Errors

var (
    // ErrInvalidKeySize is returned when a key is not 32 bytes
    ErrInvalidKeySize = fmt.Errorf("key must be %d bytes", 32)

    // ErrHyperbeamNotFound is returned when hyperbeam is not installed
    ErrHyperbeamNotFound = fmt.Errorf("hyperbeam not found - install with: npm install -g hyperbeam")
)

License

MIT

Documentation

Overview

Package hyperspace provides a Go interface to Hyperbeam for P2P connections with connection pooling, automatic retries, and streaming support.

Hyperspace enables two parties with X25519 keypairs to establish a direct P2P connection through NAT traversal. The passcode is derived from the X25519 shared secret, allowing both parties to compute the same passcode without exchanging any additional information.

The package maintains a pool of connections that can be queried by ID, automatically retries failed connections, and supports bidirectional streaming.

Example (CancelAll)

Example_cancelAll demonstrates cancelling all connections.

package main

import (
	"context"
	"fmt"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	pool := hyperspace.NewPool(nil)
	defer pool.Close()

	ctx := context.Background()

	// Add multiple connections
	_ = pool.Connect(ctx, "conn-1", "passcode-1")
	_ = pool.Connect(ctx, "conn-2", "passcode-2")
	_ = pool.Connect(ctx, "conn-3", "passcode-3")

	// Cancel all connections at once
	errors := pool.CancelAll()
	for id, err := range errors {
		if err != nil {
			fmt.Printf("Failed to cancel %s: %v\n", id, err)
		}
	}

	// All connections are now in StateCancelled
	for _, info := range pool.List() {
		fmt.Printf("%s: %s\n", info.ID, info.State)
	}
}
Example (Cancellation)

Example_cancellation demonstrates cancelling connections.

package main

import (
	"context"
	"fmt"
	"log"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	pool := hyperspace.NewPool(nil)
	defer pool.Close()

	ctx := context.Background()

	// Start a connection
	_ = pool.Connect(ctx, "conn-1", "passcode-1")

	// Cancel a specific connection
	err := pool.Cancel("conn-1")
	if err != nil {
		log.Printf("Cancel failed: %v", err)
	}

	// Check connection state after cancellation
	info, err := pool.Get("conn-1")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Connection state: %s\n", info.State)
}
Example (CloseAll)

Example_closeAll demonstrates closing all connections.

package main

import (
	"context"
	"fmt"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	pool := hyperspace.NewPool(nil)
	defer pool.Close()

	ctx := context.Background()

	// Add multiple connections
	_ = pool.Connect(ctx, "conn-1", "passcode-1")
	_ = pool.Connect(ctx, "conn-2", "passcode-2")

	// Close all connections (gracefully)
	errors := pool.CloseAll()
	for id, err := range errors {
		if err != nil {
			fmt.Printf("Failed to close %s: %v\n", id, err)
		}
	}

	// All connections are now in StateClosed
	for _, info := range pool.List() {
		fmt.Printf("%s: %s\n", info.ID, info.State)
	}
}
Example (ConnectWithKeys)

Example_connectWithKeys demonstrates connecting using X25519 keys.

package main

import (
	"context"
	"fmt"
	"log"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	pool := hyperspace.NewPool(nil)
	defer pool.Close()

	// In a real application, these would come from the keychain:
	// myPrivateKey: from keychain.Store.LoadSecret(myKeyID)
	// theirPublicKey: from keychain.AddressBook.GetContactPublicKeys(contactID)

	// For demonstration, generate test keys
	myPrivateKey := make([]byte, 32)
	theirPublicKey := make([]byte, 32)
	for i := range myPrivateKey {
		myPrivateKey[i] = byte(i)
		theirPublicKey[i] = byte(i + 1)
	}

	ctx := context.Background()

	// Create a keypair
	kp := &hyperspace.KeyPair{
		PrivateKey: myPrivateKey,
		PublicKey:  theirPublicKey,
	}

	// Connect using keys (passcode derived automatically)
	err := pool.ConnectWithKeys(ctx, "peer-connection", "", kp)
	if err != nil {
		log.Fatal(err)
	}

	// List all connections
	connections := pool.List()
	for _, conn := range connections {
		fmt.Printf("Connection %s: %s\n", conn.ID, conn.State)
	}
}
Example (GetIDs)

Example_getIDs demonstrates getting all connection IDs.

package main

import (
	"context"
	"fmt"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	pool := hyperspace.NewPool(nil)
	defer pool.Close()

	ctx := context.Background()

	// Add multiple connections
	_ = pool.Connect(ctx, "alice", "passcode-1")
	_ = pool.Connect(ctx, "bob", "passcode-2")
	_ = pool.Connect(ctx, "charlie", "passcode-3")

	// Get all connection IDs
	ids := pool.IDs()
	fmt.Printf("Active connections: %v\n", ids)

	// Iterate over all connections
	for _, id := range ids {
		info, _ := pool.Get(id)
		fmt.Printf("%s: %s\n", id, info.State)
	}
}
Example (PasscodeEncoding)

ExamplePasscodeEncoding demonstrates encoding functions.

package main

import (
	"fmt"
	"log"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	// Create a passcode
	sharedSecret := make([]byte, 32)
	for i := range sharedSecret {
		sharedSecret[i] = byte(i)
	}

	passcode, _ := hyperspace.DerivePasscode(sharedSecret)

	// Z-base-32 encoding (what hyperbeam expects)
	z32 := hyperspace.PasscodeToZ32(passcode)
	fmt.Printf("z-base-32: %s\n", z32)

	// Hex encoding (for debugging)
	hex := hyperspace.PasscodeToHex(passcode)
	fmt.Printf("hex: %s\n", hex)

	// Decode z-base-32 back to bytes
	decoded, err := hyperspace.Z32ToPasscode(z32)
	if err != nil {
		log.Fatal(err)
	}
	_ = decoded
}
Example (Query)

Example_query demonstrates querying connections.

package main

import (
	"context"
	"fmt"
	"log"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	pool := hyperspace.NewPool(nil)
	defer pool.Close()

	ctx := context.Background()

	// Add multiple connections
	_ = pool.Connect(ctx, "conn-1", "passcode-1")
	_ = pool.Connect(ctx, "conn-2", "passcode-2")
	_ = pool.Connect(ctx, "conn-3", "passcode-3")

	// List all connections
	allConns := pool.List()
	fmt.Printf("Total connections: %d\n", len(allConns))

	// Get all connection IDs
	ids := pool.IDs()
	fmt.Printf("Connection IDs: %v\n", ids)

	// List only active connections
	activeConns := pool.ListActive()
	fmt.Printf("Active connections: %d\n", len(activeConns))

	// List connections by state
	pendingConns := pool.ListByState(hyperspace.StatePending)
	fmt.Printf("Pending connections: %d\n", len(pendingConns))

	// Get specific connection info
	info, err := pool.Get("conn-1")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Connection ID: %s, State: %s\n", info.ID, info.State)
}
Example (Subscription)

Example_subscription demonstrates using the subscription API.

package main

import (
	"context"
	"fmt"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	pool := hyperspace.NewPool(nil)
	defer pool.Close()

	// Subscribe to all connection state changes
	sub := pool.Subscribe()
	defer sub.Unsubscribe()

	// Start a goroutine to handle events
	go func() {
		for event := range sub.Events {
			fmt.Printf("Connection %s: %s -> %s\n",
				event.ID, event.OldState, event.NewState)

			// Handle specific state changes
			switch event.NewState {
			case hyperspace.StateActive:
				// Connection is ready - can now send/receive data
				fmt.Printf("Connection %s is now active!\n", event.ID)
			case hyperspace.StateFailed:
				// Connection failed after retries
				fmt.Printf("Connection %s failed: %v\n", event.ID, event.Error)
			case hyperspace.StateClosed:
				// Connection was closed
				fmt.Printf("Connection %s closed\n", event.ID)
			}
		}
	}()

	// Create a connection
	ctx := context.Background()
	_ = pool.Connect(ctx, "peer-1", "passcode")

	// The event handler will receive state updates:
	// - pending -> active (when connection succeeds)
	// - pending -> failed (when connection fails)
}
Example (SubscriptionMultiple)

Example_subscriptionMultiple demonstrates monitoring multiple connections.

package main

import (
	"context"
	"fmt"
	"log"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	pool := hyperspace.NewPool(nil)
	defer pool.Close()

	// Subscribe to all connection events
	sub := pool.Subscribe()
	defer sub.Unsubscribe()

	// Start multiple connections
	ctx := context.Background()
	_ = pool.Connect(ctx, "peer-1", "passcode-1")
	_ = pool.Connect(ctx, "peer-2", "passcode-2")
	_ = pool.Connect(ctx, "peer-3", "passcode-3")

	// Handle events from all connections
	for event := range sub.Events {
		fmt.Printf("[%s] %s -> %s\n", event.ID, event.OldState, event.NewState)

		// When a connection becomes active, we can use it
		if event.NewState == hyperspace.StateActive {
			stream, err := pool.Stream(event.ID)
			if err != nil {
				log.Printf("Failed to get stream for %s: %v", event.ID, err)
				continue
			}
			// Use the stream...
			_ = stream
		}

		// Stop when all connections are done
		active := pool.ListByState(hyperspace.StateActive)
		if len(active) == 3 {
			break
		}
	}
}

Index

Examples

Constants

View Source
const PasscodeContext = "hyperspace-v1"

PasscodeContext is the domain separation string for passcode derivation. This prevents cross-protocol attacks if the same X25519 keys are used for multiple purposes.

View Source
const PasscodeSize = 32

PasscodeSize is the size of the derived passcode in bytes (32 bytes = SHA-256 output).

Variables

View Source
var (
	ErrInvalidKeySize     = fmt.Errorf("key must be %d bytes", curve25519.PointSize)
	ErrHyperbeamNotFound  = fmt.Errorf("hyperbeam not found - install with: npm install -g hyperbeam")
	ErrPoolClosed         = fmt.Errorf("pool is closed")
	ErrConnectionExists   = fmt.Errorf("connection with this ID already exists")
	ErrConnectionNotFound = fmt.Errorf("connection not found")
	ErrConnectionNotReady = fmt.Errorf("connection is not ready")
	ErrMaxRetries         = fmt.Errorf("max retries exceeded")
	ErrCancelled          = fmt.Errorf("connection cancelled")
)

Errors returned by the package.

Functions

func Base32ToPasscode added in v1.2.0

func Base32ToPasscode(base32Str string) ([]byte, error)

Base32ToPasscode decodes a standard RFC 4648 base32 string to a passcode. Accepts both uppercase and lowercase.

func ComputeSharedSecret

func ComputeSharedSecret(privateKey, peerPublicKey []byte) ([]byte, error)

ComputeSharedSecret performs X25519 key exchange between a private key and a peer's public key.

func DefaultNodePath

func DefaultNodePath() string

DefaultNodePath returns the default path for Node.js installation.

func DerivePasscode

func DerivePasscode(sharedSecret []byte) ([]byte, error)

DerivePasscode derives a 32-byte passcode from an X25519 shared secret.

Example

ExampleDerivePasscode demonstrates passcode derivation.

package main

import (
	"fmt"
	"log"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	// X25519 keys (32 bytes each)
	myPrivateKey := make([]byte, 32)
	myPrivateKey[0] = 1 // Non-zero for valid key
	theirPublicKey := make([]byte, 32)
	theirPublicKey[0] = 2

	// Derive passcode
	passcode, err := hyperspace.DerivePasscodeFromKeys(myPrivateKey, theirPublicKey)
	if err != nil {
		log.Fatal(err)
	}

	// Convert to z-base-32 for hyperbeam
	z32 := hyperspace.PasscodeToZ32(passcode)
	fmt.Printf("Passcode: %s\n", z32)

}
Output:
Passcode: wtb1teqygth1xoni8cdssiu39ypb7n9i5pabjepuofde88qoccto

func DerivePasscodeFromKeys

func DerivePasscodeFromKeys(myPrivateKey, theirPublicKey []byte) ([]byte, error)

DerivePasscodeFromKeys derives a passcode directly from X25519 keypairs.

func EnsureHyperbeam

func EnsureHyperbeam() error

EnsureHyperbeam ensures hyperbeam is installed.

func HyperbeamPath

func HyperbeamPath() (string, error)

HyperbeamPath returns the path to the hyperbeam executable.

func InstallHyperbeam

func InstallHyperbeam() error

InstallHyperbeam attempts to install hyperbeam globally via npm.

func IsHyperbeamInstalled

func IsHyperbeamInstalled() bool

IsHyperbeamInstalled returns true if hyperbeam is available.

func NodePath

func NodePath() (string, error)

NodePath returns the path to node, or an error if not found.

func NpmPath

func NpmPath() (string, error)

NpmPath returns the path to npm, or an error if not found.

func PasscodeToBase32 added in v1.2.0

func PasscodeToBase32(passcode []byte) string

PasscodeToBase32 encodes a passcode to standard RFC 4648 base32 format. This is the format expected by hyperbeam. Uses lowercase encoding.

func PasscodeToHex

func PasscodeToHex(passcode []byte) string

PasscodeToHex converts a passcode to a hexadecimal string.

func PasscodeToZ32

func PasscodeToZ32(passcode []byte) string

PasscodeToZ32 encodes a passcode to z-base-32 format.

func Z32ToPasscode

func Z32ToPasscode(z32 string) ([]byte, error)

Z32ToPasscode decodes a z-base-32 string to a passcode.

Types

type Connection

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

Connection represents an active Hyperbeam connection.

func (*Connection) Close

func (c *Connection) Close() error

Close closes the connection and terminates the hyperbeam process.

func (*Connection) Done added in v1.0.0

func (c *Connection) Done() <-chan struct{}

Done returns a channel that's closed when the connection terminates.

func (*Connection) Read

func (c *Connection) Read(p []byte) (n int, err error)

Read reads data from the connection.

func (*Connection) Stderr

func (c *Connection) Stderr() io.Reader

Stderr returns a reader for the hyperbeam process's stderr output.

func (*Connection) Write

func (c *Connection) Write(p []byte) (n int, err error)

Write writes data to the connection.

type ConnectionEvent added in v1.1.0

type ConnectionEvent struct {
	// ID is the connection identifier.
	ID string `json:"id"`
	// OldState is the previous connection state.
	OldState ConnectionState `json:"oldState"`
	// NewState is the new connection state.
	NewState ConnectionState `json:"newState"`
	// Error contains error information if applicable.
	Error error `json:"error,omitempty"`
	// Timestamp is when the event occurred.
	Timestamp time.Time `json:"timestamp"`
}

ConnectionEvent represents a connection state change event.

type ConnectionInfo added in v1.0.0

type ConnectionInfo struct {
	// ID is the user-supplied unique identifier.
	ID string `json:"id"`
	// State is the current connection state.
	State ConnectionState `json:"state"`
	// RetryCount is the number of connection attempts made.
	RetryCount int `json:"retryCount"`
	// MaxRetries is the maximum number of retries allowed.
	MaxRetries int `json:"maxRetries"`
	// LastError is the most recent error encountered.
	LastError string `json:"lastError,omitempty"`
	// CreatedAt is when the connection was added to the pool.
	CreatedAt time.Time `json:"createdAt"`
	// LastAttemptAt is when the last connection attempt was made.
	LastAttemptAt time.Time `json:"lastAttemptAt,omitempty"`
	// ConnectedAt is when the connection became active (if applicable).
	ConnectedAt time.Time `json:"connectedAt,omitempty"`
	// PasscodeZ32 is the z-base-32 encoded passcode (for reference).
	PasscodeZ32 string `json:"passcodeZ32,omitempty"`
}

ConnectionInfo provides information about a connection in the pool.

type ConnectionRole added in v1.2.2

type ConnectionRole int

ConnectionRole determines whether to announce (listen) or connect.

const (
	// RoleAuto automatically determines role (first announcer, then connector)
	RoleAuto ConnectionRole = iota
	// RoleAnnounce creates a server and listens for incoming connections
	RoleAnnounce
	// RoleConnect connects to an existing announcer
	RoleConnect
)

type ConnectionState added in v1.0.0

type ConnectionState int

ConnectionState represents the current state of a connection.

const (
	// StatePending means the connection is being established (possibly retrying).
	StatePending ConnectionState = iota
	// StateActive means the connection is successfully established and ready.
	StateActive
	// StateFailed means the connection failed after exhausting all retries.
	StateFailed
	// StateCancelled means the connection was explicitly cancelled.
	StateCancelled
	// StateClosed means the connection was closed by user or peer.
	StateClosed
)

func (ConnectionState) String added in v1.0.0

func (s ConnectionState) String() string

type KeyPair added in v1.0.0

type KeyPair struct {
	PrivateKey []byte
	PublicKey  []byte
}

KeyPair represents an X25519 keypair for connection establishment.

type Pool added in v1.0.0

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

Pool manages a collection of Hyperbeam connections with automatic retries.

Example

ExamplePool demonstrates basic connection pool usage.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	// Create a connection pool with default configuration
	pool := hyperspace.NewPool(nil)
	defer pool.Close()

	// Check if hyperbeam is installed
	if !hyperspace.IsHyperbeamInstalled() {
		fmt.Println("Installing hyperbeam...")
		if err := hyperspace.InstallHyperbeam(); err != nil {
			log.Fatal(err)
		}
	}

	ctx := context.Background()

	// Connect with a passcode (derived from X25519 keys in a real app)
	passcode := "test-passcode-z32-encoded"
	err := pool.Connect(ctx, "my-connection-id", passcode)
	if err != nil {
		log.Fatal(err)
	}

	// Wait for connection to be ready (with timeout)
	waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	err = pool.WaitForReady(waitCtx, "my-connection-id")
	if err != nil {
		log.Fatal(err)
	}

	// Get a bidirectional stream
	stream, err := pool.Stream("my-connection-id")
	if err != nil {
		log.Fatal(err)
	}
	defer stream.Close()

	// Write to the stream
	_, _ = stream.Write([]byte("Hello, peer!"))
}

func NewPool added in v1.0.0

func NewPool(config *PoolConfig) *Pool

NewPool creates a new connection pool with the given configuration. If config is nil, default configuration is used.

func (*Pool) Cancel added in v1.0.0

func (p *Pool) Cancel(id string) error

Cancel explicitly cancels a connection. If the connection is active, it will be closed. If the connection is pending with retries, the retries will be cancelled. Returns ErrConnectionNotFound if the ID doesn't exist.

func (*Pool) CancelAll added in v1.0.0

func (p *Pool) CancelAll() map[string]error

CancelAll cancels all connections in the pool. This closes all active connections and stops any pending retries. Returns a map of connection IDs to errors (nil for successful cancellations).

func (*Pool) Close added in v1.0.0

func (p *Pool) Close() error

Close closes the pool and all connections. After Close, the pool cannot be used.

func (*Pool) CloseAll added in v1.0.0

func (p *Pool) CloseAll() map[string]error

CloseAll closes all connections but keeps the pool open for new connections. This is similar to CancelAll but doesn't set state to cancelled. Returns a map of connection IDs to errors.

func (*Pool) Connect added in v1.0.0

func (p *Pool) Connect(ctx context.Context, id string, passcode string) error

Connect establishes a new connection with the given ID and passcode. The ID must be unique within the pool.

The connection is established asynchronously with automatic retries. Use WaitForReady or Stream to block until the connection is active.

Returns ErrConnectionExists if a connection with the same ID already exists. This is a connector role - use ConnectAnnounce for the announcer role.

func (*Pool) ConnectAnnounce added in v1.2.2

func (p *Pool) ConnectAnnounce(ctx context.Context, id string, passcode string) error

ConnectAnnounce establishes a connection in announcer mode. The announcer creates a server and waits for incoming connections. Use this when you want to be the "host" that others connect to.

func (*Pool) ConnectWithKeys added in v1.0.0

func (p *Pool) ConnectWithKeys(ctx context.Context, id string, passcode string, keys *KeyPair) error

ConnectWithKeys establishes a connection using X25519 keys. The passcode is derived from the keys using DerivePasscodeFromKeys. If passcode is empty, it will be derived from the keys. Uses RoleConnect by default - for announcer role, use ConnectWithKeysAndRole.

func (*Pool) ConnectWithKeysAndRole added in v1.2.2

func (p *Pool) ConnectWithKeysAndRole(ctx context.Context, id string, passcode string, keys *KeyPair, role ConnectionRole) error

ConnectWithKeysAndRole establishes a connection using X25519 keys with a specific role.

func (*Pool) ConnectWithRole added in v1.2.2

func (p *Pool) ConnectWithRole(ctx context.Context, id string, passcode string, role ConnectionRole) error

ConnectWithRole establishes a connection with a specific role. RoleAnnounce creates a server and waits for connections. RoleConnect connects to an existing announcer.

func (*Pool) Get added in v1.0.0

func (p *Pool) Get(id string) (*ConnectionInfo, error)

Get retrieves a connection by ID. Returns ErrConnectionNotFound if the ID doesn't exist.

func (*Pool) IDs added in v1.0.0

func (p *Pool) IDs() []string

IDs returns all connection IDs in the pool.

func (*Pool) List added in v1.0.0

func (p *Pool) List() []ConnectionInfo

List returns information about all connections in the pool.

func (*Pool) ListActive added in v1.0.0

func (p *Pool) ListActive() []ConnectionInfo

ListActive returns all currently active connections.

func (*Pool) ListByState added in v1.0.0

func (p *Pool) ListByState(state ConnectionState) []ConnectionInfo

ListByState returns connections matching a specific state.

func (*Pool) Remove added in v1.0.0

func (p *Pool) Remove(id string) error

Remove removes a connection from the pool. This cancels the connection if it's pending and removes it from tracking. Returns ErrConnectionNotFound if the ID doesn't exist.

func (*Pool) Stream added in v1.0.0

func (p *Pool) Stream(id string) (io.ReadWriteCloser, error)

Stream returns a bidirectional data stream for an active connection. Returns ErrConnectionNotFound if the ID doesn't exist. Returns ErrConnectionNotReady if the connection is not in the active state.

func (*Pool) Subscribe added in v1.1.0

func (p *Pool) Subscribe() *Subscription

Subscribe creates a subscription for all connection state change events. The returned channel receives events whenever any connection's state changes. The channel has a buffer of 100 events. If the buffer is full, new events are dropped. Call Unsubscribe() on the returned Subscription to release resources.

func (*Pool) SubscribeConnection added in v1.1.0

func (p *Pool) SubscribeConnection(id string) *Subscription

SubscribeConnection creates a subscription for a specific connection's state changes. Only events for the specified connection ID will be sent.

func (*Pool) WaitForAllReady added in v1.0.0

func (p *Pool) WaitForAllReady(ctx context.Context) map[string]error

WaitForAllReady blocks until all connections are ready or context is cancelled. Returns a map of connection IDs to errors (nil for successful connections).

func (*Pool) WaitForReady added in v1.0.0

func (p *Pool) WaitForReady(ctx context.Context, id string) error

WaitForReady blocks until the connection is active or fails/cancels. Returns nil when the connection is ready, or an error if it fails/cancels.

type PoolConfig added in v1.0.0

type PoolConfig struct {
	// MaxRetries is the maximum number of connection retry attempts.
	// Default is 3.
	MaxRetries int
	// RetryDelay is the duration to wait between retry attempts.
	// Default is 5 seconds.
	RetryDelay time.Duration
	// ConnectTimeout is the timeout for each connection attempt.
	// Default is 30 seconds.
	ConnectTimeout time.Duration
}

PoolConfig configures the connection pool behavior.

Example

ExamplePoolConfig demonstrates custom pool configuration.

package main

import (
	"context"
	"time"

	"codeberg.org/OperatorFoundation/hyperspace"
)

func main() {
	// Custom configuration with more retries and longer timeouts
	config := &hyperspace.PoolConfig{
		MaxRetries:     5,
		RetryDelay:     10 * time.Second,
		ConnectTimeout: 60 * time.Second,
	}

	pool := hyperspace.NewPool(config)
	defer pool.Close()

	ctx := context.Background()
	_ = pool.Connect(ctx, "robust-connection", "passcode")

	// The connection will retry up to 5 times with 10s delay between attempts
}

func DefaultPoolConfig added in v1.0.0

func DefaultPoolConfig() PoolConfig

DefaultPoolConfig returns a PoolConfig with sensible defaults.

type Subscription added in v1.1.0

type Subscription struct {
	// Events is a channel for receiving connection events.
	// The channel is closed when the subscription is cancelled or the pool is closed.
	Events <-chan ConnectionEvent
	// contains filtered or unexported fields
}

Subscription represents an active event subscription.

func (*Subscription) Unsubscribe added in v1.1.0

func (s *Subscription) Unsubscribe()

Unsubscribe cancels the subscription.

Directories

Path Synopsis
cmd
hyperspace command
Command hyperspace provides a CLI for testing hyperspace connections using the velada keychain for contact management.
Command hyperspace provides a CLI for testing hyperspace connections using the velada keychain for contact management.

Jump to

Keyboard shortcuts

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